ask 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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);