├── .gitignore ├── .rubocop.yml ├── .travis.yml ├── Gemfile ├── Gemfile.lock ├── MIT-LICENSE ├── README.md ├── Rakefile ├── app └── models │ └── survey │ ├── answer.rb │ ├── attempt.rb │ ├── option.rb │ ├── options_type.rb │ ├── predefined_value.rb │ ├── question.rb │ ├── questions_type.rb │ ├── section.rb │ └── survey.rb ├── config └── locales │ ├── en.yml │ ├── pt-PT.yml │ └── pt.yml ├── lib ├── generators │ ├── survey │ │ ├── install_generator.rb │ │ └── survey_generator.rb │ └── templates │ │ ├── active_admin.rb │ │ ├── attempts_plain.rb │ │ ├── attempts_views │ │ ├── _form.html.erb │ │ └── new.html.erb │ │ ├── helper.rb │ │ ├── migration.rb │ │ ├── migration_add_head_number_to_options_table.rb │ │ ├── migration_add_mandatory_to_questions_table.rb │ │ ├── migration_add_types_to_questions_and_options.rb │ │ ├── migration_create_predefined_values_table.rb │ │ ├── migration_section.rb │ │ ├── migration_update_survey_tables.rb │ │ ├── rails_admin.rb │ │ ├── survey_plain.rb │ │ └── survey_views │ │ ├── _form.html.erb │ │ ├── _option_fields.html.erb │ │ ├── _question_fields.html.erb │ │ ├── _section_fields.html.erb │ │ ├── edit.html.erb │ │ ├── index.html.erb │ │ └── new.html.erb ├── survey.rb └── survey │ ├── active_record.rb │ ├── engine.rb │ └── version.rb ├── survey.gemspec └── test ├── dummy ├── Gemfile ├── Gemfile.lock ├── Rakefile ├── app │ ├── assets │ │ ├── javascripts │ │ │ ├── application.js │ │ │ └── users.js │ │ └── stylesheets │ │ │ └── users.css │ ├── controllers │ │ ├── application_controller.rb │ │ ├── users_controller.rb │ │ └── welcome_controller.rb │ ├── helpers │ │ ├── application_helper.rb │ │ └── users_helper.rb │ ├── models │ │ ├── lesson.rb │ │ ├── survey │ │ │ └── belongs_to_lesson.rb │ │ └── user.rb │ └── views │ │ └── layouts │ │ └── application.html.erb ├── config.ru ├── config │ ├── application.rb │ ├── boot.rb │ ├── database.yml │ ├── environment.rb │ ├── environments │ │ ├── development.rb │ │ ├── production.rb │ │ └── test.rb │ ├── initializers │ │ ├── backtrace_silencers.rb │ │ ├── inflections.rb │ │ ├── mime_types.rb │ │ ├── secret_token.rb │ │ ├── session_store.rb │ │ └── survey.rb │ ├── locales │ │ ├── devise.en.yml │ │ └── en.yml │ └── routes.rb ├── db │ ├── migrate │ │ ├── 20130123110019_create_users.rb │ │ ├── 20130201105206_create_survey.rb │ │ ├── 20130904084520_create_sections.rb │ │ ├── 20130904115621_update_survey_tables.rb │ │ ├── 20130905093710_add_types_to_questions_and_options.rb │ │ ├── 20130916081314_add_head_number_to_options_table.rb │ │ ├── 20130916102353_create_predefined_values_table.rb │ │ ├── 20130929102221_add_mandatory_to_questions_table.rb │ │ ├── 20170619155054_create_lessons.rb │ │ └── 20170619155608_add_lesson_id_to_survey_surveys.rb │ └── schema.rb ├── public │ ├── 404.html │ ├── 422.html │ ├── 500.html │ ├── favicon.ico │ ├── javascripts │ │ ├── application.js │ │ ├── controls.js │ │ ├── dragdrop.js │ │ ├── effects.js │ │ ├── prototype.js │ │ └── rails.js │ └── stylesheets │ │ └── .gitkeep └── script │ └── rails ├── models ├── answer_test.rb ├── attempt_test.rb ├── option_test.rb ├── predefined_value_test.rb ├── question_test.rb ├── section_test.rb └── survey_test.rb ├── support ├── assertions.rb ├── factories.rb └── handlers.rb ├── survey_test.rb └── test_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | .bundle/ 2 | .project 3 | log/*.log 4 | pkg/ 5 | test/dummy/db/*.sqlite3 6 | test/dummy/log/*.log 7 | test/dummy/tmp/ 8 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | TargetRubyVersion: 2.3 3 | TargetRailsVersion: 4.0 4 | Exclude: 5 | - 'tmp/**/*' 6 | - 'bin/**/*' 7 | - 'db/**/*' 8 | - 'test/fixtures/**/*' 9 | - 'Gemfile' 10 | - 'Rakefile' 11 | - 'node_modules/**/*' 12 | - 'Vagrantfile' 13 | Rails: 14 | Enabled: true 15 | 16 | Metrics/MethodLength: 17 | Max: 35 18 | Metrics/LineLength: 19 | Max: 120 20 | Metrics/AbcSize: 21 | # The ABC size is a calculated magnitude, so this number can be a Fixnum or 22 | # a Float. 23 | Max: 35 24 | Metrics/PerceivedComplexity: 25 | Max: 10 26 | Metrics/CyclomaticComplexity: 27 | Max: 10 28 | Metrics/ClassLength: 29 | CountComments: false # count full line comments? 30 | Max: 175 31 | 32 | Rails/HttpPositionalArguments: 33 | # see https://github.com/bbatsov/rubocop/issues/3629, this is for Rails 5 only 34 | Enabled: false 35 | 36 | Style/NumericLiterals: 37 | Enabled: false 38 | Style/Documentation: 39 | Description: 'Document classes and non-namespace modules.' 40 | Enabled: false 41 | Style/AsciiComments: 42 | Enabled: false 43 | Style/StructInheritance: 44 | Enabled: false 45 | Style/ClassAndModuleChildren: 46 | Enabled: false 47 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 1.9.3 4 | - 2.0.0 -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | ruby '2.4.1' 3 | 4 | gemspec 5 | 6 | gem "rdoc" 7 | 8 | group :test do 9 | gem 'sqlite3' 10 | end 11 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | questionnaire_engine (0.1) 5 | rails (~> 5.1) 6 | 7 | GEM 8 | remote: https://rubygems.org/ 9 | specs: 10 | actioncable (5.2.8.1) 11 | actionpack (= 5.2.8.1) 12 | nio4r (~> 2.0) 13 | websocket-driver (>= 0.6.1) 14 | actionmailer (5.2.8.1) 15 | actionpack (= 5.2.8.1) 16 | actionview (= 5.2.8.1) 17 | activejob (= 5.2.8.1) 18 | mail (~> 2.5, >= 2.5.4) 19 | rails-dom-testing (~> 2.0) 20 | actionpack (5.2.8.1) 21 | actionview (= 5.2.8.1) 22 | activesupport (= 5.2.8.1) 23 | rack (~> 2.0, >= 2.0.8) 24 | rack-test (>= 0.6.3) 25 | rails-dom-testing (~> 2.0) 26 | rails-html-sanitizer (~> 1.0, >= 1.0.2) 27 | actionview (5.2.8.1) 28 | activesupport (= 5.2.8.1) 29 | builder (~> 3.1) 30 | erubi (~> 1.4) 31 | rails-dom-testing (~> 2.0) 32 | rails-html-sanitizer (~> 1.0, >= 1.0.3) 33 | activejob (5.2.8.1) 34 | activesupport (= 5.2.8.1) 35 | globalid (>= 0.3.6) 36 | activemodel (5.2.8.1) 37 | activesupport (= 5.2.8.1) 38 | activerecord (5.2.8.1) 39 | activemodel (= 5.2.8.1) 40 | activesupport (= 5.2.8.1) 41 | arel (>= 9.0) 42 | activestorage (5.2.8.1) 43 | actionpack (= 5.2.8.1) 44 | activerecord (= 5.2.8.1) 45 | marcel (~> 1.0.0) 46 | activesupport (5.2.8.1) 47 | concurrent-ruby (~> 1.0, >= 1.0.2) 48 | i18n (>= 0.7, < 2) 49 | minitest (~> 5.1) 50 | tzinfo (~> 1.1) 51 | arel (9.0.0) 52 | ast (2.3.0) 53 | builder (3.2.4) 54 | byebug (9.0.6) 55 | coderay (1.1.1) 56 | concurrent-ruby (1.1.10) 57 | crass (1.0.6) 58 | erubi (1.10.0) 59 | faker (1.2.0) 60 | i18n (~> 0.5) 61 | globalid (0.4.2) 62 | activesupport (>= 4.2.0) 63 | i18n (0.9.5) 64 | concurrent-ruby (~> 1.0) 65 | loofah (2.19.1) 66 | crass (~> 1.0.2) 67 | nokogiri (>= 1.5.9) 68 | mail (2.7.1) 69 | mini_mime (>= 0.1.1) 70 | marcel (1.0.2) 71 | metaclass (0.0.1) 72 | method_source (0.8.2) 73 | mini_mime (1.1.2) 74 | mini_portile2 (2.4.0) 75 | minitest (5.15.0) 76 | mocha (0.14.0) 77 | metaclass (~> 0.0.1) 78 | nio4r (2.5.8) 79 | nokogiri (1.10.10) 80 | mini_portile2 (~> 2.4.0) 81 | parallel (1.11.2) 82 | parser (2.4.0.0) 83 | ast (~> 2.2) 84 | powerpack (0.1.1) 85 | pry (0.10.4) 86 | coderay (~> 1.1.0) 87 | method_source (~> 0.8.1) 88 | slop (~> 3.4) 89 | pry-byebug (3.4.2) 90 | byebug (~> 9.0) 91 | pry (~> 0.10) 92 | pry-rails (0.3.6) 93 | pry (>= 0.10.4) 94 | rack (2.2.8.1) 95 | rack-test (2.0.2) 96 | rack (>= 1.3) 97 | rails (5.2.8.1) 98 | actioncable (= 5.2.8.1) 99 | actionmailer (= 5.2.8.1) 100 | actionpack (= 5.2.8.1) 101 | actionview (= 5.2.8.1) 102 | activejob (= 5.2.8.1) 103 | activemodel (= 5.2.8.1) 104 | activerecord (= 5.2.8.1) 105 | activestorage (= 5.2.8.1) 106 | activesupport (= 5.2.8.1) 107 | bundler (>= 1.3.0) 108 | railties (= 5.2.8.1) 109 | sprockets-rails (>= 2.0.0) 110 | rails-dom-testing (2.0.3) 111 | activesupport (>= 4.2.0) 112 | nokogiri (>= 1.6) 113 | rails-html-sanitizer (1.4.4) 114 | loofah (~> 2.19, >= 2.19.1) 115 | railties (5.2.8.1) 116 | actionpack (= 5.2.8.1) 117 | activesupport (= 5.2.8.1) 118 | method_source 119 | rake (>= 0.8.7) 120 | thor (>= 0.19.0, < 2.0) 121 | rainbow (2.2.2) 122 | rake 123 | rake (13.0.1) 124 | rdoc (6.3.4.1) 125 | rubocop (0.49.1) 126 | parallel (~> 1.10) 127 | parser (>= 2.3.3.1, < 3.0) 128 | powerpack (~> 0.1) 129 | rainbow (>= 1.99.1, < 3.0) 130 | ruby-progressbar (~> 1.7) 131 | unicode-display_width (~> 1.0, >= 1.0.1) 132 | ruby-progressbar (1.8.1) 133 | slop (3.6.0) 134 | sprockets (3.7.2) 135 | concurrent-ruby (~> 1.0) 136 | rack (> 1, < 3) 137 | sprockets-rails (3.2.2) 138 | actionpack (>= 4.0) 139 | activesupport (>= 4.0) 140 | sprockets (>= 3.0.0) 141 | sqlite3 (1.3.13) 142 | thor (1.2.1) 143 | thread_safe (0.3.6) 144 | tzinfo (1.2.10) 145 | thread_safe (~> 0.1) 146 | unicode-display_width (1.3.0) 147 | websocket-driver (0.7.5) 148 | websocket-extensions (>= 0.1.0) 149 | websocket-extensions (0.1.5) 150 | 151 | PLATFORMS 152 | ruby 153 | 154 | DEPENDENCIES 155 | faker 156 | mocha 157 | pry-byebug 158 | pry-rails 159 | questionnaire_engine! 160 | rake 161 | rdoc 162 | rubocop 163 | sqlite3 164 | 165 | RUBY VERSION 166 | ruby 2.4.1p111 167 | 168 | BUNDLED WITH 169 | 1.16.2 170 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2013 Runtime Revolution 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Questionnaire 2 | 3 | [![Code Climate](https://codeclimate.com/github/dr-click/survey.png)](https://codeclimate.com/github/dr-click/questionnaire) 4 | ### Questionnaire on Rails... 5 | 6 | Questionnaire is a Rails Engine that brings multi types of quizzes, surveys and contests into your Rails 7 | application. Questionnaire models were designed to be flexible enough in order to be extended and 8 | integrated with your own models. Questionnaire was initially extracted from a real application that handles contests and quizzes. 9 | 10 | ## Documentation 11 | 12 | You can view the Questionnaire documentation in RDoc format here: 13 | 14 | http://rubydoc.info/github/dr-click/questionnaire/master/frames 15 | 16 | ## Main Features: 17 | - Questionnaire can limit the number of attempts for each participant, can have multiple sections 18 | - Sections can have multiple questions 19 | - Questions can have multiple answers 20 | - Answers can have different weights and types (multi choices, single choice, number, text) 21 | - Can use 2 languages (Main language field, Localized field) for Surveys, Sections, Questions and Answers attributes 22 | - Base Scaffold Support for Active Admin, Rails Admin and default Rails Controllers 23 | - Base calculation for scores 24 | - Easy integration with your project 25 | 26 | ## Installation 27 | 28 | Add survey to your Gemfile: 29 | ```ruby 30 | gem 'questionnaire_engine', '0.1', :require=>"survey" 31 | 32 | ``` 33 | or 34 | ```ruby 35 | gem 'questionnaire_engine', github: 'dr-click/questionnaire', branch: 'master', :require=>"survey" 36 | 37 | ``` 38 | or use this for Rails 5 39 | ```ruby 40 | gem 'questionnaire_engine', github: 'clearfunction/questionnaire', branch: 'master', :require=>"survey" 41 | 42 | ``` 43 | Then run bundle to install the Gem: 44 | ```sh 45 | bundle install 46 | ``` 47 | Now generate and run migrations: 48 | ```sh 49 | rails generate survey:install 50 | 51 | bundle exec rake db:migrate 52 | ``` 53 | 54 | ## Important notice for Rails 5.1 55 | Add Rails version to all generated migrations. Example 56 | 57 | ```ruby 58 | class CreateSurvey < ActiveRecord::Migration # change to: class CreateSurvey < ActiveRecord::Migration[5.1] 59 | ``` 60 | 61 | ## Getting started with Survey 62 | 63 | ## Survey inside your models 64 | To make a model aware of you just need to add `has_surveys` on it: 65 | ```ruby 66 | class User < ActiveRecord::Base 67 | has_surveys 68 | 69 | #... (your code) ... 70 | end 71 | ``` 72 | There is the concept of participant, in our example we choose the User Model. 73 | Every participant can respond to surveys and every response is registered as a attempt. 74 | By default, survey logic assumes an infinite number of attempts per participant 75 | but if your surveys need to have a maximum number of attempts 76 | you can pass the attribute `attempts_number` when creating them. 77 | ```ruby 78 | # Each Participant can respond 4 times this survey 79 | Survey::Survey.new(:name => "Star Wars Quiz", :attempts_number => 4) 80 | ``` 81 | ## Questionnaire used in your controllers 82 | In this example we are using the current_user helper 83 | but you can do it in the way you want. 84 | 85 | ```ruby 86 | class ContestsController < ApplicationController 87 | 88 | helper_method :survey, :participant 89 | 90 | # create a new attempt to this survey 91 | def new 92 | @survey = Survey::Survey.active.last 93 | @attempt = @survey.attempts.new 94 | @attempt.answers.build 95 | @participant = current_user # you have to decide what to do here 96 | end 97 | 98 | # create a new attempt in this survey 99 | # an attempt needs to have a participant assigned 100 | def create 101 | @survey = Survey::Survey.active.last 102 | @attempt = @survey.attempts.new(attempt_params) 103 | @attempt.participant = current_user 104 | if @attempt.valid? and @attempt.save 105 | redirect_to view_context.new_attempt_path, alert: I18n.t("attempts_controller.#{action_name}") 106 | else 107 | flash.now[:error] = @attempt.errors.full_messages.join(', ') 108 | render :action => :new 109 | end 110 | end 111 | 112 | ####### 113 | private 114 | ####### 115 | 116 | # Rails 4 Strong Params 117 | def attempt_params 118 | if Rails::VERSION::MAJOR < 4 119 | params[:survey_attempt] 120 | else 121 | params.require(:survey_attempt).permit(answers_attributes: [:id, :question_id, :option_id, :option_text, :option_number, :predefined_value_id, :_destroy, :finished]) 122 | end 123 | end 124 | 125 | end 126 | ``` 127 | 128 | ## Survey Associations 129 | To add a survey to a particular model (model has_many :surveys), use the `has_many_surveys` helper 130 | ```ruby 131 | class Lesson < ActiveRecord::Base 132 | has_many_surveys 133 | 134 | #... (your code) ... 135 | end 136 | ``` 137 | Then, create a module mixin that adds `belongs_to` to the survey model based on your class name 138 | ```ruby 139 | # app/models/survey/belongs_to_lesson.rb 140 | module Survey 141 | module BelongsToLesson 142 | extend ActiveSupport::Concern 143 | included do 144 | belongs_to :lesson 145 | end 146 | end 147 | end 148 | ``` 149 | This will dynamically add the association to the survey model. However, you will need to generate a migration in order to add the foreign key to the `survey_surveys` table like so: 150 | ```ruby 151 | class AddLessonIdToSurveySurveys < ActiveRecord::Migration 152 | def change 153 | add_column :survey_surveys, :lesson_id, :integer 154 | end 155 | end 156 | ``` 157 | 158 | 159 | ## Survey inside your Views 160 | 161 | ### Controlling Survey avaliability per participant 162 | To control which page participants see you can use method `avaliable_for_participant?` 163 | that checks if the participant already spent his attempts. 164 | ```erb 165 |

<%= flash[:alert]%>

166 |

<%= flash[:error]%>

167 | 168 | <% if @survey.avaliable_for_participant?(@participant) %> 169 | <%= render 'form' %> 170 | <% else %> 171 |

172 | <%= @participant.name %> spent all the possible attempts to answer this Survey 173 |

174 | <% end -%> 175 | 176 | <% # in _form.html.erb %> 177 |

<%= @survey.name %>

178 |

<%= @survey.description %>

