ask 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/MIT-LICENSE +20 -0
- data/README.markdown +64 -0
- data/Rakefile +39 -0
- data/app/assets/images/add.png +0 -0
- data/app/assets/images/delete.png +0 -0
- data/app/assets/javascripts/ask.js +2 -0
- data/app/assets/javascripts/ask/forms.js +100 -0
- data/app/assets/stylesheets/ask.css +4 -0
- data/app/assets/stylesheets/ask/forms.css.sass +139 -0
- data/app/assets/stylesheets/ask/submissions.css.sass +60 -0
- data/app/helpers/ask_helper.rb +23 -0
- data/app/models/answer.rb +20 -0
- data/app/models/checklist_question.rb +11 -0
- data/app/models/choice.rb +7 -0
- data/app/models/choose_one_question.rb +7 -0
- data/app/models/essay_question.rb +3 -0
- data/app/models/form_section.rb +7 -0
- data/app/models/question.rb +43 -0
- data/app/models/text_question.rb +3 -0
- data/app/views/answerer/_answers.html.haml +9 -0
- data/app/views/answerer/_form.html.haml +39 -0
- data/app/views/asker/_form.html.haml +27 -0
- data/app/views/asker/_form_preview.html.haml +35 -0
- data/db/migrate/20110913214050_create_answers.rb +15 -0
- data/db/migrate/20110913214255_create_choices.rb +11 -0
- data/db/migrate/20110913214410_create_questions.rb +15 -0
- data/lib/ask.rb +6 -0
- data/lib/ask/acts_as_answerer.rb +84 -0
- data/lib/ask/acts_as_asker.rb +8 -0
- data/lib/ask/engine.rb +4 -0
- data/lib/ask/version.rb +3 -0
- data/lib/assets/javascripts/jquery.handle-nesting.js +113 -0
- data/test/ask_test.rb +7 -0
- data/test/dummy/Rakefile +7 -0
- data/test/dummy/app/assets/javascripts/application.js +9 -0
- data/test/dummy/app/assets/stylesheets/application.css +7 -0
- data/test/dummy/app/controllers/application_controller.rb +3 -0
- data/test/dummy/app/helpers/application_helper.rb +2 -0
- data/test/dummy/app/views/layouts/application.html.erb +14 -0
- data/test/dummy/config.ru +4 -0
- data/test/dummy/config/application.rb +45 -0
- data/test/dummy/config/boot.rb +10 -0
- data/test/dummy/config/database.yml +25 -0
- data/test/dummy/config/environment.rb +5 -0
- data/test/dummy/config/environments/development.rb +30 -0
- data/test/dummy/config/environments/production.rb +60 -0
- data/test/dummy/config/environments/test.rb +42 -0
- data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/test/dummy/config/initializers/inflections.rb +10 -0
- data/test/dummy/config/initializers/mime_types.rb +5 -0
- data/test/dummy/config/initializers/secret_token.rb +7 -0
- data/test/dummy/config/initializers/session_store.rb +8 -0
- data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/test/dummy/config/locales/en.yml +5 -0
- data/test/dummy/config/routes.rb +58 -0
- data/test/dummy/log/development.log +0 -0
- data/test/dummy/public/404.html +26 -0
- data/test/dummy/public/422.html +26 -0
- data/test/dummy/public/500.html +26 -0
- data/test/dummy/public/favicon.ico +0 -0
- data/test/dummy/script/rails +6 -0
- data/test/fixtures/answers.yml +13 -0
- data/test/fixtures/choices.yml +9 -0
- data/test/fixtures/questions.yml +17 -0
- data/test/fixtures/submissions.yml +9 -0
- data/test/functional/answers_controller_test.rb +49 -0
- data/test/functional/choices_controller_test.rb +49 -0
- data/test/functional/questions_controller_test.rb +49 -0
- data/test/integration/navigation_test.rb +10 -0
- data/test/test_helper.rb +10 -0
- data/test/unit/answer_test.rb +7 -0
- data/test/unit/choice_test.rb +7 -0
- data/test/unit/helpers/answers_helper_test.rb +4 -0
- data/test/unit/helpers/choices_helper_test.rb +4 -0
- data/test/unit/helpers/questions_helper_test.rb +4 -0
- data/test/unit/question_test.rb +7 -0
- metadata +231 -0
@@ -0,0 +1,20 @@
|
|
1
|
+
class Answer < ActiveRecord::Base
|
2
|
+
|
3
|
+
attr_accessible :question_id, :answer, :choice_id
|
4
|
+
|
5
|
+
belongs_to :answerer, :polymorphic => true
|
6
|
+
belongs_to :question
|
7
|
+
belongs_to :choice
|
8
|
+
|
9
|
+
validates_presence_of :question_id
|
10
|
+
validates_presence_of :answer, :if=>lambda{|answer| !answer.question.is_a?(ChecklistQuestion) && answer.question.required }
|
11
|
+
|
12
|
+
default_scope joins(:question).order('questions.position')
|
13
|
+
scope :for_answerer, lambda{|answerer| where(:answerer_type => answerer.class.to_s, :answerer_id => answerer.id)}
|
14
|
+
scope :for_question, lambda{|question| where(:question_id => question.id)}
|
15
|
+
|
16
|
+
def to_s
|
17
|
+
self.answer.to_s
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
class Question < ActiveRecord::Base
|
2
|
+
|
3
|
+
require 'acts_as_list'
|
4
|
+
|
5
|
+
TYPES = ['TextQuestion', 'EssayQuestion', 'ChooseOneQuestion', 'ChecklistQuestion', 'FormSection']
|
6
|
+
|
7
|
+
belongs_to :asker, :polymorphic => true
|
8
|
+
has_many :choices, :dependent=>:destroy
|
9
|
+
|
10
|
+
acts_as_list :scope=>:asker
|
11
|
+
|
12
|
+
attr_accessible :type, :name, :instructions, :required, :choices_attributes, :position
|
13
|
+
accepts_nested_attributes_for :choices, :allow_destroy=>true, :reject_if=>lambda{|attrs| attrs['name'].blank? }
|
14
|
+
|
15
|
+
validates_presence_of :type, :name
|
16
|
+
validates_inclusion_of :type, :in=>TYPES
|
17
|
+
|
18
|
+
default_scope :order => :position
|
19
|
+
|
20
|
+
def attributes_protected_by_default
|
21
|
+
default = [ self.class.primary_key ]
|
22
|
+
default << 'id' unless self.class.primary_key.eql? 'id'
|
23
|
+
default
|
24
|
+
end
|
25
|
+
|
26
|
+
def rhetorical?
|
27
|
+
false
|
28
|
+
end
|
29
|
+
|
30
|
+
def supports_choices
|
31
|
+
false
|
32
|
+
end
|
33
|
+
|
34
|
+
def choice_names
|
35
|
+
return [] unless supports_choices
|
36
|
+
self.choices.map(&:name)
|
37
|
+
end
|
38
|
+
|
39
|
+
def supports_multiple_answers?
|
40
|
+
false
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
-last_question = Question.new
|
2
|
+
=f.fields_for(:answers) do |a|
|
3
|
+
-question = a.object.question
|
4
|
+
-if(question.rhetorical?)
|
5
|
+
%h3.section
|
6
|
+
=question.name
|
7
|
+
%p.section.instructions
|
8
|
+
=question.instructions
|
9
|
+
-else
|
10
|
+
=a.hidden_field(:question_id, :value=>question.id)
|
11
|
+
-if question.name != last_question.name
|
12
|
+
%dt
|
13
|
+
=question_label(question, a)
|
14
|
+
=question_instructions(question)
|
15
|
+
-if question.required
|
16
|
+
%strong{:style=>"color:red;"} *
|
17
|
+
-case(question.type.to_sym)
|
18
|
+
|
19
|
+
-when :TextQuestion
|
20
|
+
%dd=a.text_field(:answer)
|
21
|
+
|
22
|
+
-when :EssayQuestion
|
23
|
+
%dd=a.text_area(:answer)
|
24
|
+
|
25
|
+
-when :ChooseOneQuestion
|
26
|
+
-if question.choices.count > 5
|
27
|
+
%dd=a.collection_select(:answer, question.choice_names, :to_s, :to_s, :include_blank=>!question.required)
|
28
|
+
-else
|
29
|
+
-question.choices.each do |choice|
|
30
|
+
%dd
|
31
|
+
=a.radio_button(:answer, choice.name)
|
32
|
+
=a.label(:answer, choice.name, :value=>choice.name)
|
33
|
+
|
34
|
+
-when :ChecklistQuestion
|
35
|
+
%dd
|
36
|
+
=a.hidden_field(:choice_id, :value=>a.object.choice_id)
|
37
|
+
=a.check_box(:answer, {}, a.object.choice.try(:name), '')
|
38
|
+
=a.label(:answer, a.object.choice.try(:name))
|
39
|
+
-last_question = question
|
@@ -0,0 +1,27 @@
|
|
1
|
+
%ul.ask.questions
|
2
|
+
=f.fields_for(:questions) do |question|
|
3
|
+
%li.question
|
4
|
+
%dl
|
5
|
+
.hidden.position-wrapper
|
6
|
+
=question.hidden_field(:position)
|
7
|
+
%dt.name=question.label(:name)
|
8
|
+
%dd.name=question.text_field(:name, :placeholder=>'Title')
|
9
|
+
%dt.instructions=question.label(:instructions)
|
10
|
+
%dd.instructions=question.text_area(:instructions, :placeholder=>'Instructions')
|
11
|
+
%dt.type=question.label(:type)
|
12
|
+
%dd.type=question.collection_select(:type, Question::TYPES, :to_s, :titleize)
|
13
|
+
%dt.required=question.label(:required)
|
14
|
+
%dd.required=question.select(:required, [['Optional', false], ['Required', true]])
|
15
|
+
-unless question.object.new_record?
|
16
|
+
%dt.delete=question.label(:_destroy, 'Delete')
|
17
|
+
%dd.delete=question.check_box(:_destroy)
|
18
|
+
%dt.choices
|
19
|
+
%dd.choices
|
20
|
+
%ul.choices
|
21
|
+
=question.fields_for(:choices) do |choice|
|
22
|
+
%li.choice
|
23
|
+
=choice.label(:name)
|
24
|
+
=choice.text_field(:name, :placeholder=>'Choice')
|
25
|
+
-unless choice.object.new_record?
|
26
|
+
=choice.label(:_destroy, 'Remove')
|
27
|
+
=choice.check_box(:_destroy)
|
@@ -0,0 +1,35 @@
|
|
1
|
+
%form(action="#")
|
2
|
+
-@form.questions.each do |question|
|
3
|
+
-last_question = Question.new
|
4
|
+
-if(question.rhetorical?)
|
5
|
+
%h3.section
|
6
|
+
=question.name
|
7
|
+
%p.section.instructions
|
8
|
+
=question.instructions
|
9
|
+
-else
|
10
|
+
-if question.name != last_question.name
|
11
|
+
%dt
|
12
|
+
=question_label(question)
|
13
|
+
=question_instructions(question)
|
14
|
+
-if question.required
|
15
|
+
%strong{:style=>"color:red;"} *
|
16
|
+
-case(question.type.to_sym)
|
17
|
+
-when :TextQuestion
|
18
|
+
%dd=text_field_tag(question.name.parameterize)
|
19
|
+
-when :EssayQuestion
|
20
|
+
%dd=text_area_tag(question.name.parameterize)
|
21
|
+
-when :ChooseOneQuestion
|
22
|
+
-if question.choices.count > 5
|
23
|
+
%dd=select_tag(question.name.parameterize, options_for_select((question.required ? [] : ['']) + question.choice_names))
|
24
|
+
-else
|
25
|
+
-question.choices.each do |choice|
|
26
|
+
%dd
|
27
|
+
=radio_button_tag(question.name.parameterize, choice.name)
|
28
|
+
=label_tag(question.name.parameterize, choice.name, :value=>choice.name)
|
29
|
+
-when :ChecklistQuestion
|
30
|
+
-question.choices.each do |choice|
|
31
|
+
%dd
|
32
|
+
=check_box_tag(question.name.parameterize, choice.name)
|
33
|
+
=label_tag(question.name.parameterize, choice.name)
|
34
|
+
|
35
|
+
-last_question = question
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class CreateAnswers < ActiveRecord::Migration
|
2
|
+
def change
|
3
|
+
create_table :answers do |t|
|
4
|
+
t.belongs_to :answerer, :polymorphic => true
|
5
|
+
t.belongs_to :question
|
6
|
+
t.belongs_to :choice
|
7
|
+
t.text :answer
|
8
|
+
|
9
|
+
t.timestamps
|
10
|
+
end
|
11
|
+
add_index :answers, [:answerer_id, :answerer_type]
|
12
|
+
add_index :answers, :question_id
|
13
|
+
add_index :answers, :choice_id
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class CreateQuestions < ActiveRecord::Migration
|
2
|
+
def change
|
3
|
+
create_table :questions do |t|
|
4
|
+
t.belongs_to :asker, :polymorphic => true
|
5
|
+
t.string :type
|
6
|
+
t.string :name
|
7
|
+
t.text :instructions
|
8
|
+
t.boolean :required
|
9
|
+
t.integer :position
|
10
|
+
|
11
|
+
t.timestamps
|
12
|
+
end
|
13
|
+
add_index :questions, [:asker_id, :asker_type]
|
14
|
+
end
|
15
|
+
end
|
data/lib/ask.rb
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
module Ask
|
2
|
+
module ActiveRecord
|
3
|
+
|
4
|
+
module InstanceMethods
|
5
|
+
def all_questions
|
6
|
+
if respond_to? :asker
|
7
|
+
@all_questions ||= asker.questions
|
8
|
+
else
|
9
|
+
raise NoMethodError, "You must define a #{self}#asker method that returns the related acts_as_asker model"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def find_question(question)
|
14
|
+
if question.is_a? Question
|
15
|
+
question
|
16
|
+
elsif question.is_a? Fixnum
|
17
|
+
all_questions.find_by_id(question)
|
18
|
+
else
|
19
|
+
all_questions.find_by_name(question.to_s)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def answer_to(question)
|
24
|
+
return nil if question.nil?
|
25
|
+
if question.supports_multiple_answers?
|
26
|
+
answers_to(question)
|
27
|
+
else
|
28
|
+
answers.find_by_question_id(question.id)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def answers_to(question)
|
33
|
+
return nil if question.nil?
|
34
|
+
answers.for_question(question)
|
35
|
+
end
|
36
|
+
|
37
|
+
def build_or_create_answers(questions)
|
38
|
+
method = new_record? ? :build : :create
|
39
|
+
|
40
|
+
questions.each do |question|
|
41
|
+
if answers_to(question).empty?
|
42
|
+
|
43
|
+
if question.supports_multiple_answers?
|
44
|
+
question.choices.each do |choice|
|
45
|
+
answers.send(method, :question_id=>question.id, :answer=>'', :choice_id=>choice.id)
|
46
|
+
end
|
47
|
+
else
|
48
|
+
answers.send(method, :question_id=>question.id)
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def questions_with_answers
|
56
|
+
qa = ActiveSupport::OrderedHash.new
|
57
|
+
|
58
|
+
# Set the correct order and make sure we have all the questions
|
59
|
+
all_questions.each do |q|
|
60
|
+
qa[q.name] = []
|
61
|
+
end
|
62
|
+
|
63
|
+
answers.each do |a|
|
64
|
+
qa[a.question.name] << a.answer.to_s.strip unless a.answer.blank?
|
65
|
+
end
|
66
|
+
|
67
|
+
qa
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
class ActiveRecord::Base
|
75
|
+
def self.acts_as_answerer
|
76
|
+
include Ask::ActiveRecord::InstanceMethods
|
77
|
+
|
78
|
+
has_many :answers, :as => :answerer, :dependent=>:destroy
|
79
|
+
has_many :questions, :through=>:answers
|
80
|
+
|
81
|
+
attr_accessible :answers_attributes
|
82
|
+
accepts_nested_attributes_for :answers, :allow_destroy=>true
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,8 @@
|
|
1
|
+
class ActiveRecord::Base
|
2
|
+
def self.acts_as_asker
|
3
|
+
has_many :questions, :as => :asker, :dependent => :destroy
|
4
|
+
|
5
|
+
attr_accessible :questions_attributes
|
6
|
+
accepts_nested_attributes_for :questions, :allow_destroy=>true, :reject_if=>lambda{|attrs| attrs['name'].blank? }
|
7
|
+
end
|
8
|
+
end
|
data/lib/ask/engine.rb
ADDED
data/lib/ask/version.rb
ADDED
@@ -0,0 +1,113 @@
|
|
1
|
+
(function($){
|
2
|
+
$.HandleNesting = function(el, options){
|
3
|
+
// To avoid scope issues, use 'base' instead of 'this'
|
4
|
+
// to reference this class from internal events and functions.
|
5
|
+
var base = this;
|
6
|
+
|
7
|
+
// Access to jQuery and DOM versions of element
|
8
|
+
base.$el = $(el);
|
9
|
+
base.el = el;
|
10
|
+
base.items = [];
|
11
|
+
base.blank = null;
|
12
|
+
|
13
|
+
// Add a reverse reference to the DOM object
|
14
|
+
base.$el.data("HandleNesting", base);
|
15
|
+
|
16
|
+
base.init = function(){
|
17
|
+
|
18
|
+
base.options = $.extend({},$.HandleNesting.defaultOptions, options);
|
19
|
+
|
20
|
+
base.items = base.$el.children('li');
|
21
|
+
if(base.items.length) {
|
22
|
+
base.blank = $(base.items[base.items.length-1]).clone();
|
23
|
+
}
|
24
|
+
|
25
|
+
base.addButtons();
|
26
|
+
|
27
|
+
};
|
28
|
+
|
29
|
+
base.addButtons = function() {
|
30
|
+
base.addAddButton();
|
31
|
+
base.items.each(function(i,item) { base.addRemoveButton(item); })
|
32
|
+
};
|
33
|
+
|
34
|
+
base.addAddButton = function() {
|
35
|
+
var addButton = $('<button type="button"/>').addClass('add').text('Add');
|
36
|
+
addButton.click(function() { base.add(); });
|
37
|
+
base.$el.after(addButton);
|
38
|
+
}
|
39
|
+
|
40
|
+
base.addRemoveButton = function(item) {
|
41
|
+
|
42
|
+
var destroys = $(item).find('input[name*=_destroy]')
|
43
|
+
|
44
|
+
destroys.each(function(i,destroy) {
|
45
|
+
|
46
|
+
if($(item).find('button.remove').length == 0) {
|
47
|
+
var removeButton = $('<button type="button"/>').addClass('remove').text('Remove');
|
48
|
+
removeButton.click(function() { base.remove(this); });
|
49
|
+
$(item).append(removeButton);
|
50
|
+
}
|
51
|
+
|
52
|
+
$(destroy).hide();
|
53
|
+
$(item).find('label[for='+$(destroy).attr('id')+']').hide();
|
54
|
+
|
55
|
+
});
|
56
|
+
}
|
57
|
+
|
58
|
+
base.add = function() {
|
59
|
+
|
60
|
+
var newId = new Date().getTime();
|
61
|
+
var match = base.options.nameMatch;
|
62
|
+
var replace = base.options.nameReplace.replace(/{NEW_ID}/g, newId);
|
63
|
+
|
64
|
+
if(base.options.beforeAdd != null) {
|
65
|
+
base.options.beforeAdd();
|
66
|
+
}
|
67
|
+
|
68
|
+
var added = base.blank.clone().hide();
|
69
|
+
|
70
|
+
added.find('*').each(function(i, el) {
|
71
|
+
$(el).attr('id', null);
|
72
|
+
});
|
73
|
+
|
74
|
+
added.find('input, select, textarea').each(function(i, el) {
|
75
|
+
$(el).val('');
|
76
|
+
el.name = el.name.replace(match, replace);
|
77
|
+
});
|
78
|
+
|
79
|
+
base.$el.append(added);
|
80
|
+
|
81
|
+
if(base.options.afterAdd != null) {
|
82
|
+
base.options.afterAdd(added);
|
83
|
+
}
|
84
|
+
|
85
|
+
added.fadeIn("fast");
|
86
|
+
|
87
|
+
}
|
88
|
+
|
89
|
+
base.remove = function(item) {
|
90
|
+
var li = $(item).closest('li');
|
91
|
+
li.find('input[name*=_destroy]').val(1);
|
92
|
+
li.find('input:checkbox[name*=_destroy]').attr('checked', 'checked');
|
93
|
+
li.fadeOut("fast");//, function() { $(this).remove() });
|
94
|
+
}
|
95
|
+
|
96
|
+
// Run initializer
|
97
|
+
base.init();
|
98
|
+
};
|
99
|
+
|
100
|
+
$.HandleNesting.defaultOptions = {
|
101
|
+
nameMatch: /\[[0-9]+\]/,
|
102
|
+
nameReplace: '[{NEW_ID}]',
|
103
|
+
beforeAdd: null,
|
104
|
+
afterAdd: null
|
105
|
+
};
|
106
|
+
|
107
|
+
$.fn.handleNesting = function(options){
|
108
|
+
return this.each(function(){
|
109
|
+
(new $.HandleNesting(this, options));
|
110
|
+
});
|
111
|
+
};
|
112
|
+
|
113
|
+
})(jQuery);
|