ask 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (77) hide show
  1. data/MIT-LICENSE +20 -0
  2. data/README.markdown +64 -0
  3. data/Rakefile +39 -0
  4. data/app/assets/images/add.png +0 -0
  5. data/app/assets/images/delete.png +0 -0
  6. data/app/assets/javascripts/ask.js +2 -0
  7. data/app/assets/javascripts/ask/forms.js +100 -0
  8. data/app/assets/stylesheets/ask.css +4 -0
  9. data/app/assets/stylesheets/ask/forms.css.sass +139 -0
  10. data/app/assets/stylesheets/ask/submissions.css.sass +60 -0
  11. data/app/helpers/ask_helper.rb +23 -0
  12. data/app/models/answer.rb +20 -0
  13. data/app/models/checklist_question.rb +11 -0
  14. data/app/models/choice.rb +7 -0
  15. data/app/models/choose_one_question.rb +7 -0
  16. data/app/models/essay_question.rb +3 -0
  17. data/app/models/form_section.rb +7 -0
  18. data/app/models/question.rb +43 -0
  19. data/app/models/text_question.rb +3 -0
  20. data/app/views/answerer/_answers.html.haml +9 -0
  21. data/app/views/answerer/_form.html.haml +39 -0
  22. data/app/views/asker/_form.html.haml +27 -0
  23. data/app/views/asker/_form_preview.html.haml +35 -0
  24. data/db/migrate/20110913214050_create_answers.rb +15 -0
  25. data/db/migrate/20110913214255_create_choices.rb +11 -0
  26. data/db/migrate/20110913214410_create_questions.rb +15 -0
  27. data/lib/ask.rb +6 -0
  28. data/lib/ask/acts_as_answerer.rb +84 -0
  29. data/lib/ask/acts_as_asker.rb +8 -0
  30. data/lib/ask/engine.rb +4 -0
  31. data/lib/ask/version.rb +3 -0
  32. data/lib/assets/javascripts/jquery.handle-nesting.js +113 -0
  33. data/test/ask_test.rb +7 -0
  34. data/test/dummy/Rakefile +7 -0
  35. data/test/dummy/app/assets/javascripts/application.js +9 -0
  36. data/test/dummy/app/assets/stylesheets/application.css +7 -0
  37. data/test/dummy/app/controllers/application_controller.rb +3 -0
  38. data/test/dummy/app/helpers/application_helper.rb +2 -0
  39. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  40. data/test/dummy/config.ru +4 -0
  41. data/test/dummy/config/application.rb +45 -0
  42. data/test/dummy/config/boot.rb +10 -0
  43. data/test/dummy/config/database.yml +25 -0
  44. data/test/dummy/config/environment.rb +5 -0
  45. data/test/dummy/config/environments/development.rb +30 -0
  46. data/test/dummy/config/environments/production.rb +60 -0
  47. data/test/dummy/config/environments/test.rb +42 -0
  48. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  49. data/test/dummy/config/initializers/inflections.rb +10 -0
  50. data/test/dummy/config/initializers/mime_types.rb +5 -0
  51. data/test/dummy/config/initializers/secret_token.rb +7 -0
  52. data/test/dummy/config/initializers/session_store.rb +8 -0
  53. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  54. data/test/dummy/config/locales/en.yml +5 -0
  55. data/test/dummy/config/routes.rb +58 -0
  56. data/test/dummy/log/development.log +0 -0
  57. data/test/dummy/public/404.html +26 -0
  58. data/test/dummy/public/422.html +26 -0
  59. data/test/dummy/public/500.html +26 -0
  60. data/test/dummy/public/favicon.ico +0 -0
  61. data/test/dummy/script/rails +6 -0
  62. data/test/fixtures/answers.yml +13 -0
  63. data/test/fixtures/choices.yml +9 -0
  64. data/test/fixtures/questions.yml +17 -0
  65. data/test/fixtures/submissions.yml +9 -0
  66. data/test/functional/answers_controller_test.rb +49 -0
  67. data/test/functional/choices_controller_test.rb +49 -0
  68. data/test/functional/questions_controller_test.rb +49 -0
  69. data/test/integration/navigation_test.rb +10 -0
  70. data/test/test_helper.rb +10 -0
  71. data/test/unit/answer_test.rb +7 -0
  72. data/test/unit/choice_test.rb +7 -0
  73. data/test/unit/helpers/answers_helper_test.rb +4 -0
  74. data/test/unit/helpers/choices_helper_test.rb +4 -0
  75. data/test/unit/helpers/questions_helper_test.rb +4 -0
  76. data/test/unit/question_test.rb +7 -0
  77. 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,11 @@
1
+ class ChecklistQuestion < Question
2
+
3
+ def supports_choices
4
+ true
5
+ end
6
+
7
+ def supports_multiple_answers?
8
+ true
9
+ end
10
+
11
+ end
@@ -0,0 +1,7 @@
1
+ class Choice < ActiveRecord::Base
2
+
3
+ attr_accessible :name
4
+
5
+ belongs_to :question
6
+
7
+ end
@@ -0,0 +1,7 @@
1
+ class ChooseOneQuestion < Question
2
+
3
+ def supports_choices
4
+ true
5
+ end
6
+
7
+ end
@@ -0,0 +1,3 @@
1
+ class EssayQuestion < Question
2
+
3
+ end
@@ -0,0 +1,7 @@
1
+ class FormSection < Question
2
+
3
+ def rhetorical?
4
+ true
5
+ end
6
+
7
+ 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,3 @@
1
+ class TextQuestion < Question
2
+
3
+ end
@@ -0,0 +1,9 @@
1
+ -answerer.all_questions.each do |question|
2
+ -if(question.rhetorical?)
3
+ %h3=question.name
4
+ %p=question.instructions
5
+ -else
6
+ %dt=question.name
7
+ -answerer.answers_to(question).each do |answer|
8
+ -unless answer.answer.blank?
9
+ %dd=answer.answer
@@ -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,11 @@
1
+ class CreateChoices < ActiveRecord::Migration
2
+ def change
3
+ create_table :choices do |t|
4
+ t.belongs_to :question
5
+ t.string :name
6
+
7
+ t.timestamps
8
+ end
9
+ add_index :choices, :question_id
10
+ end
11
+ 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
@@ -0,0 +1,6 @@
1
+ require "ask/engine"
2
+ require "ask/acts_as_asker"
3
+ require "ask/acts_as_answerer"
4
+
5
+ module Ask
6
+ end
@@ -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
@@ -0,0 +1,4 @@
1
+ module Ask
2
+ class Engine < Rails::Engine
3
+ end
4
+ end
@@ -0,0 +1,3 @@
1
+ module Ask
2
+ VERSION = "0.1.0"
3
+ end
@@ -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);