179 | <%= form_for(@attempt, :url => attempt_scope(@attempt)) do |f| %> 180 | <%= f.fields_for :answers do |builder| %> 181 | 219 | <% end -%> 220 | <%= f.submit "Submit" %> 221 | <% end -%> 222 | ``` 223 | 224 | ### Scaffolds and CRUD frameworks 225 | If you are using Rails Admin or Active Admin, you can generate base CRUD screens for Survey with: 226 | ```sh 227 | rails generate survey active_admin 228 | 229 | rails generate survey rails_admin 230 | ``` 231 | If you want a simple way to get started you can use the `plain` option which is a simple Rails scaffold to generate the controller and views related with survey logic. 232 | By default when you type `rails g survey plain` it generates a controller in the `admin` namespace but you can choose your own namespace as well: 233 | ```sh 234 | rails generate survey plain namespace:contests 235 | ``` 236 | 237 | By default when you generates your controllers using the `plain` command the task 238 | generates the associated routes as well. 239 | Afterwards if you want to generate more routes, you can using the command: 240 | 241 | ```sh 242 | rails generate survey routes namespace:admin 243 | ``` 244 | 245 | 246 | ## How to use it 247 | Every user has a collection of attempts for each survey that he respond to. Is up to you to 248 | make averages and collect reports based on that information. 249 | What makes Survey useful is that all the logic behind surveys is now abstracted and well integrated, 250 | making your job easier. 251 | 252 | ## Hacking with Survey through your Models: 253 | 254 | ```ruby 255 | # select the first active Survey 256 | survey = Survey::Survey.active.first 257 | 258 | # select all the attempts from this survey 259 | survey_answers = survey.attempts 260 | 261 | # check the highest score for current user 262 | user_highest_score = survey_answers.for_participant(@user).high_score 263 | 264 | #check the highest score made for this survey 265 | global_highest_score = survey_answers.high_score 266 | ``` 267 | # Compability 268 | ### Rails 269 | Survey supports Rails 3 and 4. For use in Rails 4 without using protected_attributes gem. 270 | Rails 4 support is recent, so some minor issues may still be present, please report them. 271 | 272 | ### Active Admin 273 | Only support versions of Active Admin higher than 0.3.1. 274 | 275 | # License 276 | - Modified by [Dr-Click](http://github.com/dr-click) 277 | - Copyright © 2013 [Runtime Revolution](http://www.runtime-revolution.com), released under the MIT license. 278 | - This repository was forked from the original one : https://github.com/runtimerevolution/survey 279 | 280 | 281 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | require 'rubygems' 3 | begin 4 | require 'bundler/setup' 5 | rescue LoadError 6 | puts 'You must `gem install bundler` and `bundle install` to run rake tasks' 7 | end 8 | 9 | require 'rake' 10 | require 'rdoc/task' 11 | 12 | require 'rake/testtask' 13 | 14 | Rake::TestTask.new(:test) do |t| 15 | t.libs << 'lib' 16 | t.libs << 'test' 17 | t.pattern = 'test/**/*_test.rb' 18 | t.verbose = true 19 | end 20 | 21 | task :default => :test 22 | 23 | Rake::RDocTask.new(:rdoc) do |rdoc| 24 | rdoc.rdoc_dir = 'rdoc' 25 | rdoc.title = 'Survey' 26 | rdoc.options << '--line-numbers' << '--inline-source' 27 | rdoc.rdoc_files.include('README.md') 28 | rdoc.rdoc_files.include('lib/**/*.rb') 29 | rdoc.rdoc_files.include('app/**/*.rb') 30 | end -------------------------------------------------------------------------------- /app/models/survey/answer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Survey::Answer < ActiveRecord::Base 4 | self.table_name = 'survey_answers' 5 | belongs_to :attempt 6 | belongs_to :option 7 | belongs_to :predefined_value 8 | belongs_to :question 9 | 10 | validates :option_id, :question_id, presence: true 11 | validates :predefined_value_id, presence: true, if: proc { |a| a.question && a.question.mandatory? && a.question.predefined_values.count > 0 && ![Survey::OptionsType.text, Survey::OptionsType.large_text].include?(a.option.options_type_id) } 12 | validates :option_text, presence: true, if: proc { |a| a.option && (a.question && a.question.mandatory? && a.question.predefined_values.count == 0 && [Survey::OptionsType.text, Survey::OptionsType.multi_choices_with_text, Survey::OptionsType.single_choice_with_text, Survey::OptionsType.large_text].include?(a.option.options_type_id)) } 13 | validates :option_number, presence: true, if: proc { |a| a.option && (a.question && a.question.mandatory? && [Survey::OptionsType.number, Survey::OptionsType.multi_choices_with_number, Survey::OptionsType.single_choice_with_number].include?(a.option.options_type_id)) } 14 | 15 | # rails 3 attr_accessible support 16 | if Rails::VERSION::MAJOR < 4 17 | attr_accessible :option, :attempt, :question, :question_id, :option_id, :predefined_value_id, :attempt_id, :option_text, :option_number 18 | end 19 | 20 | before_create :characterize_answer 21 | before_save :check_single_choice_with_field_case 22 | 23 | def value 24 | if option.nil? 25 | Survey::Option.find(option_id).weight 26 | else 27 | option.weight 28 | end 29 | end 30 | 31 | def correct? 32 | correct || option.correct? 33 | end 34 | 35 | ####### 36 | 37 | private 38 | 39 | ####### 40 | 41 | def characterize_answer 42 | self.correct = true if option.correct? 43 | end 44 | 45 | def check_single_choice_with_field_case 46 | if [Survey::OptionsType.multi_choices, Survey::OptionsType.single_choice].include?(option.options_type_id) 47 | self.option_text = nil 48 | self.option_number = nil 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /app/models/survey/attempt.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Survey::Attempt < ActiveRecord::Base 4 | self.table_name = 'survey_attempts' 5 | 6 | # relations 7 | 8 | has_many :answers, dependent: :destroy 9 | belongs_to :survey 10 | belongs_to :participant, polymorphic: true 11 | 12 | # rails 3 attr_accessible support 13 | if Rails::VERSION::MAJOR < 4 14 | attr_accessible :participant_id, :survey_id, :answers_attributes, :survey, :winner, :participant 15 | end 16 | 17 | # validations 18 | validates :participant_id, :participant_type, 19 | presence: true 20 | 21 | accepts_nested_attributes_for :answers, 22 | reject_if: ->(q) { q[:question_id].blank? && q[:option_id].blank? }, 23 | allow_destroy: true 24 | 25 | # scopes 26 | 27 | scope :for_survey, ->(survey) { 28 | where(survey_id: survey.try(:id)) 29 | } 30 | 31 | scope :exclude_survey, ->(survey) { 32 | where("NOT survey_id = #{survey.try(:id)}") 33 | } 34 | 35 | scope :for_participant, ->(participant) { 36 | where(participant_id: participant.try(:id), 37 | participant_type: participant.class.to_s) 38 | } 39 | 40 | scope :wins, -> { where(winner: true) } 41 | scope :looses, -> { where(winner: false) } 42 | scope :scores, -> { order('score DESC') } 43 | 44 | # callbacks 45 | 46 | validate :check_number_of_attempts_by_survey, on: :create 47 | after_create :collect_scores 48 | 49 | def correct_answers 50 | answers.where(correct: true) 51 | end 52 | 53 | def incorrect_answers 54 | answers.where(correct: false) 55 | end 56 | 57 | def collect_scores! 58 | collect_scores 59 | save 60 | end 61 | 62 | def self.high_score 63 | scores.first.score 64 | end 65 | 66 | private 67 | 68 | def check_number_of_attempts_by_survey 69 | attempts = self.class.for_survey(survey).for_participant(participant) 70 | upper_bound = survey.attempts_number 71 | errors.add(:questionnaire_id, 'Number of attempts exceeded') if attempts.size >= upper_bound && upper_bound.nonzero? 72 | end 73 | 74 | def collect_scores 75 | multi_select_questions = Survey::Question.joins(:section) 76 | .where(survey_sections: { survey_id: survey.id }, 77 | survey_questions: { 78 | questions_type_id: Survey::QuestionsType.multi_select 79 | }) 80 | if multi_select_questions.empty? # No multi-select questions 81 | raw_score = answers.map(&:value).reduce(:+) 82 | self.score = raw_score 83 | else 84 | # Initial score without multi-select questions 85 | raw_score = answers.where.not(question_id: multi_select_questions.ids).map(&:value).reduce(:+) || 0 86 | multi_select_questions.each do |question| 87 | options = question.options 88 | correct_question_answers = answers.where(question_id: question.id, correct: true) 89 | break if correct_question_answers.empty? # If they didn't select any correct answers, then skip this step 90 | correct_options_sum = options.correct.map(&:weight).reduce(:+) 91 | correct_percentage = correct_question_answers.map(&:value).reduce(:+).fdiv(correct_options_sum) 92 | raw_score += correct_percentage 93 | if correct_percentage == 1 94 | option_value = 1 / options.count.to_f 95 | raw_score -= (option_value * answers.where(question_id: question.id, correct: false).count) 96 | end 97 | end 98 | self.score = raw_score || 0 99 | save 100 | end 101 | end 102 | end 103 | -------------------------------------------------------------------------------- /app/models/survey/option.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Survey::Option < ActiveRecord::Base 4 | self.table_name = 'survey_options' 5 | # relations 6 | belongs_to :question 7 | has_many :answers 8 | 9 | # rails 3 attr_accessible support 10 | if Rails::VERSION::MAJOR < 4 11 | attr_accessible :text, :correct, :weight, :question_id, :locale_text, :options_type_id, :head_number 12 | end 13 | 14 | # validations 15 | validates :text, presence: true, allow_blank: false, if: proc { |o| [Survey::OptionsType.multi_choices, Survey::OptionsType.single_choice, Survey::OptionsType.single_choice_with_text, Survey::OptionsType.single_choice_with_number, Survey::OptionsType.multi_choices_with_text, Survey::OptionsType.multi_choices_with_number, Survey::OptionsType.large_text].include?(o.options_type_id) } 16 | validates :options_type_id, presence: true 17 | validates :options_type_id, inclusion: { in: Survey::OptionsType.options_type_ids, unless: proc { |o| o.options_type_id.blank? } } 18 | 19 | scope :correct, -> { where(correct: true) } 20 | scope :incorrect, -> { where(correct: false) } 21 | 22 | before_create :default_option_weigth 23 | 24 | def to_s 25 | text 26 | end 27 | 28 | def correct? 29 | correct == true 30 | end 31 | 32 | def text 33 | I18n.locale == I18n.default_locale ? super : locale_text.blank? ? super : locale_text 34 | end 35 | 36 | ####### 37 | 38 | private 39 | 40 | ####### 41 | 42 | def default_option_weigth 43 | self.weight = 1 if correct && weight == 0 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /app/models/survey/options_type.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Survey::OptionsType 4 | @@options_types = { multi_choices: 1, 5 | single_choice: 2, 6 | number: 3, 7 | text: 4, 8 | multi_choices_with_text: 5, 9 | single_choice_with_text: 6, 10 | multi_choices_with_number: 7, 11 | single_choice_with_number: 8, 12 | large_text: 9 } 13 | 14 | def self.options_types 15 | @@options_types 16 | end 17 | 18 | def self.options_types_title 19 | titled = {} 20 | Survey::OptionsType.options_types.each { |k, v| titled[k.to_s.titleize] = v } 21 | titled 22 | end 23 | 24 | def self.options_type_ids 25 | @@options_types.values 26 | end 27 | 28 | def self.options_type_keys 29 | @@options_types.keys 30 | end 31 | 32 | @@options_types.each do |key, val| 33 | define_singleton_method key.to_s do 34 | val 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /app/models/survey/predefined_value.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Survey::PredefinedValue < ActiveRecord::Base 4 | self.table_name = 'survey_predefined_values' 5 | 6 | # relations 7 | belongs_to :question 8 | 9 | # rails 3 attr_accessible support 10 | if Rails::VERSION::MAJOR < 4 11 | attr_accessible :head_number, :name, :locale_name, :question_id 12 | end 13 | 14 | # validations 15 | validates :name, presence: true 16 | 17 | def to_s 18 | name 19 | end 20 | 21 | def name 22 | I18n.locale == I18n.default_locale ? super : locale_name || super 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /app/models/survey/question.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Survey::Question < ActiveRecord::Base 4 | self.table_name = 'survey_questions' 5 | # relations 6 | has_many :options 7 | has_many :predefined_values 8 | has_many :answers 9 | belongs_to :section 10 | 11 | # rails 3 attr_accessible support 12 | if Rails::VERSION::MAJOR < 4 13 | attr_accessible :options_attributes, :predefined_values_attributes, :text, :section_id, :head_number, :description, :locale_text, :locale_head_number, :locale_description, :questions_type_id 14 | end 15 | 16 | accepts_nested_attributes_for :options, 17 | reject_if: ->(a) { a[:options_type_id].blank? }, 18 | allow_destroy: true 19 | 20 | accepts_nested_attributes_for :predefined_values, 21 | reject_if: ->(a) { a[:name].blank? }, 22 | allow_destroy: true 23 | 24 | # validations 25 | validates :text, presence: true, allow_blank: false 26 | validates :questions_type_id, presence: true 27 | validates :questions_type_id, inclusion: { in: Survey::QuestionsType.questions_type_ids, unless: proc { |q| q.questions_type_id.blank? } } 28 | 29 | scope :mandatory_only, -> { where(mandatory: true) } 30 | 31 | def correct_options 32 | options.correct 33 | end 34 | 35 | def incorrect_options 36 | options.incorrect 37 | end 38 | 39 | def text 40 | I18n.locale == I18n.default_locale ? super : locale_text.blank? ? super : locale_text 41 | end 42 | 43 | def description 44 | I18n.locale == I18n.default_locale ? super : locale_description.blank? ? super : locale_description 45 | end 46 | 47 | def head_number 48 | I18n.locale == I18n.default_locale ? super : locale_head_number.blank? ? super : locale_head_number 49 | end 50 | 51 | def mandatory? 52 | mandatory == true 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /app/models/survey/questions_type.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Survey::QuestionsType 4 | @@questions_types = { multiple_choice: 2, free_response: 9, multi_select: 1 } 5 | 6 | def self.questions_types 7 | @@questions_types 8 | end 9 | 10 | def self.questions_types_title 11 | titled = {} 12 | Survey::QuestionsType.questions_types.each { |k, v| titled[k.to_s.titleize] = v } 13 | titled 14 | end 15 | 16 | def self.questions_type_ids 17 | @@questions_types.values 18 | end 19 | 20 | def self.questions_type_keys 21 | @@questions_types.keys 22 | end 23 | 24 | @@questions_types.each do |key, val| 25 | define_singleton_method key.to_s do 26 | val 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /app/models/survey/section.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Survey::Section < ActiveRecord::Base 4 | self.table_name = 'survey_sections' 5 | 6 | # relations 7 | has_many :questions 8 | belongs_to :survey 9 | 10 | # rails 3 attr_accessible support 11 | if Rails::VERSION::MAJOR < 4 12 | attr_accessible :questions_attributes, :head_number, :name, :description, :survey_id, :locale_head_number, :locale_name, :locale_description 13 | end 14 | 15 | accepts_nested_attributes_for :questions, 16 | reject_if: ->(q) { q[:text].blank? }, allow_destroy: true 17 | 18 | # validations 19 | validates :name, presence: true, allow_blank: false 20 | validate :check_questions_requirements 21 | 22 | def name 23 | I18n.locale == I18n.default_locale ? super : locale_name.blank? ? super : locale_name 24 | end 25 | 26 | def description 27 | I18n.locale == I18n.default_locale ? super : locale_description.blank? ? super : locale_description 28 | end 29 | 30 | def head_number 31 | I18n.locale == I18n.default_locale ? super : locale_head_number.blank? ? super : locale_head_number 32 | end 33 | 34 | def full_name 35 | head_name = head_number.blank? ? '' : "#{head_number}: " 36 | "#{head_name}#{name}" 37 | end 38 | 39 | ####### 40 | 41 | private 42 | 43 | ####### 44 | 45 | # a section only can be saved if has one or more questions and options 46 | def check_questions_requirements 47 | if questions.empty? || questions.collect(&:options).empty? 48 | errors.add(:base, 'Section without questions or options cannot be saved') 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /app/models/survey/survey.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Survey::Survey < ActiveRecord::Base 4 | self.table_name = 'survey_surveys' 5 | 6 | # relations 7 | has_many :attempts 8 | has_many :sections 9 | 10 | # rails 3 attr_accessible support 11 | if Rails::VERSION::MAJOR < 4 12 | attr_accessible :name, :description, :finished, :active, :sections_attributes, :attempts_number, :locale_name, :locale_description 13 | end 14 | 15 | accepts_nested_attributes_for :sections, 16 | reject_if: ->(q) { q[:name].blank? }, allow_destroy: true 17 | 18 | scope :active, -> { where(active: true) } 19 | scope :inactive, -> { where(active: false) } 20 | 21 | validates :attempts_number, 22 | numericality: { only_integer: true, greater_than: -1 } 23 | 24 | # validations 25 | validates :description, :name, presence: true, allow_blank: false 26 | validate :check_active_requirements 27 | 28 | # returns all the correct options for current surveys 29 | def correct_options 30 | Survey::Question.where(section_id: section_ids).map(&:correct_options).flatten 31 | end 32 | 33 | # returns all the incorrect options for current surveys 34 | def incorrect_options 35 | Survey::Question.where(section_id: sections.collect(&:id)).map(&:incorrect_options).flatten 36 | end 37 | 38 | def avaliable_for_participant?(participant) 39 | current_number_of_attempts = 40 | attempts.for_participant(participant).size 41 | upper_bound = attempts_number 42 | !((current_number_of_attempts >= upper_bound && upper_bound != 0)) 43 | end 44 | 45 | def name 46 | I18n.locale == I18n.default_locale ? super : locale_name.blank? ? super : locale_name 47 | end 48 | 49 | def description 50 | I18n.locale == I18n.default_locale ? super : locale_description.blank? ? super : locale_description 51 | end 52 | 53 | ####### 54 | 55 | private 56 | 57 | ####### 58 | 59 | # a surveys only can be activated if has one or more sections and questions 60 | def check_active_requirements 61 | if sections.empty? || sections.collect(&:questions).empty? 62 | errors.add(:base, 'Survey without sections or questions cannot be saved') 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | surveys_controller: 3 | create: Your Survey has been successfull created 4 | update: Your Survey has been successfull updated 5 | attempts_controller: 6 | create: "Congratulations, you have responded to Survey." 7 | surveys: Surveys 8 | survey_details: Survey Details 9 | questions: Questions 10 | sections: Sections 11 | predefined_values: Predefined Values 12 | activerecord: 13 | models: 14 | survey: 15 | survey: Survey 16 | other: Surveys 17 | attributes: 18 | survey: 19 | name: Name 20 | locale_name: Localized name 21 | finished: Finished 22 | description: Short description 23 | locale_description: Localized short description 24 | active: "Activated?" 25 | attempts_number: Number of Maximum Attempts 26 | sections_attributes: Sections 27 | section: 28 | head_number: Head Number 29 | name: Name 30 | description: Description 31 | locale_head_number: Localized head number 32 | locale_name: Localized name 33 | locale_description: Localized Description 34 | question: 35 | head_number: Head Number 36 | text: Question Text 37 | description: Description 38 | locale_head_number: Localized head number 39 | locale_text: Localized Question Text 40 | locale_description: Localized Description 41 | options_attributes: Options 42 | predefined_values_attributes: Predefined Values 43 | options: 44 | text: Option Text 45 | locale_text: Localized Option Text 46 | correct: Correct 47 | attempt: 48 | answers_attributes: Answers 49 | survey: Survey 50 | answer: 51 | attempt: Attempt 52 | question: Question 53 | option: Option 54 | correct: Answer Correct? -------------------------------------------------------------------------------- /config/locales/pt-PT.yml: -------------------------------------------------------------------------------- 1 | pt-PT: 2 | questions: Perguntas 3 | survey_details: Detalhes do Questionário 4 | surveys_controller: 5 | create: O questionário foi criado com sucesso 6 | update: O questionário foi atualizado com sucesso 7 | attempts_controller: 8 | create: "A sua resposta foi efetuada com sucesso." 9 | surveys: Questionários 10 | survey_details: Detalhes do Questionário 11 | questions: Perguntas 12 | activerecord: 13 | models: 14 | survey: 15 | survey: Questionário 16 | other: Questionários 17 | question: 18 | question: Pergunta 19 | other: Perguntas 20 | option: 21 | option: Opção 22 | other: Opções 23 | attempt: 24 | option: Resposta 25 | other: Respostas 26 | attributes: 27 | survey: 28 | name: Nome 29 | finished: Acabado 30 | description: Pequena Descrição 31 | active: "Ativo?" 32 | attempts_number: Número máximo de tentativas 33 | questions_attributes: Perguntas 34 | question: 35 | text: Corpo da Pergunta 36 | options_attributes: Opções 37 | options: 38 | text: Corpo da Opção 39 | correct: Correta 40 | attempt: 41 | answers_attributes: Respostas 42 | survey: Questionário 43 | winner: Vencedor 44 | answer: 45 | attempt: Tentativa 46 | question: Pergunta 47 | option: Opção 48 | correct: Resposta Correta? -------------------------------------------------------------------------------- /config/locales/pt.yml: -------------------------------------------------------------------------------- 1 | pt: 2 | questions: Perguntas 3 | survey_details: Detalhes do Questionário 4 | surveys_controller: 5 | create: O questionário foi criado com sucesso 6 | update: O questionário foi atualizado com sucesso 7 | attempts_controller: 8 | create: "A sua resposta foi efetuada com sucesso." 9 | surveys: Questionários 10 | survey_details: Detalhes do Questionário 11 | questions: Perguntas 12 | activerecord: 13 | models: 14 | survey: 15 | survey: Questionário 16 | other: Questionários 17 | question: 18 | question: Pergunta 19 | other: Perguntas 20 | option: 21 | option: Opção 22 | other: Opções 23 | attempt: 24 | option: Resposta 25 | other: Respostas 26 | attributes: 27 | survey: 28 | name: Nome 29 | finished: Acabado 30 | description: Pequena Descrição 31 | active: "Ativo?" 32 | attempts_number: Número máximo de tentativas 33 | questions_attributes: Perguntas 34 | question: 35 | text: Corpo da Pergunta 36 | options_attributes: Opções 37 | options: 38 | text: Corpo da Opção 39 | correct: Correta 40 | attempt: 41 | answers_attributes: Respostas 42 | survey: Questionário 43 | winner: Vencedor 44 | answer: 45 | attempt: Tentativa 46 | question: Pergunta 47 | option: Opção 48 | correct: Resposta Correta? -------------------------------------------------------------------------------- /lib/generators/survey/install_generator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Survey 4 | module Generators 5 | class InstallGenerator < Rails::Generators::Base 6 | source_root File.expand_path('../../templates', __FILE__) 7 | 8 | def copy_migration 9 | timestamp_number = Time.now.utc.strftime('%Y%m%d%H%M%S').to_i 10 | 11 | migration_files = [{ new_file_name: 'create_survey', origin_file_name: 'migration' }, 12 | { new_file_name: 'create_sections', origin_file_name: 'migration_section' }, 13 | { new_file_name: 'update_survey_tables', origin_file_name: 'migration_update_survey_tables' }, 14 | { new_file_name: 'add_types_to_questions_and_options', origin_file_name: 'migration_add_types_to_questions_and_options' }, 15 | { new_file_name: 'add_head_number_to_options_table', origin_file_name: 'migration_add_head_number_to_options_table' }, 16 | { new_file_name: 'create_predefined_values_table', origin_file_name: 'migration_create_predefined_values_table' }, 17 | { new_file_name: 'add_mandatory_to_questions_table', origin_file_name: 'migration_add_mandatory_to_questions_table' }] 18 | 19 | migration_files.each do |migration_file| 20 | unless already_exists?(migration_file[:new_file_name]) 21 | copy_file "#{migration_file[:origin_file_name]}.rb", "db/migrate/#{timestamp_number}_#{migration_file[:new_file_name]}.rb" 22 | timestamp_number += 1 23 | end 24 | end 25 | end 26 | 27 | ####### 28 | 29 | private 30 | 31 | ####### 32 | 33 | def already_exists?(file_name) 34 | Dir.glob("#{File.join(destination_root, File.join('db', 'migrate'))}/[0-9]*_*.rb").grep(Regexp.new('\d+_' + file_name + '.rb$')).first 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/generators/survey/survey_generator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Survey 4 | class SurveyGenerator < Rails::Generators::Base 5 | source_root File.expand_path('../../templates', __FILE__) 6 | 7 | TEMPLATES = %w[active_admin rails_admin plain routes].freeze 8 | 9 | argument :arguments, 10 | type: :array, 11 | default: [], 12 | banner: "< #{TEMPLATES.join('|')} > [options]" 13 | 14 | def create_resolution 15 | strategy = arguments.first 16 | if TEMPLATES.include? strategy 17 | send("generate_#{strategy}_resolution") 18 | success_message(strategy) 19 | else 20 | error_message(strategy) 21 | end 22 | end 23 | 24 | private 25 | 26 | def generate_active_admin_resolution 27 | copy_file 'active_admin.rb', 'app/admin/survey.rb' 28 | end 29 | 30 | def generate_rails_admin_resolution 31 | copy_file 'rails_admin.rb', 'config/initializers/survey_rails_admin.rb' 32 | end 33 | 34 | def generate_plain_resolution 35 | scope = get_scope 36 | template 'survey_plain.rb', "app/controllers/#{scope}/surveys_controller.rb" 37 | template 'attempts_plain.rb', "app/controllers/#{scope}/attempts_controller.rb" 38 | template 'helper.rb', "app/helpers/#{scope}/surveys_helper.rb" 39 | directory 'survey_views', "app/views/#{scope}/surveys", recursive: true 40 | directory 'attempts_views', "app/views/#{scope}/attempts", recursive: true 41 | generate_routes_for(scope) 42 | end 43 | 44 | def generate_routes_resolution 45 | generate_routes_for(get_scope) 46 | end 47 | 48 | # Error Handlers 49 | def error_message(argument) 50 | error_message = <<-CONTENT 51 | This Resolution: '#{argument}' is not supported by Survey: 52 | We only support Active Admin, Refinery and Active Scaffold 53 | CONTENT 54 | say error_message, :red 55 | end 56 | 57 | def success_message(argument) 58 | say "Generation of #{argument.capitalize} Template Complete :) enjoy Survey", :green 59 | end 60 | 61 | def generate_routes_for(namespace, _conditional = nil) 62 | content = <<-CONTENT 63 | 64 | namespace :#{namespace} do 65 | resources :surveys 66 | resources :attempts, :only => [:new, :create] 67 | end 68 | CONTENT 69 | inject_into_file 'config/routes.rb', "\n#{content}", 70 | after: "#{Rails.application.class}.routes.draw do" 71 | end 72 | 73 | def get_scope 74 | arguments.size == 1 ? 'admin' : arguments[1].split(':').last 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /lib/generators/templates/active_admin.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | ActiveAdmin.register Survey::Survey do 4 | menu label: I18n.t('surveys') 5 | 6 | filter :name, 7 | as: :select, 8 | collection: proc { 9 | Survey::Survey.select('distinct(name)').collect do |c| 10 | [c.name, c.name] 11 | end 12 | } 13 | filter :active, 14 | as: :select, 15 | collection: %w[true false] 16 | 17 | filter :created_at 18 | 19 | index do 20 | column :name 21 | column :description 22 | column :active 23 | column :attempts_number 24 | column :finished 25 | column :created_at 26 | default_actions 27 | end 28 | 29 | form do |f| 30 | f.inputs I18n.t('survey_details') do 31 | f.input :name 32 | f.input :locale_name 33 | f.input :description 34 | f.input :locale_description 35 | f.input :active, as: :select, collection: %w[true false] 36 | f.input :attempts_number 37 | end 38 | 39 | f.inputs I18n.t('sections') do 40 | f.has_many :sections do |s| 41 | s.input :head_number 42 | s.input :locale_head_number 43 | s.input :name 44 | s.input :locale_name 45 | s.input :description 46 | s.input :locale_description 47 | 48 | s.has_many :questions do |q| 49 | q.input :head_number 50 | q.input :locale_head_number 51 | q.input :text 52 | q.input :locale_text 53 | q.input :description 54 | q.input :locale_description 55 | q.input :questions_type_id, as: :select, collection: Survey::QuestionsType.questions_types_title 56 | q.input :mandatory 57 | 58 | q.inputs I18n.t('predefined_values') do 59 | q.has_many :predefined_values do |p| 60 | p.input :head_number 61 | p.input :name 62 | p.input :locale_name 63 | end 64 | end 65 | 66 | q.has_many :options do |a| 67 | a.input :head_number 68 | a.input :text 69 | a.input :locale_text 70 | a.input :options_type_id, as: :select, collection: Survey::OptionsType.options_types_title 71 | a.input :correct 72 | end 73 | end 74 | end 75 | end 76 | 77 | f.buttons 78 | end 79 | 80 | if Rails::VERSION::MAJOR >= 4 81 | controller do 82 | def permitted_params 83 | params.permit! 84 | end 85 | end 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /lib/generators/templates/attempts_plain.rb: -------------------------------------------------------------------------------- 1 | class <%= get_scope.capitalize %>::AttemptsController < ApplicationController 2 | 3 | helper "<%= get_scope%>/surveys" 4 | 5 | def new 6 | @survey = Survey::Survey.active.last 7 | @attempt = @survey.attempts.new 8 | @attempt.answers.build 9 | @participant = current_user # you have to decide what to do here 10 | end 11 | 12 | def create 13 | @survey = Survey::Survey.active.last 14 | @attempt = @survey.attempts.new(attempt_params) 15 | @attempt.participant = current_user # you have to decide what to do here 16 | if @attempt.valid? and @attempt.save 17 | redirect_to view_context.new_attempt_path, alert: I18n.t("attempts_controller.#{action_name}") 18 | else 19 | flash.now[:error] = @attempt.errors.full_messages.join(', ') 20 | render :action => :new 21 | end 22 | end 23 | 24 | ####### 25 | private 26 | ####### 27 | 28 | # Rails 4 Strong Params 29 | def attempt_params 30 | if Rails::VERSION::MAJOR < 4 31 | params[:survey_attempt] 32 | else 33 | params.require(:survey_attempt).permit(answers_attributes: [:question_id, :option_id, :option_text, :option_number, :predefined_value_id]) 34 | end 35 | end 36 | end -------------------------------------------------------------------------------- /lib/generators/templates/attempts_views/_form.html.erb: -------------------------------------------------------------------------------- 1 |

<%= @survey.name %>

2 |

<%= @survey.description %>

3 | <%= form_for(@attempt, :url => attempt_scope(@attempt)) do |f| %> 4 | <%= f.fields_for :answers do |builder| %> 5 | 59 | <% end -%> 60 | <%= f.submit "Submit" %> 61 | <% end -%> -------------------------------------------------------------------------------- /lib/generators/templates/attempts_views/new.html.erb: -------------------------------------------------------------------------------- 1 |

<%= flash[:alert]%>

2 |

<%= flash[:error]%>

3 | 4 | <% if @survey.avaliable_for_participant?(@participant) %> 5 | <%= render 'form' %> 6 | <% else %> 7 |

8 | <%= @participant.name %> spent all the possible attempts to answer this Survey 9 |

10 | <% end -%> -------------------------------------------------------------------------------- /lib/generators/templates/helper.rb: -------------------------------------------------------------------------------- 1 | module <%= get_scope.capitalize %> 2 | module SurveysHelper 3 | def link_to_remove_field(name, f) 4 | f.hidden_field(:_destroy) + 5 | link_to_function(raw(name), "removeField(this)", :id =>"remove-attach") 6 | end 7 | 8 | def new_attempt_path 9 | new_<%= get_scope %>_attempt_path 10 | end 11 | 12 | def new_survey_path 13 | new_<%= get_scope %>_survey_path 14 | end 15 | 16 | def edit_survey_path(resource) 17 | edit_<%= get_scope %>_survey_path(resource) 18 | end 19 | 20 | def attempt_scope(resource) 21 | if action_name =~ /new|create/ 22 | <%= get_scope %>_attempts_path(resource) 23 | elsif action_name =~ /edit|update/ 24 | <%= get_scope %>_attempt_path(resource) 25 | end 26 | end 27 | 28 | def survey_scope(resource) 29 | if action_name =~ /new|create/ 30 | <%= get_scope %>_surveys_path(resource) 31 | elsif action_name =~ /edit|update/ 32 | <%= get_scope %>_survey_path(resource) 33 | end 34 | end 35 | 36 | def link_to_add_field(name, f, association) 37 | new_object = f.object.class.reflect_on_association(association).klass.new 38 | fields = f.fields_for(association, new_object,:child_index => "new_#{association}") do |builder| 39 | render(association.to_s.singularize + "_fields", :f => builder) 40 | end 41 | link_to_function(name, "addField(this, \"#{association}\", \"#{escape_javascript(fields)}\")", 42 | :id=>"add-attach", 43 | :class=>"btn btn-small btn-info") 44 | end 45 | 46 | def link_to_function(name, *args, &block) 47 | html_options = args.extract_options!.symbolize_keys 48 | 49 | function = block_given? ? update_page(&block) : args[0] || '' 50 | onclick = "#{"#{html_options[:onclick]}; " if html_options[:onclick]}#{function}; return false;" 51 | href = html_options[:href] || '#' 52 | 53 | content_tag(:a, name, html_options.merge(:href => href, :onclick => onclick)) 54 | end 55 | end 56 | end -------------------------------------------------------------------------------- /lib/generators/templates/migration.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class CreateSurvey < ActiveRecord::Migration 4 | def self.up 5 | # survey surveys logic 6 | create_table :survey_surveys do |t| 7 | t.string :name 8 | t.text :description 9 | t.integer :attempts_number, default: 0 10 | t.boolean :finished, default: false 11 | t.boolean :active, default: true 12 | 13 | t.timestamps 14 | end 15 | 16 | create_table :survey_questions do |t| 17 | t.integer :survey_id 18 | t.string :text 19 | 20 | t.timestamps 21 | end 22 | 23 | create_table :survey_options do |t| 24 | t.integer :question_id 25 | t.integer :weight, default: 0 26 | t.string :text 27 | t.boolean :correct 28 | 29 | t.timestamps 30 | end 31 | 32 | # survey answer logic 33 | create_table :survey_attempts do |t| 34 | t.belongs_to :participant, polymorphic: true 35 | t.integer :survey_id 36 | t.boolean :winner, null: false, default: false 37 | t.decimal :score 38 | 39 | t.timestamps 40 | end 41 | 42 | create_table :survey_answers do |t| 43 | t.integer :attempt_id 44 | t.integer :question_id 45 | t.integer :option_id 46 | t.boolean :correct, null: false, default: false 47 | t.timestamps 48 | end 49 | end 50 | 51 | def self.down 52 | drop_table :survey_surveys 53 | drop_table :survey_questions 54 | drop_table :survey_options 55 | 56 | drop_table :survey_attempts 57 | drop_table :survey_answers 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/generators/templates/migration_add_head_number_to_options_table.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddHeadNumberToOptionsTable < ActiveRecord::Migration 4 | def change 5 | # Survey Options table 6 | add_column :survey_options, :head_number, :string 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/generators/templates/migration_add_mandatory_to_questions_table.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddMandatoryToQuestionsTable < ActiveRecord::Migration 4 | def change 5 | # Survey Questions table 6 | add_column :survey_questions, :mandatory, :boolean, default: false 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/generators/templates/migration_add_types_to_questions_and_options.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddTypesToQuestionsAndOptions < ActiveRecord::Migration 4 | def change 5 | # Survey Questions table 6 | add_column :survey_questions, :questions_type_id, :integer 7 | 8 | # Survey Options table 9 | add_column :survey_options, :options_type_id, :integer 10 | 11 | # Survey Answers table 12 | add_column :survey_answers, :option_text, :text 13 | add_column :survey_answers, :option_number, :integer 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/generators/templates/migration_create_predefined_values_table.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class CreatePredefinedValuesTable < ActiveRecord::Migration 4 | def change 5 | create_table :survey_predefined_values do |t| 6 | t.string :head_number 7 | t.string :name 8 | t.string :locale_name 9 | t.integer :question_id 10 | 11 | t.timestamps 12 | end 13 | 14 | add_column :survey_answers, :predefined_value_id, :integer 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/generators/templates/migration_section.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class CreateSections < ActiveRecord::Migration 4 | def self.up 5 | create_table :survey_sections do |t| 6 | t.string :head_number 7 | t.string :name 8 | t.text :description 9 | t.integer :survey_id 10 | 11 | t.timestamps 12 | end 13 | 14 | remove_column :survey_questions, :survey_id 15 | add_column :survey_questions, :section_id, :integer 16 | end 17 | 18 | def self.down 19 | drop_table :survey_sections 20 | 21 | remove_column :survey_questions, :section_id 22 | add_column :survey_questions, :survey_id, :integer 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/generators/templates/migration_update_survey_tables.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class UpdateSurveyTables < ActiveRecord::Migration 4 | def change 5 | # Survey Surveys table 6 | add_column :survey_surveys, :locale_name, :string 7 | add_column :survey_surveys, :locale_description, :text 8 | 9 | # Survey Sections table 10 | add_column :survey_sections, :locale_head_number, :string 11 | add_column :survey_sections, :locale_name, :string 12 | add_column :survey_sections, :locale_description, :text 13 | 14 | # Survey Questions table 15 | add_column :survey_questions, :head_number, :string 16 | add_column :survey_questions, :description, :text 17 | add_column :survey_questions, :locale_text, :string 18 | add_column :survey_questions, :locale_head_number, :string 19 | add_column :survey_questions, :locale_description, :text 20 | 21 | # Survey Options table 22 | add_column :survey_options, :locale_text, :string 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/generators/templates/rails_admin.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RailsAdmin.config do |c| 4 | c.excluded_models = [ 5 | Survey::Answer, 6 | Survey::Option, 7 | Survey::Attempt, 8 | Survey::Question 9 | ] 10 | end 11 | -------------------------------------------------------------------------------- /lib/generators/templates/survey_plain.rb: -------------------------------------------------------------------------------- 1 | class <%= get_scope.capitalize %>::SurveysController < ApplicationController 2 | before_filter :load_survey, :only => [:show, :edit, :update] 3 | 4 | def index 5 | @surveys = Survey::Survey.all 6 | end 7 | 8 | def new 9 | @survey = Survey::Survey.new 10 | end 11 | 12 | def create 13 | @survey = Survey::Survey.new(survey_params) 14 | if @survey.valid? and @survey.save 15 | default_redirect 16 | else 17 | render :action => :new 18 | end 19 | end 20 | 21 | def edit 22 | end 23 | 24 | def show 25 | end 26 | 27 | def update 28 | if @survey.update_attributes(survey_params) 29 | default_redirect 30 | else 31 | render :action => :edit 32 | end 33 | end 34 | 35 | private 36 | 37 | def default_redirect 38 | redirect_to <%= get_scope %>_surveys_path, alert: I18n.t("surveys_controller.#{action_name}") 39 | end 40 | 41 | def load_survey 42 | @survey = Survey::Survey.find(params[:id]) 43 | end 44 | 45 | ####### 46 | private 47 | ####### 48 | 49 | # Rails 4 Strong Params 50 | def survey_params 51 | if Rails::VERSION::MAJOR < 4 52 | params[:survey_survey] 53 | else 54 | protected_attrs = ["created_at", "updated_at"] 55 | params.require(:survey_survey).permit(Survey::Survey.new.attributes.keys - protected_attrs, :sections_attributes => [Survey::Section.new.attributes.keys - protected_attrs, :questions_attributes => [Survey::Question.new.attributes.keys - protected_attrs, :options_attributes => [Survey::Option.new.attributes.keys - protected_attrs]]]) 56 | end 57 | end 58 | end -------------------------------------------------------------------------------- /lib/generators/templates/survey_views/_form.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_for(@survey, :url => survey_scope(@survey)) do |f| %> 2 | 3 | <% if f.object.errors.messages.any? %> 4 | 11 | <% end -%> 12 | 13 |
14 | <%= f.label :name %>
15 | <%= f.text_field :name %>
16 |
17 |
18 | <%= f.label :locale_name %>
19 | <%= f.text_field :locale_name %>
20 |
21 |
22 | <%= f.label :description %>
23 | <%= f.text_area :description, :size => "100x5" %>
24 |
25 |
26 | <%= f.label :locale_description %>
27 | <%= f.text_area :locale_description %>
28 |
29 |
30 | <%= f.label :attempts_number %>
31 | <%= f.text_field :attempts_number %>
32 |
33 |
34 | <%= f.label :active %>
35 | <%= f.select :active, ["true", "false"] %> 36 |
37 | 38 |
39 | 40 |
41 | 46 |
47 | <%= link_to_add_field "Add a new Section", f, :sections %> 48 |
49 | 50 |
51 | 52 |
53 | <%= f.submit %> 54 |
55 | <% end -%> 56 | 57 | -------------------------------------------------------------------------------- /lib/generators/templates/survey_views/_option_fields.html.erb: -------------------------------------------------------------------------------- 1 |
  • 2 |

    Option

    3 |
    4 | <%= f.label :head_number %>
    5 | <%= f.text_field :head_number %> 6 |
    7 |
    8 | <%= f.label :text %>
    9 | <%= f.text_field :text %> 10 |
    11 |
    12 | <%= f.label :locale_text %>
    13 | <%= f.text_field :locale_text %> 14 |
    15 |
    16 | <%= f.label :options_type_id %>
    17 | <%= f.select :options_type_id, Survey::OptionsType.options_types_title %> 18 |
    19 |
    20 | <%= f.check_box :correct %> 21 | <%= f.label :correct %> 22 |
    23 |
    24 | <%= link_to_remove_field "Remove", f %> 25 |
    26 |
  • -------------------------------------------------------------------------------- /lib/generators/templates/survey_views/_question_fields.html.erb: -------------------------------------------------------------------------------- 1 |
  • 2 |

    Question

    3 |
    4 | <%= f.label :head_number %>
    5 | <%= f.text_field :head_number %> 6 |
    7 |
    8 | <%= f.label :locale_head_number %>
    9 | <%= f.text_field :locale_head_number %> 10 |
    11 |
    12 | <%= f.label :text %>
    13 | <%= f.text_field :text %> 14 |
    15 |
    16 | <%= f.label :locale_text %>
    17 | <%= f.text_field :locale_text %> 18 |
    19 |
    20 | <%= f.label :description %>
    21 | <%= f.text_area :description %> 22 |
    23 |
    24 | <%= f.label :locale_description %>
    25 | <%= f.text_area :locale_description %> 26 |
    27 |
    28 | <%= f.label :questions_type_id %>
    29 | <%= f.select :questions_type_id, Survey::QuestionsType.questions_types_title %> 30 |
    31 | 32 |
    33 | 38 | <%= link_to_add_field "Add a new Option", f, :options %> 39 |
    40 | <%= link_to_remove_field "Remove Question", f %> 41 |
    42 |
  • 43 | -------------------------------------------------------------------------------- /lib/generators/templates/survey_views/_section_fields.html.erb: -------------------------------------------------------------------------------- 1 |
  • 2 |

    Section

    3 | <%= f.hidden_field :id %> 4 |
    5 | <%= f.label :head_number %>
    6 | <%= f.text_field :locale_head_number %> 7 |
    8 |
    9 | <%= f.label :name %>
    10 | <%= f.text_field :name %> 11 |
    12 |
    13 | <%= f.label :locale_name %>
    14 | <%= f.text_field :locale_name %> 15 |
    16 |
    17 | <%= f.label :description %>
    18 | <%= f.text_field :locale_description %> 19 |
    20 |
    21 | 26 |
    27 | <%= link_to_add_field "Add a new Question", f, :questions %> 28 |
    29 |
  • -------------------------------------------------------------------------------- /lib/generators/templates/survey_views/edit.html.erb: -------------------------------------------------------------------------------- 1 |

    Edit Survey

    2 | <%= render 'form' %> -------------------------------------------------------------------------------- /lib/generators/templates/survey_views/index.html.erb: -------------------------------------------------------------------------------- 1 |
    2 |

    3 | Surveys Index 4 |

    5 |
    6 | <%= link_to "New Survey", new_survey_path %> 7 | 14 |
    15 | -------------------------------------------------------------------------------- /lib/generators/templates/survey_views/new.html.erb: -------------------------------------------------------------------------------- 1 |

    New Survey

    2 | <%= render 'form' %> -------------------------------------------------------------------------------- /lib/survey.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'survey/engine' 4 | require 'survey/version' 5 | require 'survey/active_record' 6 | 7 | ActiveRecord::Base.send(:include, Survey::ActiveRecord) 8 | -------------------------------------------------------------------------------- /lib/survey/active_record.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Survey 4 | module ActiveRecord 5 | extend ActiveSupport::Concern 6 | 7 | module ClassMethods 8 | def has_surveys 9 | has_many :survey_attempts, as: :participant, class_name: 'Survey::Attempt' 10 | 11 | define_method('for_survey') do |survey| 12 | survey_attempts.where(survey_id: survey.id) 13 | end 14 | end 15 | 16 | def has_many_surveys 17 | has_many :surveys, class_name: 'Survey::Survey' 18 | end 19 | 20 | def has_one_survey 21 | has_one :survey, class_name: 'Survey::Survey' 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/survey/engine.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails' 4 | 5 | module Survey 6 | class Engine < Rails::Engine 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/survey/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Survey 4 | VERSION = '0.1' 5 | end 6 | -------------------------------------------------------------------------------- /survey.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # frozen_string_literal: true 3 | 4 | $LOAD_PATH.push File.expand_path('../lib', __FILE__) 5 | require 'survey/version' 6 | 7 | Gem::Specification.new do |s| 8 | s.name = 'questionnaire_engine' 9 | s.version = Survey::VERSION 10 | s.summary = %(Questionnaire is a user oriented tool that brings surveys into Rails applications.) 11 | s.description = %(A rails gem to enable surveys in your application as easy as possible) 12 | s.files = Dir['{app,lib,config}/**/*'] + ['MIT-LICENSE', 'Rakefile', 'Gemfile', 'README.md'] 13 | s.authors = 'Clearfunction' 14 | s.email = 'keith@clearfunction.com' 15 | s.homepage = 'https://github.com/clearfunction/questionnaire' 16 | s.licenses = 'MIT' 17 | s.require_paths = %w[lib] 18 | 19 | s.add_dependency('rails', '~> 5.1') 20 | s.add_development_dependency('mocha') 21 | s.add_development_dependency('faker') 22 | s.add_development_dependency('rake') 23 | s.add_development_dependency('rubocop') 24 | s.add_development_dependency('pry-rails') 25 | s.add_development_dependency('pry-byebug') 26 | end 27 | -------------------------------------------------------------------------------- /test/dummy/Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | ruby '2.1.5' 5 | 6 | gem 'execjs' 7 | gem 'rails', '3.1' 8 | gem 'therubyracer' 9 | 10 | group :development, :test do 11 | gem 'faker' 12 | gem 'pry-rails' 13 | end 14 | 15 | gem 'sqlite3' 16 | 17 | # Gems used only for assets and not required 18 | # in production environments by default. 19 | group :assets do 20 | gem 'coffee-rails' 21 | gem 'meta_search' 22 | gem 'sass-rails' 23 | gem 'uglifier' 24 | end 25 | 26 | gem 'jquery-rails' 27 | -------------------------------------------------------------------------------- /test/dummy/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | actionmailer (3.1.0) 5 | actionpack (= 3.1.0) 6 | mail (~> 2.3.0) 7 | actionpack (3.1.0) 8 | activemodel (= 3.1.0) 9 | activesupport (= 3.1.0) 10 | builder (~> 3.0.0) 11 | erubis (~> 2.7.0) 12 | i18n (~> 0.6) 13 | rack (~> 1.3.2) 14 | rack-cache (~> 1.0.3) 15 | rack-mount (~> 0.8.2) 16 | rack-test (~> 0.6.1) 17 | sprockets (~> 2.0.0) 18 | activemodel (3.1.0) 19 | activesupport (= 3.1.0) 20 | bcrypt-ruby (~> 3.0.0) 21 | builder (~> 3.0.0) 22 | i18n (~> 0.6) 23 | activerecord (3.1.0) 24 | activemodel (= 3.1.0) 25 | activesupport (= 3.1.0) 26 | arel (~> 2.2.1) 27 | tzinfo (~> 0.3.29) 28 | activeresource (3.1.0) 29 | activemodel (= 3.1.0) 30 | activesupport (= 3.1.0) 31 | activesupport (3.1.0) 32 | multi_json (~> 1.0) 33 | arel (2.2.3) 34 | bcrypt-ruby (3.0.1) 35 | builder (3.0.4) 36 | coderay (1.1.1) 37 | coffee-rails (3.1.1) 38 | coffee-script (>= 2.2.0) 39 | railties (~> 3.1.0) 40 | coffee-script (2.4.1) 41 | coffee-script-source 42 | execjs 43 | coffee-script-source (1.12.2) 44 | erubis (2.7.0) 45 | execjs (2.7.0) 46 | faker (1.7.3) 47 | i18n (~> 0.5) 48 | hike (1.2.3) 49 | i18n (0.8.4) 50 | jquery-rails (3.1.4) 51 | railties (>= 3.0, < 5.0) 52 | thor (>= 0.14, < 2.0) 53 | json (1.8.6) 54 | libv8 (3.16.14.19) 55 | mail (2.3.3) 56 | i18n (>= 0.4.0) 57 | mime-types (~> 1.16) 58 | treetop (~> 1.4.8) 59 | meta_search (1.1.3) 60 | actionpack (~> 3.1) 61 | activerecord (~> 3.1) 62 | activesupport (~> 3.1) 63 | polyamorous (~> 0.5.0) 64 | method_source (0.8.2) 65 | mime-types (1.25.1) 66 | multi_json (1.12.1) 67 | polyamorous (0.5.0) 68 | activerecord (~> 3.0) 69 | polyglot (0.3.5) 70 | pry (0.10.4) 71 | coderay (~> 1.1.0) 72 | method_source (~> 0.8.1) 73 | slop (~> 3.4) 74 | pry-rails (0.3.6) 75 | pry (>= 0.10.4) 76 | rack (1.3.10) 77 | rack-cache (1.0.3) 78 | rack (>= 0.4) 79 | rack-mount (0.8.3) 80 | rack (>= 1.0.0) 81 | rack-ssl (1.3.4) 82 | rack 83 | rack-test (0.6.3) 84 | rack (>= 1.0) 85 | rails (3.1.0) 86 | actionmailer (= 3.1.0) 87 | actionpack (= 3.1.0) 88 | activerecord (= 3.1.0) 89 | activeresource (= 3.1.0) 90 | activesupport (= 3.1.0) 91 | bundler (~> 1.0) 92 | railties (= 3.1.0) 93 | railties (3.1.0) 94 | actionpack (= 3.1.0) 95 | activesupport (= 3.1.0) 96 | rack-ssl (~> 1.3.2) 97 | rake (>= 0.8.7) 98 | rdoc (~> 3.4) 99 | thor (~> 0.14.6) 100 | rake (12.3.3) 101 | rdoc (3.12.2) 102 | json (~> 1.4) 103 | ref (2.0.0) 104 | sass (3.4.24) 105 | sass-rails (3.1.7) 106 | actionpack (~> 3.1.0) 107 | railties (~> 3.1.0) 108 | sass (>= 3.1.10) 109 | tilt (~> 1.3.2) 110 | slop (3.6.0) 111 | sprockets (2.0.5) 112 | hike (~> 1.2) 113 | rack (~> 1.0) 114 | tilt (~> 1.1, != 1.3.0) 115 | sqlite3 (1.3.13) 116 | therubyracer (0.12.3) 117 | libv8 (~> 3.16.14.15) 118 | ref 119 | thor (0.14.6) 120 | tilt (1.3.7) 121 | treetop (1.4.15) 122 | polyglot 123 | polyglot (>= 0.3.1) 124 | tzinfo (0.3.61) 125 | uglifier (3.2.0) 126 | execjs (>= 0.3.0, < 3) 127 | 128 | PLATFORMS 129 | ruby 130 | 131 | DEPENDENCIES 132 | coffee-rails 133 | execjs 134 | faker 135 | jquery-rails 136 | meta_search 137 | pry-rails 138 | rails (= 3.1) 139 | sass-rails 140 | sqlite3 141 | therubyracer 142 | uglifier 143 | -------------------------------------------------------------------------------- /test/dummy/Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Add your own tasks in files placed in lib/tasks ending in .rake, 4 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 5 | 6 | require File.expand_path('../config/application', __FILE__) 7 | require 'rake' 8 | 9 | Dummy::Application.load_tasks 10 | -------------------------------------------------------------------------------- /test/dummy/app/assets/javascripts/application.js: -------------------------------------------------------------------------------- 1 | // Place all the behaviors and hooks related to the matching controller here. 2 | // All this logic will automatically be available in application.js. 3 | 4 | //= require jquery 5 | //= require jquery_ujs 6 | //= require_tree . 7 | -------------------------------------------------------------------------------- /test/dummy/app/assets/javascripts/users.js: -------------------------------------------------------------------------------- 1 | // Place all the behaviors and hooks related to the matching controller here. 2 | // All this logic will automatically be available in application.js. 3 | -------------------------------------------------------------------------------- /test/dummy/app/assets/stylesheets/users.css: -------------------------------------------------------------------------------- 1 | /* 2 | Place all the styles related to the matching controller here. 3 | They will automatically be included in application.css. 4 | */ 5 | -------------------------------------------------------------------------------- /test/dummy/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ApplicationController < ActionController::Base 4 | protect_from_forgery 5 | end 6 | -------------------------------------------------------------------------------- /test/dummy/app/controllers/users_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class UsersController < ApplicationController 4 | end 5 | -------------------------------------------------------------------------------- /test/dummy/app/controllers/welcome_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class WelcomeController < ApplicationController 4 | def index 5 | render text: 'HELLO WORLD' 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /test/dummy/app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ApplicationHelper 4 | end 5 | -------------------------------------------------------------------------------- /test/dummy/app/helpers/users_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module UsersHelper 4 | end 5 | -------------------------------------------------------------------------------- /test/dummy/app/models/lesson.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Lesson < ActiveRecord::Base 4 | has_many_surveys 5 | end 6 | -------------------------------------------------------------------------------- /test/dummy/app/models/survey/belongs_to_lesson.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Survey 4 | module BelongsToLesson 5 | extend ActiveSupport::Concern 6 | included do 7 | belongs_to :lesson 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /test/dummy/app/models/user.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class User < ActiveRecord::Base 4 | has_surveys 5 | end 6 | -------------------------------------------------------------------------------- /test/dummy/app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Dummy 5 | <%#= stylesheet_link_tag :all %> 6 | <%= javascript_include_tag "application" %> 7 | <%= csrf_meta_tag %> 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /test/dummy/config.ru: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # This file is used by Rack-based servers to start the application. 4 | 5 | require ::File.expand_path('../config/environment', __FILE__) 6 | run Dummy::Application 7 | -------------------------------------------------------------------------------- /test/dummy/config/application.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require File.expand_path('../boot', __FILE__) 4 | 5 | require 'active_model/railtie' 6 | require 'active_record/railtie' 7 | require 'action_controller/railtie' 8 | require 'action_view/railtie' 9 | require 'action_mailer/railtie' 10 | require 'sprockets/railtie' 11 | 12 | Bundler.require 13 | require 'survey' 14 | module Dummy 15 | class Application < Rails::Application 16 | # Settings in config/environments/* take precedence over those specified here. 17 | # Application configuration should go into files in config/initializers 18 | # -- all .rb files in that directory are automatically loaded. 19 | 20 | # Custom directories with classes and modules you want to be autoloadable. 21 | # config.autoload_paths += %W(#{config.root}/extras) 22 | 23 | # Only load the plugins named here, in the order given (default is alphabetical). 24 | # :all can be used as a placeholder for all plugins not explicitly named. 25 | # config.plugins = [ :exception_notification, :ssl_requirement, :all ] 26 | 27 | # Activate observers that should always be running. 28 | # config.active_record.observers = :cacher, :garbage_collector, :forum_observer 29 | 30 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. 31 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. 32 | # config.time_zone = 'Central Time (US & Canada)' 33 | 34 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. 35 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] 36 | # config.i18n.default_locale = :de 37 | 38 | # JavaScript files you want as :defaults (application.js is always included). 39 | # config.action_view.javascript_expansions[:defaults] = %w(jquery rails) 40 | 41 | # Enable the asset pipeline 42 | config.assets.enabled = true 43 | # Version of your assets, change this if you want to expire all your assets 44 | config.assets.version = '1.0' 45 | config.assets.initialize_on_precompile = true 46 | 47 | # Configure the default encoding used in templates for Ruby 1.9. 48 | config.encoding = 'utf-8' 49 | 50 | # Configure sensitive parameters which will be filtered from the log file. 51 | config.filter_parameters += [:password] 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /test/dummy/config/boot.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rubygems' 4 | gemfile = File.expand_path('../../../../Gemfile', __FILE__) 5 | 6 | if File.exist?(gemfile) 7 | ENV['BUNDLE_GEMFILE'] = gemfile 8 | require 'bundler' 9 | Bundler.setup 10 | end 11 | 12 | $LOAD_PATH.unshift File.expand_path('../../../../lib', __FILE__) 13 | -------------------------------------------------------------------------------- /test/dummy/config/database.yml: -------------------------------------------------------------------------------- 1 | # SQLite version 3.x 2 | # gem install sqlite3 3 | development: 4 | adapter: sqlite3 5 | database: db/development.sqlite3 6 | pool: 5 7 | timeout: 5000 8 | 9 | # Warning: The database defined as "test" will be erased and 10 | # re-generated from your development database when you run "rake". 11 | # Do not set this db to the same as development or production. 12 | test: 13 | adapter: sqlite3 14 | database: db/test.sqlite3 15 | pool: 5 16 | timeout: 5000 17 | 18 | production: 19 | adapter: sqlite3 20 | database: db/production.sqlite3 21 | pool: 5 22 | timeout: 5000 23 | -------------------------------------------------------------------------------- /test/dummy/config/environment.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Load the rails application 4 | require File.expand_path('../application', __FILE__) 5 | 6 | # Initialize the rails application 7 | Dummy::Application.initialize! 8 | -------------------------------------------------------------------------------- /test/dummy/config/environments/development.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Dummy::Application.configure do 4 | # Settings specified here will take precedence over those in config/application.rb 5 | 6 | # In the development environment your application's code is reloaded on 7 | # every request. This slows down response time but is perfect for development 8 | # since you don't have to restart the webserver when you make code changes. 9 | config.cache_classes = false 10 | 11 | # Log error messages when you accidentally call methods on nil. 12 | config.whiny_nils = true 13 | 14 | # Show full error reports and disable caching 15 | config.consider_all_requests_local = true 16 | config.action_controller.perform_caching = false 17 | 18 | # Don't care if the mailer can't send 19 | config.action_mailer.raise_delivery_errors = false 20 | 21 | # Print deprecation notices to the Rails logger 22 | config.active_support.deprecation = :log 23 | 24 | # Only use best-standards-support built into browsers 25 | config.action_dispatch.best_standards_support = :builtin 26 | end 27 | -------------------------------------------------------------------------------- /test/dummy/config/environments/production.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Dummy::Application.configure do 4 | # Settings specified here will take precedence over those in config/application.rb 5 | 6 | # The production environment is meant for finished, "live" apps. 7 | # Code is not reloaded between requests 8 | config.cache_classes = true 9 | 10 | # Full error reports are disabled and caching is turned on 11 | config.consider_all_requests_local = false 12 | config.action_controller.perform_caching = true 13 | 14 | # Specifies the header that your server uses for sending files 15 | config.action_dispatch.x_sendfile_header = 'X-Sendfile' 16 | 17 | # For nginx: 18 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' 19 | 20 | # If you have no front-end server that supports something like X-Sendfile, 21 | # just comment this out and Rails will serve the files 22 | 23 | # See everything in the log (default is :info) 24 | # config.log_level = :debug 25 | 26 | # Use a different logger for distributed setups 27 | # config.logger = SyslogLogger.new 28 | 29 | # Use a different cache store in production 30 | # config.cache_store = :mem_cache_store 31 | 32 | # Disable Rails's static asset server 33 | # In production, Apache or nginx will already do this 34 | config.serve_static_assets = false 35 | 36 | # Enable serving of images, stylesheets, and javascripts from an asset server 37 | # config.action_controller.asset_host = "http://assets.example.com" 38 | 39 | # Disable delivery errors, bad email addresses will be ignored 40 | # config.action_mailer.raise_delivery_errors = false 41 | 42 | # Enable threaded mode 43 | # config.threadsafe! 44 | 45 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 46 | # the I18n.default_locale when a translation can not be found) 47 | config.i18n.fallbacks = true 48 | 49 | # Send deprecation notices to registered listeners 50 | config.active_support.deprecation = :notify 51 | end 52 | -------------------------------------------------------------------------------- /test/dummy/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Dummy::Application.configure do 4 | # Settings specified here will take precedence over those in config/application.rb 5 | 6 | # The test environment is used exclusively to run your application's 7 | # test suite. You never need to work with it otherwise. Remember that 8 | # your test database is "scratch space" for the test suite and is wiped 9 | # and recreated between test runs. Don't rely on the data there! 10 | config.cache_classes = true 11 | 12 | # Log error messages when you accidentally call methods on nil. 13 | config.whiny_nils = true 14 | 15 | # Show full error reports and disable caching 16 | config.consider_all_requests_local = true 17 | config.action_controller.perform_caching = false 18 | 19 | # Raise exceptions instead of rendering exception templates 20 | config.action_dispatch.show_exceptions = false 21 | 22 | # Disable request forgery protection in test environment 23 | config.action_controller.allow_forgery_protection = false 24 | 25 | # Tell Action Mailer not to deliver emails to the real world. 26 | # The :test delivery method accumulates sent emails in the 27 | # ActionMailer::Base.deliveries array. 28 | config.action_mailer.delivery_method = :test 29 | 30 | # Use SQL instead of Active Record's schema dumper when creating the test database. 31 | # This is necessary if your schema can't be completely dumped by the schema dumper, 32 | # like if you have constraints or database-specific column types 33 | # config.active_record.schema_format = :sql 34 | 35 | # Print deprecation notices to the stderr 36 | config.active_support.deprecation = :stderr 37 | end 38 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Be sure to restart your server when you modify this file. 3 | 4 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 5 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 6 | 7 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 8 | # Rails.backtrace_cleaner.remove_silencers! 9 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Be sure to restart your server when you modify this file. 3 | 4 | # Add new inflection rules using the following format 5 | # (all these examples are active by default): 6 | # ActiveSupport::Inflector.inflections do |inflect| 7 | # inflect.plural /^(ox)$/i, '\1en' 8 | # inflect.singular /^(ox)en/i, '\1' 9 | # inflect.irregular 'person', 'people' 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Be sure to restart your server when you modify this file. 3 | 4 | # Add new mime types for use in respond_to blocks: 5 | # Mime::Type.register "text/richtext", :rtf 6 | # Mime::Type.register_alias "text/html", :iphone 7 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/secret_token.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Be sure to restart your server when you modify this file. 4 | 5 | # Your secret key for verifying the integrity of signed cookies. 6 | # If you change this key, all old signed cookies will become invalid! 7 | # Make sure the secret is at least 30 characters and all random, 8 | # no regular words or you'll be exposed to dictionary attacks. 9 | Dummy::Application.config.secret_token = 'a96634b5f2750043d337dc7cf99f46dae98fe918cadb32e93b85312f247149b633b5bb3499da3826ee6b67588e64c57f0c86feaca2dc0a51db143a72da530c65' 10 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Be sure to restart your server when you modify this file. 4 | 5 | Dummy::Application.config.session_store :cookie_store, key: '_dummy_session' 6 | 7 | # Use the database for sessions instead of the cookie-based default, 8 | # which shouldn't be used to store highly confidential information 9 | # (create the session table with "rails generate session_migration") 10 | # Dummy::Application.config.session_store :active_record_store 11 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/survey.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Survey::Survey.include Survey::BelongsToLesson 4 | -------------------------------------------------------------------------------- /test/dummy/config/locales/devise.en.yml: -------------------------------------------------------------------------------- 1 | # Additional translations at https://github.com/plataformatec/devise/wiki/I18n 2 | 3 | en: 4 | errors: 5 | messages: 6 | expired: "has expired, please request a new one" 7 | not_found: "not found" 8 | already_confirmed: "was already confirmed, please try signing in" 9 | not_locked: "was not locked" 10 | not_saved: 11 | one: "1 error prohibited this %{resource} from being saved:" 12 | other: "%{count} errors prohibited this %{resource} from being saved:" 13 | confirmation_period_expired: "needs to be confirmed within %{period}, please request a new one" 14 | 15 | devise: 16 | failure: 17 | already_authenticated: 'You are already signed in.' 18 | unauthenticated: 'You need to sign in or sign up before continuing.' 19 | unconfirmed: 'You have to confirm your account before continuing.' 20 | locked: 'Your account is locked.' 21 | not_found_in_database: 'Invalid email or password.' 22 | invalid: 'Invalid email or password.' 23 | invalid_token: 'Invalid authentication token.' 24 | timeout: 'Your session expired, please sign in again to continue.' 25 | inactive: 'Your account was not activated yet.' 26 | sessions: 27 | signed_in: 'Signed in successfully.' 28 | signed_out: 'Signed out successfully.' 29 | passwords: 30 | send_instructions: 'You will receive an email with instructions about how to reset your password in a few minutes.' 31 | updated: 'Your password was changed successfully. You are now signed in.' 32 | updated_not_active: 'Your password was changed successfully.' 33 | send_paranoid_instructions: "If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes." 34 | no_token: "You can't access this page without coming from a password reset email. If you do come from a password reset email, please make sure you used the full URL provided." 35 | confirmations: 36 | send_instructions: 'You will receive an email with instructions about how to confirm your account in a few minutes.' 37 | send_paranoid_instructions: 'If your email address exists in our database, you will receive an email with instructions about how to confirm your account in a few minutes.' 38 | confirmed: 'Your account was successfully confirmed. You are now signed in.' 39 | registrations: 40 | signed_up: 'Welcome! You have signed up successfully.' 41 | signed_up_but_unconfirmed: 'A message with a confirmation link has been sent to your email address. Please open the link to activate your account.' 42 | signed_up_but_inactive: 'You have signed up successfully. However, we could not sign you in because your account is not yet activated.' 43 | signed_up_but_locked: 'You have signed up successfully. However, we could not sign you in because your account is locked.' 44 | updated: 'You updated your account successfully.' 45 | update_needs_confirmation: "You updated your account successfully, but we need to verify your new email address. Please check your email and click on the confirm link to finalize confirming your new email address." 46 | destroyed: 'Bye! Your account was successfully cancelled. We hope to see you again soon.' 47 | unlocks: 48 | send_instructions: 'You will receive an email with instructions about how to unlock your account in a few minutes.' 49 | unlocked: 'Your account has been unlocked successfully. Please sign in to continue.' 50 | send_paranoid_instructions: 'If your account exists, you will receive an email with instructions about how to unlock it in a few minutes.' 51 | omniauth_callbacks: 52 | success: 'Successfully authenticated from %{kind} account.' 53 | failure: 'Could not authenticate you from %{kind} because "%{reason}".' 54 | mailer: 55 | confirmation_instructions: 56 | subject: 'Confirmation instructions' 57 | reset_password_instructions: 58 | subject: 'Reset password instructions' 59 | unlock_instructions: 60 | subject: 'Unlock Instructions' 61 | -------------------------------------------------------------------------------- /test/dummy/config/locales/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 | hello: "Hello world" 6 | -------------------------------------------------------------------------------- /test/dummy/config/routes.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Dummy::Application.routes.draw do 4 | root to: 'welcome#index' 5 | resources :users 6 | end 7 | -------------------------------------------------------------------------------- /test/dummy/db/migrate/20130123110019_create_users.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class CreateUsers < ActiveRecord::Migration 4 | def change 5 | create_table :users do |t| 6 | t.string :name 7 | 8 | t.timestamps 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /test/dummy/db/migrate/20130201105206_create_survey.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class CreateSurvey < ActiveRecord::Migration 4 | def self.up 5 | # survey surveys logic 6 | create_table :survey_surveys do |t| 7 | t.string :name 8 | t.text :description 9 | t.integer :attempts_number, default: 0 10 | t.boolean :finished, default: false 11 | t.boolean :active, default: true 12 | 13 | t.timestamps 14 | end 15 | 16 | create_table :survey_questions do |t| 17 | t.integer :survey_id 18 | t.string :text 19 | 20 | t.timestamps 21 | end 22 | 23 | create_table :survey_options do |t| 24 | t.integer :question_id 25 | t.integer :weight, default: 0 26 | t.string :text 27 | t.boolean :correct 28 | 29 | t.timestamps 30 | end 31 | 32 | # survey answer logic 33 | create_table :survey_attempts do |t| 34 | t.belongs_to :participant, polymorphic: true 35 | t.integer :survey_id 36 | t.integer :score 37 | end 38 | 39 | create_table :survey_answers do |t| 40 | t.integer :attempt_id 41 | t.integer :question_id 42 | t.integer :option_id 43 | t.boolean :correct 44 | t.timestamps 45 | end 46 | end 47 | 48 | def self.down 49 | drop_table :survey_surveys 50 | drop_table :survey_questions 51 | drop_table :survey_options 52 | 53 | drop_table :survey_attempts 54 | drop_table :survey_answers 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /test/dummy/db/migrate/20130904084520_create_sections.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class CreateSections < ActiveRecord::Migration 4 | def self.up 5 | create_table :survey_sections do |t| 6 | t.string :head_number 7 | t.string :name 8 | t.text :description 9 | t.integer :survey_id 10 | 11 | t.timestamps 12 | end 13 | 14 | remove_column :survey_questions, :survey_id 15 | add_column :survey_questions, :section_id, :integer 16 | end 17 | 18 | def self.down 19 | drop_table :survey_sections 20 | 21 | remove_column :survey_questions, :section_id 22 | add_column :survey_questions, :survey_id, :integer 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /test/dummy/db/migrate/20130904115621_update_survey_tables.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class UpdateSurveyTables < ActiveRecord::Migration 4 | def change 5 | # Survey Surveys table 6 | add_column :survey_surveys, :locale_name, :string 7 | add_column :survey_surveys, :locale_description, :text 8 | 9 | # Survey Sections table 10 | add_column :survey_sections, :locale_head_number, :string 11 | add_column :survey_sections, :locale_name, :string 12 | add_column :survey_sections, :locale_description, :text 13 | 14 | # Survey Questions table 15 | add_column :survey_questions, :head_number, :string 16 | add_column :survey_questions, :description, :text 17 | add_column :survey_questions, :locale_text, :string 18 | add_column :survey_questions, :locale_head_number, :string 19 | add_column :survey_questions, :locale_description, :text 20 | 21 | # Survey Options table 22 | add_column :survey_options, :locale_text, :string 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /test/dummy/db/migrate/20130905093710_add_types_to_questions_and_options.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddTypesToQuestionsAndOptions < ActiveRecord::Migration 4 | def change 5 | # Survey Questions table 6 | add_column :survey_questions, :questions_type_id, :integer 7 | 8 | # Survey Options table 9 | add_column :survey_options, :options_type_id, :integer 10 | 11 | # Survey Answers table 12 | add_column :survey_answers, :option_text, :text 13 | add_column :survey_answers, :option_number, :integer 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /test/dummy/db/migrate/20130916081314_add_head_number_to_options_table.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddHeadNumberToOptionsTable < ActiveRecord::Migration 4 | def change 5 | # Survey Options table 6 | add_column :survey_options, :head_number, :string 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /test/dummy/db/migrate/20130916102353_create_predefined_values_table.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class CreatePredefinedValuesTable < ActiveRecord::Migration 4 | def change 5 | create_table :survey_predefined_values do |t| 6 | t.string :head_number 7 | t.string :name 8 | t.string :locale_name 9 | t.integer :question_id 10 | 11 | t.timestamps 12 | end 13 | 14 | add_column :survey_answers, :predefined_value_id, :integer 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /test/dummy/db/migrate/20130929102221_add_mandatory_to_questions_table.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddMandatoryToQuestionsTable < ActiveRecord::Migration 4 | def change 5 | # Survey Questions table 6 | add_column :survey_questions, :mandatory, :boolean, default: false 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /test/dummy/db/migrate/20170619155054_create_lessons.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class CreateLessons < ActiveRecord::Migration 4 | def change 5 | create_table :lessons do |t| 6 | t.string :name 7 | 8 | t.timestamps 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /test/dummy/db/migrate/20170619155608_add_lesson_id_to_survey_surveys.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddLessonIdToSurveySurveys < ActiveRecord::Migration 4 | def change 5 | add_column :survey_surveys, :lesson_id, :integer 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /test/dummy/db/schema.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # frozen_string_literal: true 3 | 4 | # This file is auto-generated from the current state of the database. Instead 5 | # of editing this file, please use the migrations feature of Active Record to 6 | # incrementally modify your database, and then regenerate this schema definition. 7 | # 8 | # Note that this schema.rb definition is the authoritative source for your 9 | # database schema. If you need to create the application database on another 10 | # system, you should be using db:schema:load, not running all the migrations 11 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations 12 | # you'll amass, the slower it'll run and the greater likelihood for issues). 13 | # 14 | # It's strongly recommended to check this file into your version control system. 15 | 16 | ActiveRecord::Schema.define(version: 20170619155608) do 17 | create_table 'lessons', force: true do |t| 18 | t.string 'name' 19 | t.datetime 'created_at' 20 | t.datetime 'updated_at' 21 | end 22 | 23 | create_table 'survey_answers', force: true do |t| 24 | t.integer 'attempt_id' 25 | t.integer 'question_id' 26 | t.integer 'option_id' 27 | t.boolean 'correct' 28 | t.datetime 'created_at' 29 | t.datetime 'updated_at' 30 | t.text 'option_text' 31 | t.integer 'option_number' 32 | t.integer 'predefined_value_id' 33 | end 34 | 35 | create_table 'survey_attempts', force: true do |t| 36 | t.integer 'participant_id' 37 | t.string 'participant_type' 38 | t.integer 'survey_id' 39 | t.integer 'score' 40 | end 41 | 42 | create_table 'survey_options', force: true do |t| 43 | t.integer 'question_id' 44 | t.integer 'weight', default: 0 45 | t.string 'text' 46 | t.boolean 'correct' 47 | t.datetime 'created_at' 48 | t.datetime 'updated_at' 49 | t.string 'locale_text' 50 | t.integer 'options_type_id' 51 | t.string 'head_number' 52 | end 53 | 54 | create_table 'survey_predefined_values', force: true do |t| 55 | t.string 'head_number' 56 | t.string 'name' 57 | t.string 'locale_name' 58 | t.integer 'question_id' 59 | t.datetime 'created_at' 60 | t.datetime 'updated_at' 61 | end 62 | 63 | create_table 'survey_questions', force: true do |t| 64 | t.string 'text' 65 | t.datetime 'created_at' 66 | t.datetime 'updated_at' 67 | t.integer 'section_id' 68 | t.string 'head_number' 69 | t.text 'description' 70 | t.string 'locale_text' 71 | t.string 'locale_head_number' 72 | t.text 'locale_description' 73 | t.integer 'questions_type_id' 74 | t.boolean 'mandatory', default: false 75 | end 76 | 77 | create_table 'survey_sections', force: true do |t| 78 | t.string 'head_number' 79 | t.string 'name' 80 | t.text 'description' 81 | t.integer 'survey_id' 82 | t.datetime 'created_at' 83 | t.datetime 'updated_at' 84 | t.string 'locale_head_number' 85 | t.string 'locale_name' 86 | t.text 'locale_description' 87 | end 88 | 89 | create_table 'survey_surveys', force: true do |t| 90 | t.string 'name' 91 | t.text 'description' 92 | t.integer 'attempts_number', default: 0 93 | t.boolean 'finished', default: false 94 | t.boolean 'active', default: true 95 | t.datetime 'created_at' 96 | t.datetime 'updated_at' 97 | t.string 'locale_name' 98 | t.text 'locale_description' 99 | t.integer 'lesson_id' 100 | end 101 | 102 | create_table 'users', force: true do |t| 103 | t.string 'name' 104 | t.datetime 'created_at' 105 | t.datetime 'updated_at' 106 | end 107 | end 108 | -------------------------------------------------------------------------------- /test/dummy/public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 17 | 18 | 19 | 20 | 21 |
    22 |

    The page you were looking for doesn't exist.

    23 |

    You may have mistyped the address or the page may have moved.

    24 |
    25 | 26 | 27 | -------------------------------------------------------------------------------- /test/dummy/public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 17 | 18 | 19 | 20 | 21 |
    22 |

    The change you wanted was rejected.

    23 |

    Maybe you tried to change something you didn't have access to.

    24 |
    25 | 26 | 27 | -------------------------------------------------------------------------------- /test/dummy/public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 17 | 18 | 19 | 20 | 21 |
    22 |

    We're sorry, but something went wrong.

    23 |

    We've been notified about this issue and we'll take a look at it shortly.

    24 |
    25 | 26 | 27 | -------------------------------------------------------------------------------- /test/dummy/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clearfunction/questionnaire/61be93261334b89ea812ad38bd75d1c361295e88/test/dummy/public/favicon.ico -------------------------------------------------------------------------------- /test/dummy/public/javascripts/controls.js: -------------------------------------------------------------------------------- 1 | // script.aculo.us controls.js v1.8.3, Thu Oct 08 11:23:33 +0200 2009 2 | 3 | // Copyright (c) 2005-2009 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) 4 | // (c) 2005-2009 Ivan Krstic (http://blogs.law.harvard.edu/ivan) 5 | // (c) 2005-2009 Jon Tirsen (http://www.tirsen.com) 6 | // Contributors: 7 | // Richard Livsey 8 | // Rahul Bhargava 9 | // Rob Wills 10 | // 11 | // script.aculo.us is freely distributable under the terms of an MIT-style license. 12 | // For details, see the script.aculo.us web site: http://script.aculo.us/ 13 | 14 | // Autocompleter.Base handles all the autocompletion functionality 15 | // that's independent of the data source for autocompletion. This 16 | // includes drawing the autocompletion menu, observing keyboard 17 | // and mouse events, and similar. 18 | // 19 | // Specific autocompleters need to provide, at the very least, 20 | // a getUpdatedChoices function that will be invoked every time 21 | // the text inside the monitored textbox changes. This method 22 | // should get the text for which to provide autocompletion by 23 | // invoking this.getToken(), NOT by directly accessing 24 | // this.element.value. This is to allow incremental tokenized 25 | // autocompletion. Specific auto-completion logic (AJAX, etc) 26 | // belongs in getUpdatedChoices. 27 | // 28 | // Tokenized incremental autocompletion is enabled automatically 29 | // when an autocompleter is instantiated with the 'tokens' option 30 | // in the options parameter, e.g.: 31 | // new Ajax.Autocompleter('id','upd', '/url/', { tokens: ',' }); 32 | // will incrementally autocomplete with a comma as the token. 33 | // Additionally, ',' in the above example can be replaced with 34 | // a token array, e.g. { tokens: [',', '\n'] } which 35 | // enables autocompletion on multiple tokens. This is most 36 | // useful when one of the tokens is \n (a newline), as it 37 | // allows smart autocompletion after linebreaks. 38 | 39 | if(typeof Effect == 'undefined') 40 | throw("controls.js requires including script.aculo.us' effects.js library"); 41 | 42 | var Autocompleter = { }; 43 | Autocompleter.Base = Class.create({ 44 | baseInitialize: function(element, update, options) { 45 | element = $(element); 46 | this.element = element; 47 | this.update = $(update); 48 | this.hasFocus = false; 49 | this.changed = false; 50 | this.active = false; 51 | this.index = 0; 52 | this.entryCount = 0; 53 | this.oldElementValue = this.element.value; 54 | 55 | if(this.setOptions) 56 | this.setOptions(options); 57 | else 58 | this.options = options || { }; 59 | 60 | this.options.paramName = this.options.paramName || this.element.name; 61 | this.options.tokens = this.options.tokens || []; 62 | this.options.frequency = this.options.frequency || 0.4; 63 | this.options.minChars = this.options.minChars || 1; 64 | this.options.onShow = this.options.onShow || 65 | function(element, update){ 66 | if(!update.style.position || update.style.position=='absolute') { 67 | update.style.position = 'absolute'; 68 | Position.clone(element, update, { 69 | setHeight: false, 70 | offsetTop: element.offsetHeight 71 | }); 72 | } 73 | Effect.Appear(update,{duration:0.15}); 74 | }; 75 | this.options.onHide = this.options.onHide || 76 | function(element, update){ new Effect.Fade(update,{duration:0.15}) }; 77 | 78 | if(typeof(this.options.tokens) == 'string') 79 | this.options.tokens = new Array(this.options.tokens); 80 | // Force carriage returns as token delimiters anyway 81 | if (!this.options.tokens.include('\n')) 82 | this.options.tokens.push('\n'); 83 | 84 | this.observer = null; 85 | 86 | this.element.setAttribute('autocomplete','off'); 87 | 88 | Element.hide(this.update); 89 | 90 | Event.observe(this.element, 'blur', this.onBlur.bindAsEventListener(this)); 91 | Event.observe(this.element, 'keydown', this.onKeyPress.bindAsEventListener(this)); 92 | }, 93 | 94 | show: function() { 95 | if(Element.getStyle(this.update, 'display')=='none') this.options.onShow(this.element, this.update); 96 | if(!this.iefix && 97 | (Prototype.Browser.IE) && 98 | (Element.getStyle(this.update, 'position')=='absolute')) { 99 | new Insertion.After(this.update, 100 | ''); 103 | this.iefix = $(this.update.id+'_iefix'); 104 | } 105 | if(this.iefix) setTimeout(this.fixIEOverlapping.bind(this), 50); 106 | }, 107 | 108 | fixIEOverlapping: function() { 109 | Position.clone(this.update, this.iefix, {setTop:(!this.update.style.height)}); 110 | this.iefix.style.zIndex = 1; 111 | this.update.style.zIndex = 2; 112 | Element.show(this.iefix); 113 | }, 114 | 115 | hide: function() { 116 | this.stopIndicator(); 117 | if(Element.getStyle(this.update, 'display')!='none') this.options.onHide(this.element, this.update); 118 | if(this.iefix) Element.hide(this.iefix); 119 | }, 120 | 121 | startIndicator: function() { 122 | if(this.options.indicator) Element.show(this.options.indicator); 123 | }, 124 | 125 | stopIndicator: function() { 126 | if(this.options.indicator) Element.hide(this.options.indicator); 127 | }, 128 | 129 | onKeyPress: function(event) { 130 | if(this.active) 131 | switch(event.keyCode) { 132 | case Event.KEY_TAB: 133 | case Event.KEY_RETURN: 134 | this.selectEntry(); 135 | Event.stop(event); 136 | case Event.KEY_ESC: 137 | this.hide(); 138 | this.active = false; 139 | Event.stop(event); 140 | return; 141 | case Event.KEY_LEFT: 142 | case Event.KEY_RIGHT: 143 | return; 144 | case Event.KEY_UP: 145 | this.markPrevious(); 146 | this.render(); 147 | Event.stop(event); 148 | return; 149 | case Event.KEY_DOWN: 150 | this.markNext(); 151 | this.render(); 152 | Event.stop(event); 153 | return; 154 | } 155 | else 156 | if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN || 157 | (Prototype.Browser.WebKit > 0 && event.keyCode == 0)) return; 158 | 159 | this.changed = true; 160 | this.hasFocus = true; 161 | 162 | if(this.observer) clearTimeout(this.observer); 163 | this.observer = 164 | setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000); 165 | }, 166 | 167 | activate: function() { 168 | this.changed = false; 169 | this.hasFocus = true; 170 | this.getUpdatedChoices(); 171 | }, 172 | 173 | onHover: function(event) { 174 | var element = Event.findElement(event, 'LI'); 175 | if(this.index != element.autocompleteIndex) 176 | { 177 | this.index = element.autocompleteIndex; 178 | this.render(); 179 | } 180 | Event.stop(event); 181 | }, 182 | 183 | onClick: function(event) { 184 | var element = Event.findElement(event, 'LI'); 185 | this.index = element.autocompleteIndex; 186 | this.selectEntry(); 187 | this.hide(); 188 | }, 189 | 190 | onBlur: function(event) { 191 | // needed to make click events working 192 | setTimeout(this.hide.bind(this), 250); 193 | this.hasFocus = false; 194 | this.active = false; 195 | }, 196 | 197 | render: function() { 198 | if(this.entryCount > 0) { 199 | for (var i = 0; i < this.entryCount; i++) 200 | this.index==i ? 201 | Element.addClassName(this.getEntry(i),"selected") : 202 | Element.removeClassName(this.getEntry(i),"selected"); 203 | if(this.hasFocus) { 204 | this.show(); 205 | this.active = true; 206 | } 207 | } else { 208 | this.active = false; 209 | this.hide(); 210 | } 211 | }, 212 | 213 | markPrevious: function() { 214 | if(this.index > 0) this.index--; 215 | else this.index = this.entryCount-1; 216 | this.getEntry(this.index).scrollIntoView(true); 217 | }, 218 | 219 | markNext: function() { 220 | if(this.index < this.entryCount-1) this.index++; 221 | else this.index = 0; 222 | this.getEntry(this.index).scrollIntoView(false); 223 | }, 224 | 225 | getEntry: function(index) { 226 | return this.update.firstChild.childNodes[index]; 227 | }, 228 | 229 | getCurrentEntry: function() { 230 | return this.getEntry(this.index); 231 | }, 232 | 233 | selectEntry: function() { 234 | this.active = false; 235 | this.updateElement(this.getCurrentEntry()); 236 | }, 237 | 238 | updateElement: function(selectedElement) { 239 | if (this.options.updateElement) { 240 | this.options.updateElement(selectedElement); 241 | return; 242 | } 243 | var value = ''; 244 | if (this.options.select) { 245 | var nodes = $(selectedElement).select('.' + this.options.select) || []; 246 | if(nodes.length>0) value = Element.collectTextNodes(nodes[0], this.options.select); 247 | } else 248 | value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal'); 249 | 250 | var bounds = this.getTokenBounds(); 251 | if (bounds[0] != -1) { 252 | var newValue = this.element.value.substr(0, bounds[0]); 253 | var whitespace = this.element.value.substr(bounds[0]).match(/^\s+/); 254 | if (whitespace) 255 | newValue += whitespace[0]; 256 | this.element.value = newValue + value + this.element.value.substr(bounds[1]); 257 | } else { 258 | this.element.value = value; 259 | } 260 | this.oldElementValue = this.element.value; 261 | this.element.focus(); 262 | 263 | if (this.options.afterUpdateElement) 264 | this.options.afterUpdateElement(this.element, selectedElement); 265 | }, 266 | 267 | updateChoices: function(choices) { 268 | if(!this.changed && this.hasFocus) { 269 | this.update.innerHTML = choices; 270 | Element.cleanWhitespace(this.update); 271 | Element.cleanWhitespace(this.update.down()); 272 | 273 | if(this.update.firstChild && this.update.down().childNodes) { 274 | this.entryCount = 275 | this.update.down().childNodes.length; 276 | for (var i = 0; i < this.entryCount; i++) { 277 | var entry = this.getEntry(i); 278 | entry.autocompleteIndex = i; 279 | this.addObservers(entry); 280 | } 281 | } else { 282 | this.entryCount = 0; 283 | } 284 | 285 | this.stopIndicator(); 286 | this.index = 0; 287 | 288 | if(this.entryCount==1 && this.options.autoSelect) { 289 | this.selectEntry(); 290 | this.hide(); 291 | } else { 292 | this.render(); 293 | } 294 | } 295 | }, 296 | 297 | addObservers: function(element) { 298 | Event.observe(element, "mouseover", this.onHover.bindAsEventListener(this)); 299 | Event.observe(element, "click", this.onClick.bindAsEventListener(this)); 300 | }, 301 | 302 | onObserverEvent: function() { 303 | this.changed = false; 304 | this.tokenBounds = null; 305 | if(this.getToken().length>=this.options.minChars) { 306 | this.getUpdatedChoices(); 307 | } else { 308 | this.active = false; 309 | this.hide(); 310 | } 311 | this.oldElementValue = this.element.value; 312 | }, 313 | 314 | getToken: function() { 315 | var bounds = this.getTokenBounds(); 316 | return this.element.value.substring(bounds[0], bounds[1]).strip(); 317 | }, 318 | 319 | getTokenBounds: function() { 320 | if (null != this.tokenBounds) return this.tokenBounds; 321 | var value = this.element.value; 322 | if (value.strip().empty()) return [-1, 0]; 323 | var diff = arguments.callee.getFirstDifferencePos(value, this.oldElementValue); 324 | var offset = (diff == this.oldElementValue.length ? 1 : 0); 325 | var prevTokenPos = -1, nextTokenPos = value.length; 326 | var tp; 327 | for (var index = 0, l = this.options.tokens.length; index < l; ++index) { 328 | tp = value.lastIndexOf(this.options.tokens[index], diff + offset - 1); 329 | if (tp > prevTokenPos) prevTokenPos = tp; 330 | tp = value.indexOf(this.options.tokens[index], diff + offset); 331 | if (-1 != tp && tp < nextTokenPos) nextTokenPos = tp; 332 | } 333 | return (this.tokenBounds = [prevTokenPos + 1, nextTokenPos]); 334 | } 335 | }); 336 | 337 | Autocompleter.Base.prototype.getTokenBounds.getFirstDifferencePos = function(newS, oldS) { 338 | var boundary = Math.min(newS.length, oldS.length); 339 | for (var index = 0; index < boundary; ++index) 340 | if (newS[index] != oldS[index]) 341 | return index; 342 | return boundary; 343 | }; 344 | 345 | Ajax.Autocompleter = Class.create(Autocompleter.Base, { 346 | initialize: function(element, update, url, options) { 347 | this.baseInitialize(element, update, options); 348 | this.options.asynchronous = true; 349 | this.options.onComplete = this.onComplete.bind(this); 350 | this.options.defaultParams = this.options.parameters || null; 351 | this.url = url; 352 | }, 353 | 354 | getUpdatedChoices: function() { 355 | this.startIndicator(); 356 | 357 | var entry = encodeURIComponent(this.options.paramName) + '=' + 358 | encodeURIComponent(this.getToken()); 359 | 360 | this.options.parameters = this.options.callback ? 361 | this.options.callback(this.element, entry) : entry; 362 | 363 | if(this.options.defaultParams) 364 | this.options.parameters += '&' + this.options.defaultParams; 365 | 366 | new Ajax.Request(this.url, this.options); 367 | }, 368 | 369 | onComplete: function(request) { 370 | this.updateChoices(request.responseText); 371 | } 372 | }); 373 | 374 | // The local array autocompleter. Used when you'd prefer to 375 | // inject an array of autocompletion options into the page, rather 376 | // than sending out Ajax queries, which can be quite slow sometimes. 377 | // 378 | // The constructor takes four parameters. The first two are, as usual, 379 | // the id of the monitored textbox, and id of the autocompletion menu. 380 | // The third is the array you want to autocomplete from, and the fourth 381 | // is the options block. 382 | // 383 | // Extra local autocompletion options: 384 | // - choices - How many autocompletion choices to offer 385 | // 386 | // - partialSearch - If false, the autocompleter will match entered 387 | // text only at the beginning of strings in the 388 | // autocomplete array. Defaults to true, which will 389 | // match text at the beginning of any *word* in the 390 | // strings in the autocomplete array. If you want to 391 | // search anywhere in the string, additionally set 392 | // the option fullSearch to true (default: off). 393 | // 394 | // - fullSsearch - Search anywhere in autocomplete array strings. 395 | // 396 | // - partialChars - How many characters to enter before triggering 397 | // a partial match (unlike minChars, which defines 398 | // how many characters are required to do any match 399 | // at all). Defaults to 2. 400 | // 401 | // - ignoreCase - Whether to ignore case when autocompleting. 402 | // Defaults to true. 403 | // 404 | // It's possible to pass in a custom function as the 'selector' 405 | // option, if you prefer to write your own autocompletion logic. 406 | // In that case, the other options above will not apply unless 407 | // you support them. 408 | 409 | Autocompleter.Local = Class.create(Autocompleter.Base, { 410 | initialize: function(element, update, array, options) { 411 | this.baseInitialize(element, update, options); 412 | this.options.array = array; 413 | }, 414 | 415 | getUpdatedChoices: function() { 416 | this.updateChoices(this.options.selector(this)); 417 | }, 418 | 419 | setOptions: function(options) { 420 | this.options = Object.extend({ 421 | choices: 10, 422 | partialSearch: true, 423 | partialChars: 2, 424 | ignoreCase: true, 425 | fullSearch: false, 426 | selector: function(instance) { 427 | var ret = []; // Beginning matches 428 | var partial = []; // Inside matches 429 | var entry = instance.getToken(); 430 | var count = 0; 431 | 432 | for (var i = 0; i < instance.options.array.length && 433 | ret.length < instance.options.choices ; i++) { 434 | 435 | var elem = instance.options.array[i]; 436 | var foundPos = instance.options.ignoreCase ? 437 | elem.toLowerCase().indexOf(entry.toLowerCase()) : 438 | elem.indexOf(entry); 439 | 440 | while (foundPos != -1) { 441 | if (foundPos == 0 && elem.length != entry.length) { 442 | ret.push("
  • " + elem.substr(0, entry.length) + "" + 443 | elem.substr(entry.length) + "
  • "); 444 | break; 445 | } else if (entry.length >= instance.options.partialChars && 446 | instance.options.partialSearch && foundPos != -1) { 447 | if (instance.options.fullSearch || /\s/.test(elem.substr(foundPos-1,1))) { 448 | partial.push("
  • " + elem.substr(0, foundPos) + "" + 449 | elem.substr(foundPos, entry.length) + "" + elem.substr( 450 | foundPos + entry.length) + "
  • "); 451 | break; 452 | } 453 | } 454 | 455 | foundPos = instance.options.ignoreCase ? 456 | elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) : 457 | elem.indexOf(entry, foundPos + 1); 458 | 459 | } 460 | } 461 | if (partial.length) 462 | ret = ret.concat(partial.slice(0, instance.options.choices - ret.length)); 463 | return ""; 464 | } 465 | }, options || { }); 466 | } 467 | }); 468 | 469 | // AJAX in-place editor and collection editor 470 | // Full rewrite by Christophe Porteneuve (April 2007). 471 | 472 | // Use this if you notice weird scrolling problems on some browsers, 473 | // the DOM might be a bit confused when this gets called so do this 474 | // waits 1 ms (with setTimeout) until it does the activation 475 | Field.scrollFreeActivate = function(field) { 476 | setTimeout(function() { 477 | Field.activate(field); 478 | }, 1); 479 | }; 480 | 481 | Ajax.InPlaceEditor = Class.create({ 482 | initialize: function(element, url, options) { 483 | this.url = url; 484 | this.element = element = $(element); 485 | this.prepareOptions(); 486 | this._controls = { }; 487 | arguments.callee.dealWithDeprecatedOptions(options); // DEPRECATION LAYER!!! 488 | Object.extend(this.options, options || { }); 489 | if (!this.options.formId && this.element.id) { 490 | this.options.formId = this.element.id + '-inplaceeditor'; 491 | if ($(this.options.formId)) 492 | this.options.formId = ''; 493 | } 494 | if (this.options.externalControl) 495 | this.options.externalControl = $(this.options.externalControl); 496 | if (!this.options.externalControl) 497 | this.options.externalControlOnly = false; 498 | this._originalBackground = this.element.getStyle('background-color') || 'transparent'; 499 | this.element.title = this.options.clickToEditText; 500 | this._boundCancelHandler = this.handleFormCancellation.bind(this); 501 | this._boundComplete = (this.options.onComplete || Prototype.emptyFunction).bind(this); 502 | this._boundFailureHandler = this.handleAJAXFailure.bind(this); 503 | this._boundSubmitHandler = this.handleFormSubmission.bind(this); 504 | this._boundWrapperHandler = this.wrapUp.bind(this); 505 | this.registerListeners(); 506 | }, 507 | checkForEscapeOrReturn: function(e) { 508 | if (!this._editing || e.ctrlKey || e.altKey || e.shiftKey) return; 509 | if (Event.KEY_ESC == e.keyCode) 510 | this.handleFormCancellation(e); 511 | else if (Event.KEY_RETURN == e.keyCode) 512 | this.handleFormSubmission(e); 513 | }, 514 | createControl: function(mode, handler, extraClasses) { 515 | var control = this.options[mode + 'Control']; 516 | var text = this.options[mode + 'Text']; 517 | if ('button' == control) { 518 | var btn = document.createElement('input'); 519 | btn.type = 'submit'; 520 | btn.value = text; 521 | btn.className = 'editor_' + mode + '_button'; 522 | if ('cancel' == mode) 523 | btn.onclick = this._boundCancelHandler; 524 | this._form.appendChild(btn); 525 | this._controls[mode] = btn; 526 | } else if ('link' == control) { 527 | var link = document.createElement('a'); 528 | link.href = '#'; 529 | link.appendChild(document.createTextNode(text)); 530 | link.onclick = 'cancel' == mode ? this._boundCancelHandler : this._boundSubmitHandler; 531 | link.className = 'editor_' + mode + '_link'; 532 | if (extraClasses) 533 | link.className += ' ' + extraClasses; 534 | this._form.appendChild(link); 535 | this._controls[mode] = link; 536 | } 537 | }, 538 | createEditField: function() { 539 | var text = (this.options.loadTextURL ? this.options.loadingText : this.getText()); 540 | var fld; 541 | if (1 >= this.options.rows && !/\r|\n/.test(this.getText())) { 542 | fld = document.createElement('input'); 543 | fld.type = 'text'; 544 | var size = this.options.size || this.options.cols || 0; 545 | if (0 < size) fld.size = size; 546 | } else { 547 | fld = document.createElement('textarea'); 548 | fld.rows = (1 >= this.options.rows ? this.options.autoRows : this.options.rows); 549 | fld.cols = this.options.cols || 40; 550 | } 551 | fld.name = this.options.paramName; 552 | fld.value = text; // No HTML breaks conversion anymore 553 | fld.className = 'editor_field'; 554 | if (this.options.submitOnBlur) 555 | fld.onblur = this._boundSubmitHandler; 556 | this._controls.editor = fld; 557 | if (this.options.loadTextURL) 558 | this.loadExternalText(); 559 | this._form.appendChild(this._controls.editor); 560 | }, 561 | createForm: function() { 562 | var ipe = this; 563 | function addText(mode, condition) { 564 | var text = ipe.options['text' + mode + 'Controls']; 565 | if (!text || condition === false) return; 566 | ipe._form.appendChild(document.createTextNode(text)); 567 | }; 568 | this._form = $(document.createElement('form')); 569 | this._form.id = this.options.formId; 570 | this._form.addClassName(this.options.formClassName); 571 | this._form.onsubmit = this._boundSubmitHandler; 572 | this.createEditField(); 573 | if ('textarea' == this._controls.editor.tagName.toLowerCase()) 574 | this._form.appendChild(document.createElement('br')); 575 | if (this.options.onFormCustomization) 576 | this.options.onFormCustomization(this, this._form); 577 | addText('Before', this.options.okControl || this.options.cancelControl); 578 | this.createControl('ok', this._boundSubmitHandler); 579 | addText('Between', this.options.okControl && this.options.cancelControl); 580 | this.createControl('cancel', this._boundCancelHandler, 'editor_cancel'); 581 | addText('After', this.options.okControl || this.options.cancelControl); 582 | }, 583 | destroy: function() { 584 | if (this._oldInnerHTML) 585 | this.element.innerHTML = this._oldInnerHTML; 586 | this.leaveEditMode(); 587 | this.unregisterListeners(); 588 | }, 589 | enterEditMode: function(e) { 590 | if (this._saving || this._editing) return; 591 | this._editing = true; 592 | this.triggerCallback('onEnterEditMode'); 593 | if (this.options.externalControl) 594 | this.options.externalControl.hide(); 595 | this.element.hide(); 596 | this.createForm(); 597 | this.element.parentNode.insertBefore(this._form, this.element); 598 | if (!this.options.loadTextURL) 599 | this.postProcessEditField(); 600 | if (e) Event.stop(e); 601 | }, 602 | enterHover: function(e) { 603 | if (this.options.hoverClassName) 604 | this.element.addClassName(this.options.hoverClassName); 605 | if (this._saving) return; 606 | this.triggerCallback('onEnterHover'); 607 | }, 608 | getText: function() { 609 | return this.element.innerHTML.unescapeHTML(); 610 | }, 611 | handleAJAXFailure: function(transport) { 612 | this.triggerCallback('onFailure', transport); 613 | if (this._oldInnerHTML) { 614 | this.element.innerHTML = this._oldInnerHTML; 615 | this._oldInnerHTML = null; 616 | } 617 | }, 618 | handleFormCancellation: function(e) { 619 | this.wrapUp(); 620 | if (e) Event.stop(e); 621 | }, 622 | handleFormSubmission: function(e) { 623 | var form = this._form; 624 | var value = $F(this._controls.editor); 625 | this.prepareSubmission(); 626 | var params = this.options.callback(form, value) || ''; 627 | if (Object.isString(params)) 628 | params = params.toQueryParams(); 629 | params.editorId = this.element.id; 630 | if (this.options.htmlResponse) { 631 | var options = Object.extend({ evalScripts: true }, this.options.ajaxOptions); 632 | Object.extend(options, { 633 | parameters: params, 634 | onComplete: this._boundWrapperHandler, 635 | onFailure: this._boundFailureHandler 636 | }); 637 | new Ajax.Updater({ success: this.element }, this.url, options); 638 | } else { 639 | var options = Object.extend({ method: 'get' }, this.options.ajaxOptions); 640 | Object.extend(options, { 641 | parameters: params, 642 | onComplete: this._boundWrapperHandler, 643 | onFailure: this._boundFailureHandler 644 | }); 645 | new Ajax.Request(this.url, options); 646 | } 647 | if (e) Event.stop(e); 648 | }, 649 | leaveEditMode: function() { 650 | this.element.removeClassName(this.options.savingClassName); 651 | this.removeForm(); 652 | this.leaveHover(); 653 | this.element.style.backgroundColor = this._originalBackground; 654 | this.element.show(); 655 | if (this.options.externalControl) 656 | this.options.externalControl.show(); 657 | this._saving = false; 658 | this._editing = false; 659 | this._oldInnerHTML = null; 660 | this.triggerCallback('onLeaveEditMode'); 661 | }, 662 | leaveHover: function(e) { 663 | if (this.options.hoverClassName) 664 | this.element.removeClassName(this.options.hoverClassName); 665 | if (this._saving) return; 666 | this.triggerCallback('onLeaveHover'); 667 | }, 668 | loadExternalText: function() { 669 | this._form.addClassName(this.options.loadingClassName); 670 | this._controls.editor.disabled = true; 671 | var options = Object.extend({ method: 'get' }, this.options.ajaxOptions); 672 | Object.extend(options, { 673 | parameters: 'editorId=' + encodeURIComponent(this.element.id), 674 | onComplete: Prototype.emptyFunction, 675 | onSuccess: function(transport) { 676 | this._form.removeClassName(this.options.loadingClassName); 677 | var text = transport.responseText; 678 | if (this.options.stripLoadedTextTags) 679 | text = text.stripTags(); 680 | this._controls.editor.value = text; 681 | this._controls.editor.disabled = false; 682 | this.postProcessEditField(); 683 | }.bind(this), 684 | onFailure: this._boundFailureHandler 685 | }); 686 | new Ajax.Request(this.options.loadTextURL, options); 687 | }, 688 | postProcessEditField: function() { 689 | var fpc = this.options.fieldPostCreation; 690 | if (fpc) 691 | $(this._controls.editor)['focus' == fpc ? 'focus' : 'activate'](); 692 | }, 693 | prepareOptions: function() { 694 | this.options = Object.clone(Ajax.InPlaceEditor.DefaultOptions); 695 | Object.extend(this.options, Ajax.InPlaceEditor.DefaultCallbacks); 696 | [this._extraDefaultOptions].flatten().compact().each(function(defs) { 697 | Object.extend(this.options, defs); 698 | }.bind(this)); 699 | }, 700 | prepareSubmission: function() { 701 | this._saving = true; 702 | this.removeForm(); 703 | this.leaveHover(); 704 | this.showSaving(); 705 | }, 706 | registerListeners: function() { 707 | this._listeners = { }; 708 | var listener; 709 | $H(Ajax.InPlaceEditor.Listeners).each(function(pair) { 710 | listener = this[pair.value].bind(this); 711 | this._listeners[pair.key] = listener; 712 | if (!this.options.externalControlOnly) 713 | this.element.observe(pair.key, listener); 714 | if (this.options.externalControl) 715 | this.options.externalControl.observe(pair.key, listener); 716 | }.bind(this)); 717 | }, 718 | removeForm: function() { 719 | if (!this._form) return; 720 | this._form.remove(); 721 | this._form = null; 722 | this._controls = { }; 723 | }, 724 | showSaving: function() { 725 | this._oldInnerHTML = this.element.innerHTML; 726 | this.element.innerHTML = this.options.savingText; 727 | this.element.addClassName(this.options.savingClassName); 728 | this.element.style.backgroundColor = this._originalBackground; 729 | this.element.show(); 730 | }, 731 | triggerCallback: function(cbName, arg) { 732 | if ('function' == typeof this.options[cbName]) { 733 | this.options[cbName](this, arg); 734 | } 735 | }, 736 | unregisterListeners: function() { 737 | $H(this._listeners).each(function(pair) { 738 | if (!this.options.externalControlOnly) 739 | this.element.stopObserving(pair.key, pair.value); 740 | if (this.options.externalControl) 741 | this.options.externalControl.stopObserving(pair.key, pair.value); 742 | }.bind(this)); 743 | }, 744 | wrapUp: function(transport) { 745 | this.leaveEditMode(); 746 | // Can't use triggerCallback due to backward compatibility: requires 747 | // binding + direct element 748 | this._boundComplete(transport, this.element); 749 | } 750 | }); 751 | 752 | Object.extend(Ajax.InPlaceEditor.prototype, { 753 | dispose: Ajax.InPlaceEditor.prototype.destroy 754 | }); 755 | 756 | Ajax.InPlaceCollectionEditor = Class.create(Ajax.InPlaceEditor, { 757 | initialize: function($super, element, url, options) { 758 | this._extraDefaultOptions = Ajax.InPlaceCollectionEditor.DefaultOptions; 759 | $super(element, url, options); 760 | }, 761 | 762 | createEditField: function() { 763 | var list = document.createElement('select'); 764 | list.name = this.options.paramName; 765 | list.size = 1; 766 | this._controls.editor = list; 767 | this._collection = this.options.collection || []; 768 | if (this.options.loadCollectionURL) 769 | this.loadCollection(); 770 | else 771 | this.checkForExternalText(); 772 | this._form.appendChild(this._controls.editor); 773 | }, 774 | 775 | loadCollection: function() { 776 | this._form.addClassName(this.options.loadingClassName); 777 | this.showLoadingText(this.options.loadingCollectionText); 778 | var options = Object.extend({ method: 'get' }, this.options.ajaxOptions); 779 | Object.extend(options, { 780 | parameters: 'editorId=' + encodeURIComponent(this.element.id), 781 | onComplete: Prototype.emptyFunction, 782 | onSuccess: function(transport) { 783 | var js = transport.responseText.strip(); 784 | if (!/^\[.*\]$/.test(js)) // TODO: improve sanity check 785 | throw('Server returned an invalid collection representation.'); 786 | this._collection = eval(js); 787 | this.checkForExternalText(); 788 | }.bind(this), 789 | onFailure: this.onFailure 790 | }); 791 | new Ajax.Request(this.options.loadCollectionURL, options); 792 | }, 793 | 794 | showLoadingText: function(text) { 795 | this._controls.editor.disabled = true; 796 | var tempOption = this._controls.editor.firstChild; 797 | if (!tempOption) { 798 | tempOption = document.createElement('option'); 799 | tempOption.value = ''; 800 | this._controls.editor.appendChild(tempOption); 801 | tempOption.selected = true; 802 | } 803 | tempOption.update((text || '').stripScripts().stripTags()); 804 | }, 805 | 806 | checkForExternalText: function() { 807 | this._text = this.getText(); 808 | if (this.options.loadTextURL) 809 | this.loadExternalText(); 810 | else 811 | this.buildOptionList(); 812 | }, 813 | 814 | loadExternalText: function() { 815 | this.showLoadingText(this.options.loadingText); 816 | var options = Object.extend({ method: 'get' }, this.options.ajaxOptions); 817 | Object.extend(options, { 818 | parameters: 'editorId=' + encodeURIComponent(this.element.id), 819 | onComplete: Prototype.emptyFunction, 820 | onSuccess: function(transport) { 821 | this._text = transport.responseText.strip(); 822 | this.buildOptionList(); 823 | }.bind(this), 824 | onFailure: this.onFailure 825 | }); 826 | new Ajax.Request(this.options.loadTextURL, options); 827 | }, 828 | 829 | buildOptionList: function() { 830 | this._form.removeClassName(this.options.loadingClassName); 831 | this._collection = this._collection.map(function(entry) { 832 | return 2 === entry.length ? entry : [entry, entry].flatten(); 833 | }); 834 | var marker = ('value' in this.options) ? this.options.value : this._text; 835 | var textFound = this._collection.any(function(entry) { 836 | return entry[0] == marker; 837 | }.bind(this)); 838 | this._controls.editor.update(''); 839 | var option; 840 | this._collection.each(function(entry, index) { 841 | option = document.createElement('option'); 842 | option.value = entry[0]; 843 | option.selected = textFound ? entry[0] == marker : 0 == index; 844 | option.appendChild(document.createTextNode(entry[1])); 845 | this._controls.editor.appendChild(option); 846 | }.bind(this)); 847 | this._controls.editor.disabled = false; 848 | Field.scrollFreeActivate(this._controls.editor); 849 | } 850 | }); 851 | 852 | //**** DEPRECATION LAYER FOR InPlace[Collection]Editor! **** 853 | //**** This only exists for a while, in order to let **** 854 | //**** users adapt to the new API. Read up on the new **** 855 | //**** API and convert your code to it ASAP! **** 856 | 857 | Ajax.InPlaceEditor.prototype.initialize.dealWithDeprecatedOptions = function(options) { 858 | if (!options) return; 859 | function fallback(name, expr) { 860 | if (name in options || expr === undefined) return; 861 | options[name] = expr; 862 | }; 863 | fallback('cancelControl', (options.cancelLink ? 'link' : (options.cancelButton ? 'button' : 864 | options.cancelLink == options.cancelButton == false ? false : undefined))); 865 | fallback('okControl', (options.okLink ? 'link' : (options.okButton ? 'button' : 866 | options.okLink == options.okButton == false ? false : undefined))); 867 | fallback('highlightColor', options.highlightcolor); 868 | fallback('highlightEndColor', options.highlightendcolor); 869 | }; 870 | 871 | Object.extend(Ajax.InPlaceEditor, { 872 | DefaultOptions: { 873 | ajaxOptions: { }, 874 | autoRows: 3, // Use when multi-line w/ rows == 1 875 | cancelControl: 'link', // 'link'|'button'|false 876 | cancelText: 'cancel', 877 | clickToEditText: 'Click to edit', 878 | externalControl: null, // id|elt 879 | externalControlOnly: false, 880 | fieldPostCreation: 'activate', // 'activate'|'focus'|false 881 | formClassName: 'inplaceeditor-form', 882 | formId: null, // id|elt 883 | highlightColor: '#ffff99', 884 | highlightEndColor: '#ffffff', 885 | hoverClassName: '', 886 | htmlResponse: true, 887 | loadingClassName: 'inplaceeditor-loading', 888 | loadingText: 'Loading...', 889 | okControl: 'button', // 'link'|'button'|false 890 | okText: 'ok', 891 | paramName: 'value', 892 | rows: 1, // If 1 and multi-line, uses autoRows 893 | savingClassName: 'inplaceeditor-saving', 894 | savingText: 'Saving...', 895 | size: 0, 896 | stripLoadedTextTags: false, 897 | submitOnBlur: false, 898 | textAfterControls: '', 899 | textBeforeControls: '', 900 | textBetweenControls: '' 901 | }, 902 | DefaultCallbacks: { 903 | callback: function(form) { 904 | return Form.serialize(form); 905 | }, 906 | onComplete: function(transport, element) { 907 | // For backward compatibility, this one is bound to the IPE, and passes 908 | // the element directly. It was too often customized, so we don't break it. 909 | new Effect.Highlight(element, { 910 | startcolor: this.options.highlightColor, keepBackgroundImage: true }); 911 | }, 912 | onEnterEditMode: null, 913 | onEnterHover: function(ipe) { 914 | ipe.element.style.backgroundColor = ipe.options.highlightColor; 915 | if (ipe._effect) 916 | ipe._effect.cancel(); 917 | }, 918 | onFailure: function(transport, ipe) { 919 | alert('Error communication with the server: ' + transport.responseText.stripTags()); 920 | }, 921 | onFormCustomization: null, // Takes the IPE and its generated form, after editor, before controls. 922 | onLeaveEditMode: null, 923 | onLeaveHover: function(ipe) { 924 | ipe._effect = new Effect.Highlight(ipe.element, { 925 | startcolor: ipe.options.highlightColor, endcolor: ipe.options.highlightEndColor, 926 | restorecolor: ipe._originalBackground, keepBackgroundImage: true 927 | }); 928 | } 929 | }, 930 | Listeners: { 931 | click: 'enterEditMode', 932 | keydown: 'checkForEscapeOrReturn', 933 | mouseover: 'enterHover', 934 | mouseout: 'leaveHover' 935 | } 936 | }); 937 | 938 | Ajax.InPlaceCollectionEditor.DefaultOptions = { 939 | loadingCollectionText: 'Loading options...' 940 | }; 941 | 942 | // Delayed observer, like Form.Element.Observer, 943 | // but waits for delay after last key input 944 | // Ideal for live-search fields 945 | 946 | Form.Element.DelayedObserver = Class.create({ 947 | initialize: function(element, delay, callback) { 948 | this.delay = delay || 0.5; 949 | this.element = $(element); 950 | this.callback = callback; 951 | this.timer = null; 952 | this.lastValue = $F(this.element); 953 | Event.observe(this.element,'keyup',this.delayedListener.bindAsEventListener(this)); 954 | }, 955 | delayedListener: function(event) { 956 | if(this.lastValue == $F(this.element)) return; 957 | if(this.timer) clearTimeout(this.timer); 958 | this.timer = setTimeout(this.onTimerEvent.bind(this), this.delay * 1000); 959 | this.lastValue = $F(this.element); 960 | }, 961 | onTimerEvent: function() { 962 | this.timer = null; 963 | this.callback(this.element, $F(this.element)); 964 | } 965 | }); -------------------------------------------------------------------------------- /test/dummy/public/javascripts/dragdrop.js: -------------------------------------------------------------------------------- 1 | // script.aculo.us dragdrop.js v1.8.3, Thu Oct 08 11:23:33 +0200 2009 2 | 3 | // Copyright (c) 2005-2009 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) 4 | // 5 | // script.aculo.us is freely distributable under the terms of an MIT-style license. 6 | // For details, see the script.aculo.us web site: http://script.aculo.us/ 7 | 8 | if(Object.isUndefined(Effect)) 9 | throw("dragdrop.js requires including script.aculo.us' effects.js library"); 10 | 11 | var Droppables = { 12 | drops: [], 13 | 14 | remove: function(element) { 15 | this.drops = this.drops.reject(function(d) { return d.element==$(element) }); 16 | }, 17 | 18 | add: function(element) { 19 | element = $(element); 20 | var options = Object.extend({ 21 | greedy: true, 22 | hoverclass: null, 23 | tree: false 24 | }, arguments[1] || { }); 25 | 26 | // cache containers 27 | if(options.containment) { 28 | options._containers = []; 29 | var containment = options.containment; 30 | if(Object.isArray(containment)) { 31 | containment.each( function(c) { options._containers.push($(c)) }); 32 | } else { 33 | options._containers.push($(containment)); 34 | } 35 | } 36 | 37 | if(options.accept) options.accept = [options.accept].flatten(); 38 | 39 | Element.makePositioned(element); // fix IE 40 | options.element = element; 41 | 42 | this.drops.push(options); 43 | }, 44 | 45 | findDeepestChild: function(drops) { 46 | deepest = drops[0]; 47 | 48 | for (i = 1; i < drops.length; ++i) 49 | if (Element.isParent(drops[i].element, deepest.element)) 50 | deepest = drops[i]; 51 | 52 | return deepest; 53 | }, 54 | 55 | isContained: function(element, drop) { 56 | var containmentNode; 57 | if(drop.tree) { 58 | containmentNode = element.treeNode; 59 | } else { 60 | containmentNode = element.parentNode; 61 | } 62 | return drop._containers.detect(function(c) { return containmentNode == c }); 63 | }, 64 | 65 | isAffected: function(point, element, drop) { 66 | return ( 67 | (drop.element!=element) && 68 | ((!drop._containers) || 69 | this.isContained(element, drop)) && 70 | ((!drop.accept) || 71 | (Element.classNames(element).detect( 72 | function(v) { return drop.accept.include(v) } ) )) && 73 | Position.within(drop.element, point[0], point[1]) ); 74 | }, 75 | 76 | deactivate: function(drop) { 77 | if(drop.hoverclass) 78 | Element.removeClassName(drop.element, drop.hoverclass); 79 | this.last_active = null; 80 | }, 81 | 82 | activate: function(drop) { 83 | if(drop.hoverclass) 84 | Element.addClassName(drop.element, drop.hoverclass); 85 | this.last_active = drop; 86 | }, 87 | 88 | show: function(point, element) { 89 | if(!this.drops.length) return; 90 | var drop, affected = []; 91 | 92 | this.drops.each( function(drop) { 93 | if(Droppables.isAffected(point, element, drop)) 94 | affected.push(drop); 95 | }); 96 | 97 | if(affected.length>0) 98 | drop = Droppables.findDeepestChild(affected); 99 | 100 | if(this.last_active && this.last_active != drop) this.deactivate(this.last_active); 101 | if (drop) { 102 | Position.within(drop.element, point[0], point[1]); 103 | if(drop.onHover) 104 | drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element)); 105 | 106 | if (drop != this.last_active) Droppables.activate(drop); 107 | } 108 | }, 109 | 110 | fire: function(event, element) { 111 | if(!this.last_active) return; 112 | Position.prepare(); 113 | 114 | if (this.isAffected([Event.pointerX(event), Event.pointerY(event)], element, this.last_active)) 115 | if (this.last_active.onDrop) { 116 | this.last_active.onDrop(element, this.last_active.element, event); 117 | return true; 118 | } 119 | }, 120 | 121 | reset: function() { 122 | if(this.last_active) 123 | this.deactivate(this.last_active); 124 | } 125 | }; 126 | 127 | var Draggables = { 128 | drags: [], 129 | observers: [], 130 | 131 | register: function(draggable) { 132 | if(this.drags.length == 0) { 133 | this.eventMouseUp = this.endDrag.bindAsEventListener(this); 134 | this.eventMouseMove = this.updateDrag.bindAsEventListener(this); 135 | this.eventKeypress = this.keyPress.bindAsEventListener(this); 136 | 137 | Event.observe(document, "mouseup", this.eventMouseUp); 138 | Event.observe(document, "mousemove", this.eventMouseMove); 139 | Event.observe(document, "keypress", this.eventKeypress); 140 | } 141 | this.drags.push(draggable); 142 | }, 143 | 144 | unregister: function(draggable) { 145 | this.drags = this.drags.reject(function(d) { return d==draggable }); 146 | if(this.drags.length == 0) { 147 | Event.stopObserving(document, "mouseup", this.eventMouseUp); 148 | Event.stopObserving(document, "mousemove", this.eventMouseMove); 149 | Event.stopObserving(document, "keypress", this.eventKeypress); 150 | } 151 | }, 152 | 153 | activate: function(draggable) { 154 | if(draggable.options.delay) { 155 | this._timeout = setTimeout(function() { 156 | Draggables._timeout = null; 157 | window.focus(); 158 | Draggables.activeDraggable = draggable; 159 | }.bind(this), draggable.options.delay); 160 | } else { 161 | window.focus(); // allows keypress events if window isn't currently focused, fails for Safari 162 | this.activeDraggable = draggable; 163 | } 164 | }, 165 | 166 | deactivate: function() { 167 | this.activeDraggable = null; 168 | }, 169 | 170 | updateDrag: function(event) { 171 | if(!this.activeDraggable) return; 172 | var pointer = [Event.pointerX(event), Event.pointerY(event)]; 173 | // Mozilla-based browsers fire successive mousemove events with 174 | // the same coordinates, prevent needless redrawing (moz bug?) 175 | if(this._lastPointer && (this._lastPointer.inspect() == pointer.inspect())) return; 176 | this._lastPointer = pointer; 177 | 178 | this.activeDraggable.updateDrag(event, pointer); 179 | }, 180 | 181 | endDrag: function(event) { 182 | if(this._timeout) { 183 | clearTimeout(this._timeout); 184 | this._timeout = null; 185 | } 186 | if(!this.activeDraggable) return; 187 | this._lastPointer = null; 188 | this.activeDraggable.endDrag(event); 189 | this.activeDraggable = null; 190 | }, 191 | 192 | keyPress: function(event) { 193 | if(this.activeDraggable) 194 | this.activeDraggable.keyPress(event); 195 | }, 196 | 197 | addObserver: function(observer) { 198 | this.observers.push(observer); 199 | this._cacheObserverCallbacks(); 200 | }, 201 | 202 | removeObserver: function(element) { // element instead of observer fixes mem leaks 203 | this.observers = this.observers.reject( function(o) { return o.element==element }); 204 | this._cacheObserverCallbacks(); 205 | }, 206 | 207 | notify: function(eventName, draggable, event) { // 'onStart', 'onEnd', 'onDrag' 208 | if(this[eventName+'Count'] > 0) 209 | this.observers.each( function(o) { 210 | if(o[eventName]) o[eventName](eventName, draggable, event); 211 | }); 212 | if(draggable.options[eventName]) draggable.options[eventName](draggable, event); 213 | }, 214 | 215 | _cacheObserverCallbacks: function() { 216 | ['onStart','onEnd','onDrag'].each( function(eventName) { 217 | Draggables[eventName+'Count'] = Draggables.observers.select( 218 | function(o) { return o[eventName]; } 219 | ).length; 220 | }); 221 | } 222 | }; 223 | 224 | /*--------------------------------------------------------------------------*/ 225 | 226 | var Draggable = Class.create({ 227 | initialize: function(element) { 228 | var defaults = { 229 | handle: false, 230 | reverteffect: function(element, top_offset, left_offset) { 231 | var dur = Math.sqrt(Math.abs(top_offset^2)+Math.abs(left_offset^2))*0.02; 232 | new Effect.Move(element, { x: -left_offset, y: -top_offset, duration: dur, 233 | queue: {scope:'_draggable', position:'end'} 234 | }); 235 | }, 236 | endeffect: function(element) { 237 | var toOpacity = Object.isNumber(element._opacity) ? element._opacity : 1.0; 238 | new Effect.Opacity(element, {duration:0.2, from:0.7, to:toOpacity, 239 | queue: {scope:'_draggable', position:'end'}, 240 | afterFinish: function(){ 241 | Draggable._dragging[element] = false 242 | } 243 | }); 244 | }, 245 | zindex: 1000, 246 | revert: false, 247 | quiet: false, 248 | scroll: false, 249 | scrollSensitivity: 20, 250 | scrollSpeed: 15, 251 | snap: false, // false, or xy or [x,y] or function(x,y){ return [x,y] } 252 | delay: 0 253 | }; 254 | 255 | if(!arguments[1] || Object.isUndefined(arguments[1].endeffect)) 256 | Object.extend(defaults, { 257 | starteffect: function(element) { 258 | element._opacity = Element.getOpacity(element); 259 | Draggable._dragging[element] = true; 260 | new Effect.Opacity(element, {duration:0.2, from:element._opacity, to:0.7}); 261 | } 262 | }); 263 | 264 | var options = Object.extend(defaults, arguments[1] || { }); 265 | 266 | this.element = $(element); 267 | 268 | if(options.handle && Object.isString(options.handle)) 269 | this.handle = this.element.down('.'+options.handle, 0); 270 | 271 | if(!this.handle) this.handle = $(options.handle); 272 | if(!this.handle) this.handle = this.element; 273 | 274 | if(options.scroll && !options.scroll.scrollTo && !options.scroll.outerHTML) { 275 | options.scroll = $(options.scroll); 276 | this._isScrollChild = Element.childOf(this.element, options.scroll); 277 | } 278 | 279 | Element.makePositioned(this.element); // fix IE 280 | 281 | this.options = options; 282 | this.dragging = false; 283 | 284 | this.eventMouseDown = this.initDrag.bindAsEventListener(this); 285 | Event.observe(this.handle, "mousedown", this.eventMouseDown); 286 | 287 | Draggables.register(this); 288 | }, 289 | 290 | destroy: function() { 291 | Event.stopObserving(this.handle, "mousedown", this.eventMouseDown); 292 | Draggables.unregister(this); 293 | }, 294 | 295 | currentDelta: function() { 296 | return([ 297 | parseInt(Element.getStyle(this.element,'left') || '0'), 298 | parseInt(Element.getStyle(this.element,'top') || '0')]); 299 | }, 300 | 301 | initDrag: function(event) { 302 | if(!Object.isUndefined(Draggable._dragging[this.element]) && 303 | Draggable._dragging[this.element]) return; 304 | if(Event.isLeftClick(event)) { 305 | // abort on form elements, fixes a Firefox issue 306 | var src = Event.element(event); 307 | if((tag_name = src.tagName.toUpperCase()) && ( 308 | tag_name=='INPUT' || 309 | tag_name=='SELECT' || 310 | tag_name=='OPTION' || 311 | tag_name=='BUTTON' || 312 | tag_name=='TEXTAREA')) return; 313 | 314 | var pointer = [Event.pointerX(event), Event.pointerY(event)]; 315 | var pos = this.element.cumulativeOffset(); 316 | this.offset = [0,1].map( function(i) { return (pointer[i] - pos[i]) }); 317 | 318 | Draggables.activate(this); 319 | Event.stop(event); 320 | } 321 | }, 322 | 323 | startDrag: function(event) { 324 | this.dragging = true; 325 | if(!this.delta) 326 | this.delta = this.currentDelta(); 327 | 328 | if(this.options.zindex) { 329 | this.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0); 330 | this.element.style.zIndex = this.options.zindex; 331 | } 332 | 333 | if(this.options.ghosting) { 334 | this._clone = this.element.cloneNode(true); 335 | this._originallyAbsolute = (this.element.getStyle('position') == 'absolute'); 336 | if (!this._originallyAbsolute) 337 | Position.absolutize(this.element); 338 | this.element.parentNode.insertBefore(this._clone, this.element); 339 | } 340 | 341 | if(this.options.scroll) { 342 | if (this.options.scroll == window) { 343 | var where = this._getWindowScroll(this.options.scroll); 344 | this.originalScrollLeft = where.left; 345 | this.originalScrollTop = where.top; 346 | } else { 347 | this.originalScrollLeft = this.options.scroll.scrollLeft; 348 | this.originalScrollTop = this.options.scroll.scrollTop; 349 | } 350 | } 351 | 352 | Draggables.notify('onStart', this, event); 353 | 354 | if(this.options.starteffect) this.options.starteffect(this.element); 355 | }, 356 | 357 | updateDrag: function(event, pointer) { 358 | if(!this.dragging) this.startDrag(event); 359 | 360 | if(!this.options.quiet){ 361 | Position.prepare(); 362 | Droppables.show(pointer, this.element); 363 | } 364 | 365 | Draggables.notify('onDrag', this, event); 366 | 367 | this.draw(pointer); 368 | if(this.options.change) this.options.change(this); 369 | 370 | if(this.options.scroll) { 371 | this.stopScrolling(); 372 | 373 | var p; 374 | if (this.options.scroll == window) { 375 | with(this._getWindowScroll(this.options.scroll)) { p = [ left, top, left+width, top+height ]; } 376 | } else { 377 | p = Position.page(this.options.scroll); 378 | p[0] += this.options.scroll.scrollLeft + Position.deltaX; 379 | p[1] += this.options.scroll.scrollTop + Position.deltaY; 380 | p.push(p[0]+this.options.scroll.offsetWidth); 381 | p.push(p[1]+this.options.scroll.offsetHeight); 382 | } 383 | var speed = [0,0]; 384 | if(pointer[0] < (p[0]+this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[0]+this.options.scrollSensitivity); 385 | if(pointer[1] < (p[1]+this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[1]+this.options.scrollSensitivity); 386 | if(pointer[0] > (p[2]-this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[2]-this.options.scrollSensitivity); 387 | if(pointer[1] > (p[3]-this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[3]-this.options.scrollSensitivity); 388 | this.startScrolling(speed); 389 | } 390 | 391 | // fix AppleWebKit rendering 392 | if(Prototype.Browser.WebKit) window.scrollBy(0,0); 393 | 394 | Event.stop(event); 395 | }, 396 | 397 | finishDrag: function(event, success) { 398 | this.dragging = false; 399 | 400 | if(this.options.quiet){ 401 | Position.prepare(); 402 | var pointer = [Event.pointerX(event), Event.pointerY(event)]; 403 | Droppables.show(pointer, this.element); 404 | } 405 | 406 | if(this.options.ghosting) { 407 | if (!this._originallyAbsolute) 408 | Position.relativize(this.element); 409 | delete this._originallyAbsolute; 410 | Element.remove(this._clone); 411 | this._clone = null; 412 | } 413 | 414 | var dropped = false; 415 | if(success) { 416 | dropped = Droppables.fire(event, this.element); 417 | if (!dropped) dropped = false; 418 | } 419 | if(dropped && this.options.onDropped) this.options.onDropped(this.element); 420 | Draggables.notify('onEnd', this, event); 421 | 422 | var revert = this.options.revert; 423 | if(revert && Object.isFunction(revert)) revert = revert(this.element); 424 | 425 | var d = this.currentDelta(); 426 | if(revert && this.options.reverteffect) { 427 | if (dropped == 0 || revert != 'failure') 428 | this.options.reverteffect(this.element, 429 | d[1]-this.delta[1], d[0]-this.delta[0]); 430 | } else { 431 | this.delta = d; 432 | } 433 | 434 | if(this.options.zindex) 435 | this.element.style.zIndex = this.originalZ; 436 | 437 | if(this.options.endeffect) 438 | this.options.endeffect(this.element); 439 | 440 | Draggables.deactivate(this); 441 | Droppables.reset(); 442 | }, 443 | 444 | keyPress: function(event) { 445 | if(event.keyCode!=Event.KEY_ESC) return; 446 | this.finishDrag(event, false); 447 | Event.stop(event); 448 | }, 449 | 450 | endDrag: function(event) { 451 | if(!this.dragging) return; 452 | this.stopScrolling(); 453 | this.finishDrag(event, true); 454 | Event.stop(event); 455 | }, 456 | 457 | draw: function(point) { 458 | var pos = this.element.cumulativeOffset(); 459 | if(this.options.ghosting) { 460 | var r = Position.realOffset(this.element); 461 | pos[0] += r[0] - Position.deltaX; pos[1] += r[1] - Position.deltaY; 462 | } 463 | 464 | var d = this.currentDelta(); 465 | pos[0] -= d[0]; pos[1] -= d[1]; 466 | 467 | if(this.options.scroll && (this.options.scroll != window && this._isScrollChild)) { 468 | pos[0] -= this.options.scroll.scrollLeft-this.originalScrollLeft; 469 | pos[1] -= this.options.scroll.scrollTop-this.originalScrollTop; 470 | } 471 | 472 | var p = [0,1].map(function(i){ 473 | return (point[i]-pos[i]-this.offset[i]) 474 | }.bind(this)); 475 | 476 | if(this.options.snap) { 477 | if(Object.isFunction(this.options.snap)) { 478 | p = this.options.snap(p[0],p[1],this); 479 | } else { 480 | if(Object.isArray(this.options.snap)) { 481 | p = p.map( function(v, i) { 482 | return (v/this.options.snap[i]).round()*this.options.snap[i] }.bind(this)); 483 | } else { 484 | p = p.map( function(v) { 485 | return (v/this.options.snap).round()*this.options.snap }.bind(this)); 486 | } 487 | }} 488 | 489 | var style = this.element.style; 490 | if((!this.options.constraint) || (this.options.constraint=='horizontal')) 491 | style.left = p[0] + "px"; 492 | if((!this.options.constraint) || (this.options.constraint=='vertical')) 493 | style.top = p[1] + "px"; 494 | 495 | if(style.visibility=="hidden") style.visibility = ""; // fix gecko rendering 496 | }, 497 | 498 | stopScrolling: function() { 499 | if(this.scrollInterval) { 500 | clearInterval(this.scrollInterval); 501 | this.scrollInterval = null; 502 | Draggables._lastScrollPointer = null; 503 | } 504 | }, 505 | 506 | startScrolling: function(speed) { 507 | if(!(speed[0] || speed[1])) return; 508 | this.scrollSpeed = [speed[0]*this.options.scrollSpeed,speed[1]*this.options.scrollSpeed]; 509 | this.lastScrolled = new Date(); 510 | this.scrollInterval = setInterval(this.scroll.bind(this), 10); 511 | }, 512 | 513 | scroll: function() { 514 | var current = new Date(); 515 | var delta = current - this.lastScrolled; 516 | this.lastScrolled = current; 517 | if(this.options.scroll == window) { 518 | with (this._getWindowScroll(this.options.scroll)) { 519 | if (this.scrollSpeed[0] || this.scrollSpeed[1]) { 520 | var d = delta / 1000; 521 | this.options.scroll.scrollTo( left + d*this.scrollSpeed[0], top + d*this.scrollSpeed[1] ); 522 | } 523 | } 524 | } else { 525 | this.options.scroll.scrollLeft += this.scrollSpeed[0] * delta / 1000; 526 | this.options.scroll.scrollTop += this.scrollSpeed[1] * delta / 1000; 527 | } 528 | 529 | Position.prepare(); 530 | Droppables.show(Draggables._lastPointer, this.element); 531 | Draggables.notify('onDrag', this); 532 | if (this._isScrollChild) { 533 | Draggables._lastScrollPointer = Draggables._lastScrollPointer || $A(Draggables._lastPointer); 534 | Draggables._lastScrollPointer[0] += this.scrollSpeed[0] * delta / 1000; 535 | Draggables._lastScrollPointer[1] += this.scrollSpeed[1] * delta / 1000; 536 | if (Draggables._lastScrollPointer[0] < 0) 537 | Draggables._lastScrollPointer[0] = 0; 538 | if (Draggables._lastScrollPointer[1] < 0) 539 | Draggables._lastScrollPointer[1] = 0; 540 | this.draw(Draggables._lastScrollPointer); 541 | } 542 | 543 | if(this.options.change) this.options.change(this); 544 | }, 545 | 546 | _getWindowScroll: function(w) { 547 | var T, L, W, H; 548 | with (w.document) { 549 | if (w.document.documentElement && documentElement.scrollTop) { 550 | T = documentElement.scrollTop; 551 | L = documentElement.scrollLeft; 552 | } else if (w.document.body) { 553 | T = body.scrollTop; 554 | L = body.scrollLeft; 555 | } 556 | if (w.innerWidth) { 557 | W = w.innerWidth; 558 | H = w.innerHeight; 559 | } else if (w.document.documentElement && documentElement.clientWidth) { 560 | W = documentElement.clientWidth; 561 | H = documentElement.clientHeight; 562 | } else { 563 | W = body.offsetWidth; 564 | H = body.offsetHeight; 565 | } 566 | } 567 | return { top: T, left: L, width: W, height: H }; 568 | } 569 | }); 570 | 571 | Draggable._dragging = { }; 572 | 573 | /*--------------------------------------------------------------------------*/ 574 | 575 | var SortableObserver = Class.create({ 576 | initialize: function(element, observer) { 577 | this.element = $(element); 578 | this.observer = observer; 579 | this.lastValue = Sortable.serialize(this.element); 580 | }, 581 | 582 | onStart: function() { 583 | this.lastValue = Sortable.serialize(this.element); 584 | }, 585 | 586 | onEnd: function() { 587 | Sortable.unmark(); 588 | if(this.lastValue != Sortable.serialize(this.element)) 589 | this.observer(this.element) 590 | } 591 | }); 592 | 593 | var Sortable = { 594 | SERIALIZE_RULE: /^[^_\-](?:[A-Za-z0-9\-\_]*)[_](.*)$/, 595 | 596 | sortables: { }, 597 | 598 | _findRootElement: function(element) { 599 | while (element.tagName.toUpperCase() != "BODY") { 600 | if(element.id && Sortable.sortables[element.id]) return element; 601 | element = element.parentNode; 602 | } 603 | }, 604 | 605 | options: function(element) { 606 | element = Sortable._findRootElement($(element)); 607 | if(!element) return; 608 | return Sortable.sortables[element.id]; 609 | }, 610 | 611 | destroy: function(element){ 612 | element = $(element); 613 | var s = Sortable.sortables[element.id]; 614 | 615 | if(s) { 616 | Draggables.removeObserver(s.element); 617 | s.droppables.each(function(d){ Droppables.remove(d) }); 618 | s.draggables.invoke('destroy'); 619 | 620 | delete Sortable.sortables[s.element.id]; 621 | } 622 | }, 623 | 624 | create: function(element) { 625 | element = $(element); 626 | var options = Object.extend({ 627 | element: element, 628 | tag: 'li', // assumes li children, override with tag: 'tagname' 629 | dropOnEmpty: false, 630 | tree: false, 631 | treeTag: 'ul', 632 | overlap: 'vertical', // one of 'vertical', 'horizontal' 633 | constraint: 'vertical', // one of 'vertical', 'horizontal', false 634 | containment: element, // also takes array of elements (or id's); or false 635 | handle: false, // or a CSS class 636 | only: false, 637 | delay: 0, 638 | hoverclass: null, 639 | ghosting: false, 640 | quiet: false, 641 | scroll: false, 642 | scrollSensitivity: 20, 643 | scrollSpeed: 15, 644 | format: this.SERIALIZE_RULE, 645 | 646 | // these take arrays of elements or ids and can be 647 | // used for better initialization performance 648 | elements: false, 649 | handles: false, 650 | 651 | onChange: Prototype.emptyFunction, 652 | onUpdate: Prototype.emptyFunction 653 | }, arguments[1] || { }); 654 | 655 | // clear any old sortable with same element 656 | this.destroy(element); 657 | 658 | // build options for the draggables 659 | var options_for_draggable = { 660 | revert: true, 661 | quiet: options.quiet, 662 | scroll: options.scroll, 663 | scrollSpeed: options.scrollSpeed, 664 | scrollSensitivity: options.scrollSensitivity, 665 | delay: options.delay, 666 | ghosting: options.ghosting, 667 | constraint: options.constraint, 668 | handle: options.handle }; 669 | 670 | if(options.starteffect) 671 | options_for_draggable.starteffect = options.starteffect; 672 | 673 | if(options.reverteffect) 674 | options_for_draggable.reverteffect = options.reverteffect; 675 | else 676 | if(options.ghosting) options_for_draggable.reverteffect = function(element) { 677 | element.style.top = 0; 678 | element.style.left = 0; 679 | }; 680 | 681 | if(options.endeffect) 682 | options_for_draggable.endeffect = options.endeffect; 683 | 684 | if(options.zindex) 685 | options_for_draggable.zindex = options.zindex; 686 | 687 | // build options for the droppables 688 | var options_for_droppable = { 689 | overlap: options.overlap, 690 | containment: options.containment, 691 | tree: options.tree, 692 | hoverclass: options.hoverclass, 693 | onHover: Sortable.onHover 694 | }; 695 | 696 | var options_for_tree = { 697 | onHover: Sortable.onEmptyHover, 698 | overlap: options.overlap, 699 | containment: options.containment, 700 | hoverclass: options.hoverclass 701 | }; 702 | 703 | // fix for gecko engine 704 | Element.cleanWhitespace(element); 705 | 706 | options.draggables = []; 707 | options.droppables = []; 708 | 709 | // drop on empty handling 710 | if(options.dropOnEmpty || options.tree) { 711 | Droppables.add(element, options_for_tree); 712 | options.droppables.push(element); 713 | } 714 | 715 | (options.elements || this.findElements(element, options) || []).each( function(e,i) { 716 | var handle = options.handles ? $(options.handles[i]) : 717 | (options.handle ? $(e).select('.' + options.handle)[0] : e); 718 | options.draggables.push( 719 | new Draggable(e, Object.extend(options_for_draggable, { handle: handle }))); 720 | Droppables.add(e, options_for_droppable); 721 | if(options.tree) e.treeNode = element; 722 | options.droppables.push(e); 723 | }); 724 | 725 | if(options.tree) { 726 | (Sortable.findTreeElements(element, options) || []).each( function(e) { 727 | Droppables.add(e, options_for_tree); 728 | e.treeNode = element; 729 | options.droppables.push(e); 730 | }); 731 | } 732 | 733 | // keep reference 734 | this.sortables[element.identify()] = options; 735 | 736 | // for onupdate 737 | Draggables.addObserver(new SortableObserver(element, options.onUpdate)); 738 | 739 | }, 740 | 741 | // return all suitable-for-sortable elements in a guaranteed order 742 | findElements: function(element, options) { 743 | return Element.findChildren( 744 | element, options.only, options.tree ? true : false, options.tag); 745 | }, 746 | 747 | findTreeElements: function(element, options) { 748 | return Element.findChildren( 749 | element, options.only, options.tree ? true : false, options.treeTag); 750 | }, 751 | 752 | onHover: function(element, dropon, overlap) { 753 | if(Element.isParent(dropon, element)) return; 754 | 755 | if(overlap > .33 && overlap < .66 && Sortable.options(dropon).tree) { 756 | return; 757 | } else if(overlap>0.5) { 758 | Sortable.mark(dropon, 'before'); 759 | if(dropon.previousSibling != element) { 760 | var oldParentNode = element.parentNode; 761 | element.style.visibility = "hidden"; // fix gecko rendering 762 | dropon.parentNode.insertBefore(element, dropon); 763 | if(dropon.parentNode!=oldParentNode) 764 | Sortable.options(oldParentNode).onChange(element); 765 | Sortable.options(dropon.parentNode).onChange(element); 766 | } 767 | } else { 768 | Sortable.mark(dropon, 'after'); 769 | var nextElement = dropon.nextSibling || null; 770 | if(nextElement != element) { 771 | var oldParentNode = element.parentNode; 772 | element.style.visibility = "hidden"; // fix gecko rendering 773 | dropon.parentNode.insertBefore(element, nextElement); 774 | if(dropon.parentNode!=oldParentNode) 775 | Sortable.options(oldParentNode).onChange(element); 776 | Sortable.options(dropon.parentNode).onChange(element); 777 | } 778 | } 779 | }, 780 | 781 | onEmptyHover: function(element, dropon, overlap) { 782 | var oldParentNode = element.parentNode; 783 | var droponOptions = Sortable.options(dropon); 784 | 785 | if(!Element.isParent(dropon, element)) { 786 | var index; 787 | 788 | var children = Sortable.findElements(dropon, {tag: droponOptions.tag, only: droponOptions.only}); 789 | var child = null; 790 | 791 | if(children) { 792 | var offset = Element.offsetSize(dropon, droponOptions.overlap) * (1.0 - overlap); 793 | 794 | for (index = 0; index < children.length; index += 1) { 795 | if (offset - Element.offsetSize (children[index], droponOptions.overlap) >= 0) { 796 | offset -= Element.offsetSize (children[index], droponOptions.overlap); 797 | } else if (offset - (Element.offsetSize (children[index], droponOptions.overlap) / 2) >= 0) { 798 | child = index + 1 < children.length ? children[index + 1] : null; 799 | break; 800 | } else { 801 | child = children[index]; 802 | break; 803 | } 804 | } 805 | } 806 | 807 | dropon.insertBefore(element, child); 808 | 809 | Sortable.options(oldParentNode).onChange(element); 810 | droponOptions.onChange(element); 811 | } 812 | }, 813 | 814 | unmark: function() { 815 | if(Sortable._marker) Sortable._marker.hide(); 816 | }, 817 | 818 | mark: function(dropon, position) { 819 | // mark on ghosting only 820 | var sortable = Sortable.options(dropon.parentNode); 821 | if(sortable && !sortable.ghosting) return; 822 | 823 | if(!Sortable._marker) { 824 | Sortable._marker = 825 | ($('dropmarker') || Element.extend(document.createElement('DIV'))). 826 | hide().addClassName('dropmarker').setStyle({position:'absolute'}); 827 | document.getElementsByTagName("body").item(0).appendChild(Sortable._marker); 828 | } 829 | var offsets = dropon.cumulativeOffset(); 830 | Sortable._marker.setStyle({left: offsets[0]+'px', top: offsets[1] + 'px'}); 831 | 832 | if(position=='after') 833 | if(sortable.overlap == 'horizontal') 834 | Sortable._marker.setStyle({left: (offsets[0]+dropon.clientWidth) + 'px'}); 835 | else 836 | Sortable._marker.setStyle({top: (offsets[1]+dropon.clientHeight) + 'px'}); 837 | 838 | Sortable._marker.show(); 839 | }, 840 | 841 | _tree: function(element, options, parent) { 842 | var children = Sortable.findElements(element, options) || []; 843 | 844 | for (var i = 0; i < children.length; ++i) { 845 | var match = children[i].id.match(options.format); 846 | 847 | if (!match) continue; 848 | 849 | var child = { 850 | id: encodeURIComponent(match ? match[1] : null), 851 | element: element, 852 | parent: parent, 853 | children: [], 854 | position: parent.children.length, 855 | container: $(children[i]).down(options.treeTag) 856 | }; 857 | 858 | /* Get the element containing the children and recurse over it */ 859 | if (child.container) 860 | this._tree(child.container, options, child); 861 | 862 | parent.children.push (child); 863 | } 864 | 865 | return parent; 866 | }, 867 | 868 | tree: function(element) { 869 | element = $(element); 870 | var sortableOptions = this.options(element); 871 | var options = Object.extend({ 872 | tag: sortableOptions.tag, 873 | treeTag: sortableOptions.treeTag, 874 | only: sortableOptions.only, 875 | name: element.id, 876 | format: sortableOptions.format 877 | }, arguments[1] || { }); 878 | 879 | var root = { 880 | id: null, 881 | parent: null, 882 | children: [], 883 | container: element, 884 | position: 0 885 | }; 886 | 887 | return Sortable._tree(element, options, root); 888 | }, 889 | 890 | /* Construct a [i] index for a particular node */ 891 | _constructIndex: function(node) { 892 | var index = ''; 893 | do { 894 | if (node.id) index = '[' + node.position + ']' + index; 895 | } while ((node = node.parent) != null); 896 | return index; 897 | }, 898 | 899 | sequence: function(element) { 900 | element = $(element); 901 | var options = Object.extend(this.options(element), arguments[1] || { }); 902 | 903 | return $(this.findElements(element, options) || []).map( function(item) { 904 | return item.id.match(options.format) ? item.id.match(options.format)[1] : ''; 905 | }); 906 | }, 907 | 908 | setSequence: function(element, new_sequence) { 909 | element = $(element); 910 | var options = Object.extend(this.options(element), arguments[2] || { }); 911 | 912 | var nodeMap = { }; 913 | this.findElements(element, options).each( function(n) { 914 | if (n.id.match(options.format)) 915 | nodeMap[n.id.match(options.format)[1]] = [n, n.parentNode]; 916 | n.parentNode.removeChild(n); 917 | }); 918 | 919 | new_sequence.each(function(ident) { 920 | var n = nodeMap[ident]; 921 | if (n) { 922 | n[1].appendChild(n[0]); 923 | delete nodeMap[ident]; 924 | } 925 | }); 926 | }, 927 | 928 | serialize: function(element) { 929 | element = $(element); 930 | var options = Object.extend(Sortable.options(element), arguments[1] || { }); 931 | var name = encodeURIComponent( 932 | (arguments[1] && arguments[1].name) ? arguments[1].name : element.id); 933 | 934 | if (options.tree) { 935 | return Sortable.tree(element, arguments[1]).children.map( function (item) { 936 | return [name + Sortable._constructIndex(item) + "[id]=" + 937 | encodeURIComponent(item.id)].concat(item.children.map(arguments.callee)); 938 | }).flatten().join('&'); 939 | } else { 940 | return Sortable.sequence(element, arguments[1]).map( function(item) { 941 | return name + "[]=" + encodeURIComponent(item); 942 | }).join('&'); 943 | } 944 | } 945 | }; 946 | 947 | // Returns true if child is contained within element 948 | Element.isParent = function(child, element) { 949 | if (!child.parentNode || child == element) return false; 950 | if (child.parentNode == element) return true; 951 | return Element.isParent(child.parentNode, element); 952 | }; 953 | 954 | Element.findChildren = function(element, only, recursive, tagName) { 955 | if(!element.hasChildNodes()) return null; 956 | tagName = tagName.toUpperCase(); 957 | if(only) only = [only].flatten(); 958 | var elements = []; 959 | $A(element.childNodes).each( function(e) { 960 | if(e.tagName && e.tagName.toUpperCase()==tagName && 961 | (!only || (Element.classNames(e).detect(function(v) { return only.include(v) })))) 962 | elements.push(e); 963 | if(recursive) { 964 | var grandchildren = Element.findChildren(e, only, recursive, tagName); 965 | if(grandchildren) elements.push(grandchildren); 966 | } 967 | }); 968 | 969 | return (elements.length>0 ? elements.flatten() : []); 970 | }; 971 | 972 | Element.offsetSize = function (element, type) { 973 | return element['offset' + ((type=='vertical' || type=='height') ? 'Height' : 'Width')]; 974 | }; -------------------------------------------------------------------------------- /test/dummy/public/javascripts/rails.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | Ajax.Responders.register({ 3 | onCreate: function(request) { 4 | var token = $$('meta[name=csrf-token]')[0]; 5 | if (token) { 6 | if (!request.options.requestHeaders) request.options.requestHeaders = {}; 7 | request.options.requestHeaders['X-CSRF-Token'] = token.readAttribute('content'); 8 | } 9 | } 10 | }); 11 | 12 | // Technique from Juriy Zaytsev 13 | // http://thinkweb2.com/projects/prototype/detecting-event-support-without-browser-sniffing/ 14 | function isEventSupported(eventName) { 15 | var el = document.createElement('div'); 16 | eventName = 'on' + eventName; 17 | var isSupported = (eventName in el); 18 | if (!isSupported) { 19 | el.setAttribute(eventName, 'return;'); 20 | isSupported = typeof el[eventName] == 'function'; 21 | } 22 | el = null; 23 | return isSupported; 24 | } 25 | 26 | function isForm(element) { 27 | return Object.isElement(element) && element.nodeName.toUpperCase() == 'FORM'; 28 | } 29 | 30 | function isInput(element) { 31 | if (Object.isElement(element)) { 32 | var name = element.nodeName.toUpperCase(); 33 | return name == 'INPUT' || name == 'SELECT' || name == 'TEXTAREA'; 34 | } 35 | else return false; 36 | } 37 | 38 | var submitBubbles = isEventSupported('submit'), 39 | changeBubbles = isEventSupported('change'); 40 | 41 | if (!submitBubbles || !changeBubbles) { 42 | // augment the Event.Handler class to observe custom events when needed 43 | Event.Handler.prototype.initialize = Event.Handler.prototype.initialize.wrap( 44 | function(init, element, eventName, selector, callback) { 45 | init(element, eventName, selector, callback); 46 | // is the handler being attached to an element that doesn't support this event? 47 | if ( (!submitBubbles && this.eventName == 'submit' && !isForm(this.element)) || 48 | (!changeBubbles && this.eventName == 'change' && !isInput(this.element)) ) { 49 | // "submit" => "emulated:submit" 50 | this.eventName = 'emulated:' + this.eventName; 51 | } 52 | } 53 | ); 54 | } 55 | 56 | if (!submitBubbles) { 57 | // discover forms on the page by observing focus events which always bubble 58 | document.on('focusin', 'form', function(focusEvent, form) { 59 | // special handler for the real "submit" event (one-time operation) 60 | if (!form.retrieve('emulated:submit')) { 61 | form.on('submit', function(submitEvent) { 62 | var emulated = form.fire('emulated:submit', submitEvent, true); 63 | // if custom event received preventDefault, cancel the real one too 64 | if (emulated.returnValue === false) submitEvent.preventDefault(); 65 | }); 66 | form.store('emulated:submit', true); 67 | } 68 | }); 69 | } 70 | 71 | if (!changeBubbles) { 72 | // discover form inputs on the page 73 | document.on('focusin', 'input, select, textarea', function(focusEvent, input) { 74 | // special handler for real "change" events 75 | if (!input.retrieve('emulated:change')) { 76 | input.on('change', function(changeEvent) { 77 | input.fire('emulated:change', changeEvent, true); 78 | }); 79 | input.store('emulated:change', true); 80 | } 81 | }); 82 | } 83 | 84 | function handleRemote(element) { 85 | var method, url, params; 86 | 87 | var event = element.fire("ajax:before"); 88 | if (event.stopped) return false; 89 | 90 | if (element.tagName.toLowerCase() === 'form') { 91 | method = element.readAttribute('method') || 'post'; 92 | url = element.readAttribute('action'); 93 | // serialize the form with respect to the submit button that was pressed 94 | params = element.serialize({ submit: element.retrieve('rails:submit-button') }); 95 | // clear the pressed submit button information 96 | element.store('rails:submit-button', null); 97 | } else { 98 | method = element.readAttribute('data-method') || 'get'; 99 | url = element.readAttribute('href'); 100 | params = {}; 101 | } 102 | 103 | new Ajax.Request(url, { 104 | method: method, 105 | parameters: params, 106 | evalScripts: true, 107 | 108 | onCreate: function(response) { element.fire("ajax:create", response); }, 109 | onComplete: function(response) { element.fire("ajax:complete", response); }, 110 | onSuccess: function(response) { element.fire("ajax:success", response); }, 111 | onFailure: function(response) { element.fire("ajax:failure", response); } 112 | }); 113 | 114 | element.fire("ajax:after"); 115 | } 116 | 117 | function insertHiddenField(form, name, value) { 118 | form.insert(new Element('input', { type: 'hidden', name: name, value: value })); 119 | } 120 | 121 | function handleMethod(element) { 122 | var method = element.readAttribute('data-method'), 123 | url = element.readAttribute('href'), 124 | csrf_param = $$('meta[name=csrf-param]')[0], 125 | csrf_token = $$('meta[name=csrf-token]')[0]; 126 | 127 | var form = new Element('form', { method: "POST", action: url, style: "display: none;" }); 128 | $(element.parentNode).insert(form); 129 | 130 | if (method !== 'post') { 131 | insertHiddenField(form, '_method', method); 132 | } 133 | 134 | if (csrf_param) { 135 | insertHiddenField(form, csrf_param.readAttribute('content'), csrf_token.readAttribute('content')); 136 | } 137 | 138 | form.submit(); 139 | } 140 | 141 | function disableFormElements(form) { 142 | form.select('input[type=submit][data-disable-with]').each(function(input) { 143 | input.store('rails:original-value', input.getValue()); 144 | input.setValue(input.readAttribute('data-disable-with')).disable(); 145 | }); 146 | } 147 | 148 | function enableFormElements(form) { 149 | form.select('input[type=submit][data-disable-with]').each(function(input) { 150 | input.setValue(input.retrieve('rails:original-value')).enable(); 151 | }); 152 | } 153 | 154 | function allowAction(element) { 155 | var message = element.readAttribute('data-confirm'); 156 | return !message || confirm(message); 157 | } 158 | 159 | document.on('click', 'a[data-confirm], a[data-remote], a[data-method]', function(event, link) { 160 | if (!allowAction(link)) { 161 | event.stop(); 162 | return false; 163 | } 164 | 165 | if (link.readAttribute('data-remote')) { 166 | handleRemote(link); 167 | event.stop(); 168 | } else if (link.readAttribute('data-method')) { 169 | handleMethod(link); 170 | event.stop(); 171 | } 172 | }); 173 | 174 | document.on("click", "form input[type=submit], form button[type=submit], form button:not([type])", function(event, button) { 175 | // register the pressed submit button 176 | event.findElement('form').store('rails:submit-button', button.name || false); 177 | }); 178 | 179 | document.on("submit", function(event) { 180 | var form = event.findElement(); 181 | 182 | if (!allowAction(form)) { 183 | event.stop(); 184 | return false; 185 | } 186 | 187 | if (form.readAttribute('data-remote')) { 188 | handleRemote(form); 189 | event.stop(); 190 | } else { 191 | disableFormElements(form); 192 | } 193 | }); 194 | 195 | document.on('ajax:create', 'form', function(event, form) { 196 | if (form == event.findElement()) disableFormElements(form); 197 | }); 198 | 199 | document.on('ajax:complete', 'form', function(event, form) { 200 | if (form == event.findElement()) enableFormElements(form); 201 | }); 202 | })(); 203 | -------------------------------------------------------------------------------- /test/dummy/public/stylesheets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clearfunction/questionnaire/61be93261334b89ea812ad38bd75d1c361295e88/test/dummy/public/stylesheets/.gitkeep -------------------------------------------------------------------------------- /test/dummy/script/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application. 5 | 6 | APP_PATH = File.expand_path('../../config/application', __FILE__) 7 | require File.expand_path('../../config/boot', __FILE__) 8 | require 'rails/commands' 9 | -------------------------------------------------------------------------------- /test/models/answer_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | class AnswerTest < ActiveSupport::TestCase 6 | test 'should create a valid answer' do 7 | answer = create_answer 8 | should_be_persisted answer 9 | end 10 | 11 | test 'should not create an answer with a nil option' do 12 | answer = create_answer(option: nil) 13 | should_not_be_persisted answer 14 | end 15 | 16 | test 'should not create an answer with a nil question' do 17 | answer = create_answer(question: nil) 18 | should_not_be_persisted answer 19 | end 20 | 21 | test 'should create an answer with a option_number field for options with number type' do 22 | survey, option, attempt, question = create_answer_with_option_type(Survey::OptionsType.number) 23 | answer_try_1 = create_answer(option: option, attempt: attempt, question: question, option_number: 12) 24 | 25 | should_be_persisted survey 26 | should_be_persisted answer_try_1 27 | end 28 | 29 | test 'should not create an answer with a nil option_number field for options with number type' do 30 | survey, option, attempt, question = create_answer_with_option_type(Survey::OptionsType.number, true) 31 | answer_try_1 = create_answer(option: option, attempt: attempt, question: question, option_number: nil) 32 | 33 | should_be_persisted survey 34 | should_not_be_persisted answer_try_1 35 | end 36 | 37 | test 'should create an answer with a option_text field for options with text type' do 38 | survey, option, attempt, question = create_answer_with_option_type(Survey::OptionsType.text, true) 39 | answer_try_1 = create_answer(option: option, attempt: attempt, question: question, option_text: Faker::Name.name) 40 | 41 | should_be_persisted survey 42 | should_be_persisted answer_try_1 43 | end 44 | 45 | test 'should not create an answer with a nil option_text field for options with text type' do 46 | survey, option, attempt, question = create_answer_with_option_type(Survey::OptionsType.text, true) 47 | answer_try_1 = create_answer(option: option, attempt: attempt, question: question, option_text: nil) 48 | 49 | should_be_persisted survey 50 | should_not_be_persisted answer_try_1 51 | end 52 | 53 | test 'should create an answer with a option_text field for options with large_text type' do 54 | survey, option, attempt, question = create_answer_with_option_type(Survey::OptionsType.large_text, true) 55 | answer_try_1 = create_answer(option: option, attempt: attempt, question: question, option_text: Faker::Name.name) 56 | 57 | should_be_persisted survey 58 | should_be_persisted answer_try_1 59 | end 60 | 61 | test 'should not create an answer with a nil option_text field for options with large_text type' do 62 | survey, option, attempt, question = create_answer_with_option_type(Survey::OptionsType.large_text, true) 63 | answer_try_1 = create_answer(option: option, attempt: attempt, question: question, option_text: nil) 64 | 65 | should_be_persisted survey 66 | should_not_be_persisted answer_try_1 67 | end 68 | 69 | test 'should create an answer with a option_text field for options with multi_choices_with_text type' do 70 | survey, option, attempt, question = create_answer_with_option_type(Survey::OptionsType.multi_choices_with_text, true) 71 | faker_name = Faker::Name.name 72 | answer_try_1 = create_answer(option: option, attempt: attempt, question: question, option_text: faker_name) 73 | 74 | should_be_persisted survey 75 | should_be_persisted answer_try_1 76 | assert_equal answer_try_1.option_text, faker_name 77 | end 78 | 79 | test 'should not create an answer with empty option_text field for options with multi_choices_with_text type' do 80 | survey, option, attempt, question = create_answer_with_option_type(Survey::OptionsType.multi_choices_with_text, true) 81 | answer_try_1 = create_answer(option: option, attempt: attempt, question: question, option_text: nil) 82 | 83 | should_be_persisted survey 84 | should_not_be_persisted answer_try_1 85 | end 86 | 87 | test 'should not create an answer with empty option_text field for options with single_choice_with_text type' do 88 | survey, option, attempt, question = create_answer_with_option_type(Survey::OptionsType.single_choice_with_text, true) 89 | answer_try_1 = create_answer(option: option, attempt: attempt, question: question, option_text: nil) 90 | 91 | should_be_persisted survey 92 | should_not_be_persisted answer_try_1 93 | end 94 | 95 | test 'should not create an answer with empty option_number field for options with multi_choices_with_number type' do 96 | survey, option, attempt, question = create_answer_with_option_type(Survey::OptionsType.multi_choices_with_number, true) 97 | answer_try_1 = create_answer(option: option, attempt: attempt, question: question, option_number: nil) 98 | 99 | should_be_persisted survey 100 | should_not_be_persisted answer_try_1 101 | end 102 | 103 | test 'should not create an answer with empty option_number field for options with single_choice_with_number type' do 104 | survey, option, attempt, question = create_answer_with_option_type(Survey::OptionsType.single_choice_with_number, true) 105 | answer_try_1 = create_answer(option: option, attempt: attempt, question: question, option_number: nil) 106 | 107 | should_be_persisted survey 108 | should_not_be_persisted answer_try_1 109 | end 110 | 111 | test 'should create an answer with options with multi_choices type, and text field should be empty' do 112 | survey, option, attempt, question = create_answer_with_option_type(Survey::OptionsType.multi_choices, true) 113 | answer_try_1 = create_answer(option: option, attempt: attempt, question: question, option_text: Faker::Name.name) 114 | 115 | should_be_persisted survey 116 | should_be_persisted answer_try_1 117 | assert_equal answer_try_1.option_text, nil 118 | end 119 | 120 | test 'should create an answer with options with single_choice type, and text field should be empty' do 121 | survey, option, attempt, question = create_answer_with_option_type(Survey::OptionsType.single_choice, true) 122 | answer_try_1 = create_answer(option: option, attempt: attempt, question: question, option_text: Faker::Name.name) 123 | 124 | should_be_persisted survey 125 | should_be_persisted answer_try_1 126 | assert_equal answer_try_1.option_text, nil 127 | end 128 | 129 | test 'should create an answer with a predefined_value_id field for single_choice type' do 130 | survey, option, attempt, question = create_answer_with_option_type(Survey::OptionsType.single_choice, true) 131 | predefined_value = create_predefined_value 132 | question.predefined_values << predefined_value 133 | question.save 134 | answer_try_1 = create_answer(option: option, attempt: attempt, question: question, predefined_value_id: predefined_value.id) 135 | 136 | should_be_persisted survey 137 | should_be_persisted question 138 | should_be_persisted answer_try_1 139 | assert_equal answer_try_1.predefined_value_id, predefined_value.id 140 | end 141 | 142 | test 'should not create an answer with an empty predefined_value_id field for single_choice type' do 143 | survey, option, attempt, question = create_answer_with_option_type(Survey::OptionsType.single_choice, true) 144 | question.predefined_values << create_predefined_value 145 | question.save 146 | answer_try_1 = create_answer(option: option, attempt: attempt, question: question, predefined_value_id: nil) 147 | 148 | should_be_persisted survey 149 | should_not_be_persisted answer_try_1 150 | end 151 | 152 | test 'can create an answer already made to the same attempt' do 153 | answer_try_1 = create_answer 154 | attempt = answer_try_1.attempt 155 | question = answer_try_1.question 156 | option = (question.options - [answer_try_1.option]).first 157 | answer_try_2 = create_answer(attempt: attempt, question: question, option: option) 158 | 159 | should_be_persisted answer_try_1 160 | should_be_persisted answer_try_2 161 | end 162 | end 163 | -------------------------------------------------------------------------------- /test/models/attempt_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | class AttemptTest < ActiveSupport::TestCase 6 | NUMBER_OF_ATTEMPTS = 5 7 | 8 | test "should pass if the user has #{NUMBER_OF_ATTEMPTS} attempts completed" do 9 | user = create_user 10 | survey = create_survey(attempts_number: NUMBER_OF_ATTEMPTS) 11 | NUMBER_OF_ATTEMPTS.times { create_attempt_for(user, survey) } 12 | assert_equal NUMBER_OF_ATTEMPTS, number_of_current_attempts(user, survey) 13 | end 14 | 15 | test 'should raise error when the User tries to respond more times than acceptable' do 16 | user = create_user 17 | survey = create_survey(attempts_number: NUMBER_OF_ATTEMPTS) 18 | (NUMBER_OF_ATTEMPTS + 1).times { create_attempt_for(user, survey) } 19 | assert_not_equal (NUMBER_OF_ATTEMPTS + 1), number_of_current_attempts(user, survey) 20 | assert_equal NUMBER_OF_ATTEMPTS, number_of_current_attempts(user, survey) 21 | end 22 | 23 | test 'should compute the score correctly for a perfect attempt' do 24 | user = create_user 25 | survey = create_survey_with_sections(4) # These are multi-choice questions and there are 5 options per question 26 | attempt = create_attempt_for(user, survey, all: :right) 27 | assert_equal 20, attempt.score 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /test/models/option_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | class OptionTest < ActiveSupport::TestCase 6 | test 'should create a valid option' do 7 | option = create_option 8 | should_be_persisted option 9 | end 10 | 11 | test 'should create a valid option with multi choices type' do 12 | option = create_option(options_type_id: Survey::OptionsType.multi_choices) 13 | 14 | should_be_persisted option 15 | assert_equal option.options_type_id, Survey::OptionsType.multi_choices 16 | end 17 | 18 | test 'should create a valid option with single choice type' do 19 | option = create_option(options_type_id: Survey::OptionsType.single_choice) 20 | 21 | should_be_persisted option 22 | assert_equal option.options_type_id, Survey::OptionsType.single_choice 23 | end 24 | 25 | test 'should create a valid option with number type' do 26 | option = create_option(options_type_id: Survey::OptionsType.number) 27 | 28 | should_be_persisted option 29 | assert_equal option.options_type_id, Survey::OptionsType.number 30 | end 31 | 32 | test 'should create a valid option with text type' do 33 | option = create_option(options_type_id: Survey::OptionsType.text) 34 | 35 | should_be_persisted option 36 | assert_equal option.options_type_id, Survey::OptionsType.text 37 | end 38 | 39 | test 'should create a valid option with large_text type' do 40 | option = create_option(options_type_id: Survey::OptionsType.large_text) 41 | 42 | should_be_persisted option 43 | assert_equal option.options_type_id, Survey::OptionsType.large_text 44 | end 45 | 46 | test 'should create a valid option with accepted type' do 47 | option = create_option(options_type_id: 99) 48 | 49 | should_not_be_persisted option 50 | end 51 | 52 | test 'should not create an option with a empty or nil options_type_id field' do 53 | option = create_option(options_type_id: nil) 54 | 55 | should_not_be_persisted option 56 | end 57 | 58 | test 'should create a option with empty or nil text fields for text or number types' do 59 | optionA = create_option(text: '', options_type_id: Survey::OptionsType.text) 60 | optionB = create_option(text: nil, options_type_id: Survey::OptionsType.text) 61 | 62 | optionC = create_option(text: '', options_type_id: Survey::OptionsType.number) 63 | optionD = create_option(text: nil, options_type_id: Survey::OptionsType.number) 64 | 65 | should_be_persisted optionA 66 | should_be_persisted optionB 67 | 68 | should_be_persisted optionC 69 | should_be_persisted optionD 70 | end 71 | 72 | test 'should not create a option with empty or nil text fields for multi_choices or single_choice types' do 73 | optionA = create_option(text: '', options_type_id: Survey::OptionsType.multi_choices) 74 | optionB = create_option(text: nil, options_type_id: Survey::OptionsType.multi_choices) 75 | 76 | optionC = create_option(text: '', options_type_id: Survey::OptionsType.single_choice) 77 | optionD = create_option(text: nil, options_type_id: Survey::OptionsType.single_choice) 78 | 79 | optionE = create_option(text: '', options_type_id: Survey::OptionsType.multi_choices_with_text) 80 | optionF = create_option(text: nil, options_type_id: Survey::OptionsType.multi_choices_with_text) 81 | 82 | optionG = create_option(text: '', options_type_id: Survey::OptionsType.single_choice_with_text) 83 | optionH = create_option(text: nil, options_type_id: Survey::OptionsType.single_choice_with_text) 84 | 85 | optionI = create_option(text: '', options_type_id: Survey::OptionsType.multi_choices_with_number) 86 | optionJ = create_option(text: nil, options_type_id: Survey::OptionsType.multi_choices_with_number) 87 | 88 | optionK = create_option(text: '', options_type_id: Survey::OptionsType.single_choice_with_number) 89 | optionL = create_option(text: nil, options_type_id: Survey::OptionsType.single_choice_with_number) 90 | 91 | should_not_be_persisted optionA 92 | should_not_be_persisted optionB 93 | 94 | should_not_be_persisted optionC 95 | should_not_be_persisted optionD 96 | 97 | should_not_be_persisted optionE 98 | should_not_be_persisted optionF 99 | 100 | should_not_be_persisted optionG 101 | should_not_be_persisted optionH 102 | 103 | should_not_be_persisted optionI 104 | should_not_be_persisted optionJ 105 | 106 | should_not_be_persisted optionK 107 | should_not_be_persisted optionL 108 | end 109 | 110 | test 'should be true if option A is correct and option B incorrect' do 111 | optionA = create_option(correct: false) 112 | optionB = create_option(correct: true) 113 | 114 | should_be_false optionA.correct? 115 | should_be_true optionB.correct? 116 | end 117 | 118 | # correct => default weight is 1 119 | # incorrect => default weight is 0 120 | test 'should be true weights are synchronized with the correct flag' do 121 | optionA = create_option(correct: false) 122 | optionB = create_option(correct: true) 123 | optionC = create_option(correct: true, weight: 5) 124 | 125 | should_be_true (optionA.weight == 0) 126 | should_be_true (optionB.weight == 1) 127 | should_be_true (optionC.weight == 5) 128 | end 129 | end 130 | -------------------------------------------------------------------------------- /test/models/predefined_value_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | class PredefinedValueTest < ActiveSupport::TestCase 6 | test 'should create a valid predefined_value' do 7 | predefined_value = create_predefined_value 8 | should_be_persisted predefined_value 9 | end 10 | 11 | test 'should not create a predefined_value with a empty or nil name field' do 12 | predefined_value_a = create_predefined_value(name: nil) 13 | predefined_value_b = create_predefined_value(name: '') 14 | 15 | should_not_be_persisted predefined_value_a 16 | should_not_be_persisted predefined_value_b 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/models/question_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | class QuestionTest < ActiveSupport::TestCase 6 | test 'should create a valid question' do 7 | question = create_question 8 | should_be_persisted question 9 | end 10 | 11 | test 'should create a valid question with multi choices type' do 12 | question = create_question(questions_type_id: Survey::QuestionsType.multiple_choice) 13 | should_be_persisted question 14 | assert_equal question.questions_type_id, Survey::QuestionsType.multiple_choice 15 | end 16 | 17 | test 'should create a valid question with accepted type' do 18 | question = create_question(questions_type_id: 99) 19 | 20 | should_not_be_persisted question 21 | end 22 | 23 | test 'should create a valid question with predefined_values' do 24 | question = create_question(predefined_values: [create_predefined_value]) 25 | 26 | should_be_persisted question 27 | assert_equal question.predefined_values.count, 1 28 | end 29 | 30 | test 'should not create a question with a empty or nil questions_type_id field' do 31 | question = create_question(questions_type_id: nil) 32 | 33 | should_not_be_persisted question 34 | end 35 | 36 | test 'should not create a question with a empty or nil text fields' do 37 | question1 = create_question(text: nil) 38 | question2 = create_question(text: '') 39 | 40 | should_not_be_persisted question1 41 | should_not_be_persisted question2 42 | end 43 | 44 | test 'should return true when passed a correct answer to the question object' do 45 | question = create_question 46 | question.options.create(correct_option_attributes) 47 | 6.times { question.options.create(option_attributes) } 48 | 49 | correct_option = question.options.first 50 | should_be_true question.correct_options.include?(correct_option) 51 | 52 | # by default when we create a new question it creates a correct answer directly 53 | # when we create the second question with the correct flag equal a true 54 | # we have to start the iteration in position number two 55 | question.options[2..question.options.size - 2].each do |option| 56 | should_be_false question.correct_options.include?(option) 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /test/models/section_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class SectionTest < ActiveSupport::TestCase 4 | test 'should not create a valid section without questions' do 5 | section = create_section 6 | should_not_be_persisted section 7 | end 8 | 9 | test 'should create a section with 3 questions' do 10 | num_questions = 3 11 | survey = create_survey_with_sections(num_questions, 1) 12 | should_be_persisted survey 13 | assert_equal survey.sections.first.questions.size, num_questions 14 | end 15 | 16 | test 'should not save section without all the needed fields' do 17 | section_without_name = create_section(name: nil) 18 | %w[name].each do |suffix| 19 | should_not_be_persisted eval("section_without_#{suffix}") 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /test/models/survey_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class SurveyTest < ActiveSupport::TestCase 4 | test 'should not create a valid survey without sections' do 5 | survey = create_survey 6 | should_not_be_persisted survey 7 | end 8 | 9 | test 'should not create a survey with active flag true and empty questions collection' do 10 | surveyA = create_survey(active: true) 11 | surveyB = create_survey_with_sections(2) 12 | surveyB.active = true 13 | surveyB.save 14 | 15 | should_not_be_persisted surveyA 16 | should_be_persisted surveyB 17 | should_be_true surveyB.valid? 18 | end 19 | 20 | test 'should create a survey with 3 sections' do 21 | num_questions = 3 22 | survey = create_survey_with_sections(num_questions, num_questions) 23 | should_be_persisted survey 24 | assert_equal survey.sections.size, num_questions 25 | end 26 | 27 | test 'should create a survey with 2 questions' do 28 | num_questions = 2 29 | survey = create_survey_with_sections(num_questions, 1) 30 | should_be_persisted survey 31 | assert_equal survey.sections.first.questions.size, num_questions 32 | end 33 | 34 | test 'should not create a survey with attempts_number lower than 0' do 35 | survey = create_survey(attempts_number: -1) 36 | should_not_be_persisted survey 37 | end 38 | 39 | test 'should not save survey without all the needed fields' do 40 | survey_without_name = create_survey(name: nil) 41 | survey_without_description = create_survey(description: nil) 42 | %w[name description].each do |suffix| 43 | should_not_be_persisted eval("survey_without_#{suffix}") 44 | end 45 | end 46 | 47 | test 'should have the correct associations via "has_many_surveys"' do 48 | lesson = create_lesson 49 | survey = create_survey_with_sections(2, 1) 50 | survey.lesson_id = lesson.id 51 | survey.save 52 | 53 | assert_equal survey, lesson.surveys.first 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /test/support/assertions.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'active_support/test_case' 4 | 5 | class ActiveSupport::TestCase 6 | def should_be_true(assertion) 7 | assert assertion 8 | end 9 | 10 | def should_be_false(assertion) 11 | assert !assertion 12 | end 13 | 14 | def assert_not_with_message(assertion, message) 15 | assert !assertion, message 16 | end 17 | 18 | def assert_blank(assertion) 19 | should_be_true assertion.blank? 20 | end 21 | 22 | def assert_not_blank(assertion) 23 | assert assertion.present? 24 | end 25 | 26 | def assert_not_nil(assertion) 27 | should_be_true !assertion.nil? 28 | end 29 | 30 | def should_be_persisted(assertion) 31 | should_be_true assertion.valid? 32 | should_be_false assertion.new_record? 33 | end 34 | 35 | def should_not_be_persisted(assertion) 36 | should_be_true assertion.new_record? 37 | should_be_false assertion.valid? 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /test/support/factories.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Factories 4 | 5 | # Create a Survey::Survey 6 | def create_survey(opts = {}) 7 | Survey::Survey.create({ 8 | name: ::Faker::Name.name, 9 | attempts_number: 3, 10 | description: ::Faker::Lorem.paragraph(1) 11 | }.merge(opts)) 12 | end 13 | 14 | # Create a Survey::Section 15 | def create_section(opts = {}) 16 | Survey::Section.create({ 17 | head_number: ::Faker::Name.name, 18 | name: ::Faker::Name.name, 19 | description: ::Faker::Lorem.paragraph(1) 20 | }.merge(opts)) 21 | end 22 | 23 | # Create a Survey::Question 24 | def create_question(opts = {}) 25 | Survey::Question.create({ 26 | text: ::Faker::Lorem.paragraph(1), 27 | options_attributes: { option: correct_option_attributes }, 28 | questions_type_id: Survey::QuestionsType.multiple_choice, 29 | mandatory: false 30 | }.merge(opts)) 31 | end 32 | 33 | # Create a Survey::PredefinedValue 34 | def create_predefined_value(opts = {}) 35 | Survey::PredefinedValue.create({ 36 | name: ::Faker::Name.name 37 | }.merge(opts)) 38 | end 39 | 40 | # Create a Survey::option but not saved 41 | def new_option(opts = {}) 42 | Survey::Option.new(option_attributes.merge(opts)) 43 | end 44 | 45 | # Create a Survey::Option 46 | def create_option(opts = {}) 47 | Survey::Option.create(option_attributes.merge(opts)) 48 | end 49 | 50 | def option_attributes 51 | { text: ::Faker::Lorem.paragraph(1), 52 | options_type_id: Survey::OptionsType.multi_choices } 53 | end 54 | 55 | def correct_option_attributes 56 | option_attributes.merge(correct: true) 57 | end 58 | 59 | def create_attempt(opts = {}) 60 | attempt = Survey::Attempt.create do |t| 61 | t.survey = opts.fetch(:survey, nil) 62 | t.participant = opts.fetch(:user, nil) 63 | opts.fetch(:options, []).each do |option| 64 | t.answers.new(option: option, question: option.question, attempt: t) 65 | end 66 | end 67 | end 68 | 69 | def create_survey_with_sections(num, sections_num = 1) 70 | survey = create_survey 71 | sections_num.times do 72 | section = create_section 73 | num.times do 74 | question = create_question 75 | num.times do 76 | question.options << create_option(correct_option_attributes) 77 | end 78 | section.questions << question 79 | end 80 | survey.sections << section 81 | end 82 | survey.save 83 | survey 84 | end 85 | 86 | def create_attempt_for(user, survey, opts = {}) 87 | if opts.fetch(:all, :wrong) == :right 88 | correct_survey = survey.correct_options 89 | create_attempt(options: correct_survey, 90 | user: user, 91 | survey: survey) 92 | else 93 | incorrect_survey = survey.correct_options 94 | incorrect_survey.shift 95 | create_attempt(options: incorrect_survey, 96 | user: user, 97 | survey: survey) 98 | end 99 | end 100 | 101 | def create_answer(opts = {}) 102 | survey = create_survey_with_sections(1) 103 | section = survey.sections.first 104 | question = section.questions.first 105 | option = section.questions.first.options.first 106 | attempt = create_attempt(user: create_user, survey: survey) 107 | Survey::Answer.create({ option: option, attempt: attempt, question: question }.merge(opts)) 108 | end 109 | 110 | def create_answer_with_option_type(options_type, mandatory = false) 111 | option = create_option(options_type_id: options_type) 112 | question = create_question(questions_type_id: Survey::QuestionsType.multiple_choice, mandatory: mandatory) 113 | section = create_section 114 | survey = create_survey 115 | 116 | question.options << option 117 | section.questions << question 118 | survey.sections << section 119 | survey.save 120 | 121 | attempt = create_attempt(user: create_user, survey: survey) 122 | 123 | [survey, option, attempt, question] 124 | end 125 | 126 | # Dummy Model from Dummy Application 127 | def create_user 128 | User.create(name: Faker::Name.name) 129 | end 130 | 131 | def create_lesson 132 | Lesson.create(name: Faker::Company.catch_phrase) 133 | end 134 | -------------------------------------------------------------------------------- /test/support/handlers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | def number_of_current_attempts(participant, survey) 4 | participant.for_survey(survey).size if participant.respond_to?(:for_survey) 5 | end 6 | 7 | def participant_score(user, survey) 8 | survey.attempts.for_participant(user).high_score 9 | end 10 | 11 | def participant_with_more_right_answers(survey) 12 | survey.attempts.scores.first.participant 13 | end 14 | 15 | def participant_with_more_wrong_answers(survey) 16 | survey.attempts.scores.last.participant 17 | end 18 | -------------------------------------------------------------------------------- /test/survey_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | class SurveyTest < ActiveSupport::TestCase 6 | test 'should pass if all the users has the same score' do 7 | user_a = create_user 8 | user_b = create_user 9 | survey = create_survey_with_sections(2) 10 | 11 | create_attempt_for(user_a, survey, all: :right) 12 | create_attempt_for(user_b, survey, all: :right) 13 | 14 | assert_equal participant_score(user_a, survey), 15 | participant_score(user_b, survey) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Configure Rails Envinronment 4 | ENV['RAILS_ENV'] = 'test' 5 | 6 | require File.expand_path('../dummy/config/environment.rb', __FILE__) 7 | require 'rails/test_help' 8 | require 'mocha/setup' 9 | require 'faker' 10 | require 'pry' 11 | 12 | Rails.backtrace_cleaner.remove_silencers! 13 | 14 | # Run any available migration 15 | ActiveRecord::MigrationContext.new(File.expand_path('../dummy/db/migrate/', __FILE__)) 16 | 17 | # Load support files 18 | # Add support to load paths so we can overwrite broken webrat setup 19 | $LOAD_PATH.unshift File.expand_path('../support', __FILE__) 20 | Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f } 21 | --------------------------------------------------------------------------------