effective_polls 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (95) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +132 -0
  4. data/Rakefile +32 -0
  5. data/app/assets/config/effective_polls_manifest.js +1 -0
  6. data/app/assets/stylesheets/effective_polls.scss +0 -0
  7. data/app/controllers/admin/poll_notifications_controller.rb +15 -0
  8. data/app/controllers/admin/poll_questions_controller.rb +15 -0
  9. data/app/controllers/admin/polls_controller.rb +23 -0
  10. data/app/controllers/effective/ballots_controller.rb +70 -0
  11. data/app/controllers/effective/polls_controller.rb +19 -0
  12. data/app/datatables/admin/effective_poll_notifications_datatable.rb +43 -0
  13. data/app/datatables/admin/effective_poll_questions_datatable.rb +28 -0
  14. data/app/datatables/admin/effective_poll_results_datatable.rb +50 -0
  15. data/app/datatables/admin/effective_polls_datatable.rb +29 -0
  16. data/app/datatables/effective_polls_datatable.rb +30 -0
  17. data/app/helpers/effective_polls_helper.rb +23 -0
  18. data/app/mailers/effective/polls_mailer.rb +23 -0
  19. data/app/models/effective/access_denied.rb +17 -0
  20. data/app/models/effective/ballot.rb +73 -0
  21. data/app/models/effective/ballot_response.rb +92 -0
  22. data/app/models/effective/ballot_response_option.rb +6 -0
  23. data/app/models/effective/poll.rb +149 -0
  24. data/app/models/effective/poll_notification.rb +144 -0
  25. data/app/models/effective/poll_question.rb +76 -0
  26. data/app/models/effective/poll_question_option.rb +26 -0
  27. data/app/views/admin/poll_notifications/_form.html.haml +55 -0
  28. data/app/views/admin/poll_questions/_form.html.haml +100 -0
  29. data/app/views/admin/polls/_form.html.haml +20 -0
  30. data/app/views/admin/polls/_form_content.html.haml +27 -0
  31. data/app/views/admin/polls/_form_poll.html.haml +32 -0
  32. data/app/views/admin/polls/_poll.html.haml +2 -0
  33. data/app/views/admin/polls/results.html.haml +6 -0
  34. data/app/views/effective/ballot_responses/_ballot_response.html.haml +2 -0
  35. data/app/views/effective/ballot_responses/_fields.html.haml +14 -0
  36. data/app/views/effective/ballot_responses/fields/_choose_one.html.haml +1 -0
  37. data/app/views/effective/ballot_responses/fields/_date.html.haml +1 -0
  38. data/app/views/effective/ballot_responses/fields/_email.html.haml +1 -0
  39. data/app/views/effective/ballot_responses/fields/_long_answer.html.haml +1 -0
  40. data/app/views/effective/ballot_responses/fields/_number.html.haml +1 -0
  41. data/app/views/effective/ballot_responses/fields/_select_all_that_apply.html.haml +1 -0
  42. data/app/views/effective/ballot_responses/fields/_select_upto_1.html.haml +1 -0
  43. data/app/views/effective/ballot_responses/fields/_select_upto_2.html.haml +1 -0
  44. data/app/views/effective/ballot_responses/fields/_select_upto_3.html.haml +1 -0
  45. data/app/views/effective/ballot_responses/fields/_select_upto_4.html.haml +1 -0
  46. data/app/views/effective/ballot_responses/fields/_select_upto_5.html.haml +1 -0
  47. data/app/views/effective/ballot_responses/fields/_short_answer.html.haml +1 -0
  48. data/app/views/effective/ballot_responses/fields/_upload_file.html.haml +1 -0
  49. data/app/views/effective/ballot_responses/responses/_choose_one.html.haml +1 -0
  50. data/app/views/effective/ballot_responses/responses/_date.html.haml +1 -0
  51. data/app/views/effective/ballot_responses/responses/_email.html.haml +1 -0
  52. data/app/views/effective/ballot_responses/responses/_long_answer.html.haml +1 -0
  53. data/app/views/effective/ballot_responses/responses/_number.html.haml +1 -0
  54. data/app/views/effective/ballot_responses/responses/_select_all_that_apply.html.haml +5 -0
  55. data/app/views/effective/ballot_responses/responses/_select_upto_1.html.haml +1 -0
  56. data/app/views/effective/ballot_responses/responses/_select_upto_2.html.haml +5 -0
  57. data/app/views/effective/ballot_responses/responses/_select_upto_3.html.haml +5 -0
  58. data/app/views/effective/ballot_responses/responses/_select_upto_4.html.haml +5 -0
  59. data/app/views/effective/ballot_responses/responses/_select_upto_5.html.haml +5 -0
  60. data/app/views/effective/ballot_responses/responses/_short_answer.html.haml +1 -0
  61. data/app/views/effective/ballot_responses/responses/_upload_file.html.haml +4 -0
  62. data/app/views/effective/ballots/_ballot.html.haml +14 -0
  63. data/app/views/effective/ballots/complete.html.haml +11 -0
  64. data/app/views/effective/ballots/start.html.haml +17 -0
  65. data/app/views/effective/ballots/submit.html.haml +17 -0
  66. data/app/views/effective/ballots/vote.html.haml +19 -0
  67. data/app/views/effective/poll_results/_poll_result.html.haml +2 -0
  68. data/app/views/effective/poll_results/_results.html.haml +38 -0
  69. data/app/views/effective/poll_results/results/_choose_one.html.haml +2 -0
  70. data/app/views/effective/poll_results/results/_date.html.haml +14 -0
  71. data/app/views/effective/poll_results/results/_email.html.haml +14 -0
  72. data/app/views/effective/poll_results/results/_long_answer.html.haml +2 -0
  73. data/app/views/effective/poll_results/results/_number.html.haml +14 -0
  74. data/app/views/effective/poll_results/results/_poll_question_option.html.haml +17 -0
  75. data/app/views/effective/poll_results/results/_select_all_that_apply.html.haml +2 -0
  76. data/app/views/effective/poll_results/results/_select_upto_1.html.haml +2 -0
  77. data/app/views/effective/poll_results/results/_select_upto_2.html.haml +2 -0
  78. data/app/views/effective/poll_results/results/_select_upto_3.html.haml +2 -0
  79. data/app/views/effective/poll_results/results/_select_upto_4.html.haml +2 -0
  80. data/app/views/effective/poll_results/results/_select_upto_5.html.haml +2 -0
  81. data/app/views/effective/poll_results/results/_short_answer.html.haml +2 -0
  82. data/app/views/effective/poll_results/results/_upload_file.html.haml +3 -0
  83. data/app/views/effective/polls/_form.html.haml +10 -0
  84. data/app/views/effective/polls/_poll.html.haml +2 -0
  85. data/app/views/layouts/effective_polls_mailer_layout.html.haml +7 -0
  86. data/config/effective_polls.rb +59 -0
  87. data/config/routes.rb +23 -0
  88. data/db/migrate/01_create_effective_polls.rb.erb +92 -0
  89. data/lib/effective_polls.rb +50 -0
  90. data/lib/effective_polls/engine.rb +11 -0
  91. data/lib/effective_polls/version.rb +3 -0
  92. data/lib/generators/effective_polls/install_generator.rb +46 -0
  93. data/lib/generators/templates/effective_polls_mailer_preview.rb +35 -0
  94. data/lib/tasks/effective_polls_tasks.rake +24 -0
  95. metadata +276 -0
@@ -0,0 +1,144 @@
1
+ module Effective
2
+ class PollNotification < ActiveRecord::Base
3
+ belongs_to :poll
4
+
5
+ BATCHSIZE = (Rails.env.test? ? 3 : 250)
6
+ CATEGORIES = ['Upcoming reminder', 'When poll starts', 'Reminder', 'When poll ends']
7
+
8
+ UPCOMING_REMINDERS = {
9
+ '1 hour before' => 1.hours.to_i,
10
+ '3 hours before' => 3.hours.to_i,
11
+ '6 hours before' => 6.hours.to_i,
12
+ '12 hours before' => 12.hours.to_i,
13
+ '1 day before' => 1.days.to_i,
14
+ '2 days before' => 2.days.to_i,
15
+ '3 days before' => 3.days.to_i,
16
+ '4 days before' => 4.days.to_i,
17
+ '5 days before' => 5.days.to_i,
18
+ '6 days before' => 6.days.to_i,
19
+ '1 week before' => 1.weeks.to_i,
20
+ '2 weeks before' => 2.weeks.to_i,
21
+ '3 weeks before' => 3.weeks.to_i,
22
+ '1 month before' => 1.month.to_i
23
+ }
24
+
25
+ REMINDERS = {
26
+ '1 hour after' => 1.hours.to_i,
27
+ '3 hours after' => 3.hours.to_i,
28
+ '6 hours after' => 6.hours.to_i,
29
+ '12 hours after' => 12.hours.to_i,
30
+ '1 day after' => 1.days.to_i,
31
+ '2 days after' => 2.days.to_i,
32
+ '3 days after' => 3.days.to_i,
33
+ '4 days after' => 4.days.to_i,
34
+ '5 days after' => 5.days.to_i,
35
+ '6 days after' => 6.days.to_i,
36
+ '1 week after' => 1.weeks.to_i,
37
+ '2 weeks after' => 2.weeks.to_i,
38
+ '3 weeks after' => 3.weeks.to_i,
39
+ '1 month after' => 1.month.to_i
40
+ }
41
+
42
+ effective_resource do
43
+ category :string
44
+ reminder :integer # Number of seconds before poll.start_at
45
+
46
+ # Email
47
+ subject :string
48
+ body :text
49
+
50
+ # Tracking background jobs email send out
51
+ started_at :datetime
52
+ completed_at :datetime
53
+
54
+ timestamps
55
+ end
56
+
57
+ scope :sorted, -> { order(:id) }
58
+ scope :deep, -> { includes(:poll) }
59
+
60
+ scope :started, -> { where.not(started_at: nil) }
61
+ scope :completed, -> { where.not(completed_at: nil) }
62
+
63
+ # Called by the poll_notifier rake task
64
+ scope :notifiable, -> { where(started_at: nil, completed_at: nil) }
65
+
66
+ validates :poll, presence: true
67
+ validates :category, presence: true, inclusion: { in: CATEGORIES }
68
+ validates :subject, presence: true
69
+ validates :body, presence: true
70
+
71
+ validates :reminder, if: -> { reminder? || upcoming_reminder? },
72
+ presence: true, uniqueness: { scope: [:poll_id, :category], message: 'already exists' }
73
+
74
+ def to_s
75
+ 'poll notification'
76
+ end
77
+
78
+ def upcoming_reminder?
79
+ category == 'Upcoming reminder'
80
+ end
81
+
82
+ def poll_start?
83
+ category == 'When poll starts'
84
+ end
85
+
86
+ def reminder?
87
+ category == 'Reminder'
88
+ end
89
+
90
+ def poll_end?
91
+ category == 'When poll ends'
92
+ end
93
+
94
+ def notifiable?
95
+ started_at.blank? && completed_at.blank?
96
+ end
97
+
98
+ def notify_now?
99
+ return false unless notifiable?
100
+
101
+ case category
102
+ when 'When poll starts'
103
+ poll.available?
104
+ when 'When poll ends'
105
+ poll.ended?
106
+ when 'Upcoming reminder'
107
+ !poll.started? && poll.start_at < (Time.zone.now + reminder)
108
+ when 'Reminder'
109
+ !poll.ended? && poll.start_at < (Time.zone.now - reminder)
110
+ else
111
+ raise('unexpected category')
112
+ end
113
+ end
114
+
115
+ def notify!
116
+ return false unless notify_now?
117
+
118
+ # We send to all email addresses, except for the 'Reminder' that exclude completed users
119
+ emails = poll.emails(exclude_completed: (category == 'Reminder'))
120
+
121
+ update_column(:started_at, Time.zone.now)
122
+
123
+ emails.in_groups_of(BATCHSIZE, false).each do |emails|
124
+ mail = case category
125
+ when 'When poll starts'
126
+ Effective::PollsMailer.poll_start(self, emails)
127
+ when 'When poll ends'
128
+ Effective::PollsMailer.poll_end(self, emails)
129
+ when 'Upcoming reminder'
130
+ Effective::PollsMailer.poll_upcoming_reminder(self, emails)
131
+ when 'Reminder'
132
+ Effective::PollsMailer.poll_reminder(self, emails)
133
+ else
134
+ raise('unexpected category')
135
+ end
136
+
137
+ mail.deliver_now
138
+ end
139
+
140
+ update_column(:completed_at, Time.zone.now)
141
+ end
142
+
143
+ end
144
+ end
@@ -0,0 +1,76 @@
1
+ module Effective
2
+ class PollQuestion < ActiveRecord::Base
3
+ belongs_to :poll
4
+
5
+ has_many :poll_question_options, -> { order(:position) }, inverse_of: :poll_question, dependent: :delete_all
6
+ accepts_nested_attributes_for :poll_question_options, reject_if: :all_blank, allow_destroy: true
7
+
8
+ has_rich_text :body
9
+
10
+ CATEGORIES = [
11
+ 'Choose one', # Radios
12
+ 'Select all that apply', # Checks
13
+ 'Select upto 1',
14
+ 'Select upto 2',
15
+ 'Select upto 3',
16
+ 'Select upto 4',
17
+ 'Select upto 5',
18
+ 'Date', # Date Field
19
+ 'Email', # Email Field
20
+ 'Number', # Numeric Field
21
+ 'Long Answer', # Text Area
22
+ 'Short Answer', # Text Field
23
+ 'Upload File' # File field
24
+ ]
25
+
26
+ WITH_OPTIONS_CATEGORIES = [
27
+ 'Choose one', # Radios
28
+ 'Select all that apply', # Checks
29
+ 'Select upto 1',
30
+ 'Select upto 2',
31
+ 'Select upto 3',
32
+ 'Select upto 4',
33
+ 'Select upto 5'
34
+ ]
35
+
36
+ effective_resource do
37
+ title :string
38
+ category :string
39
+ required :boolean
40
+
41
+ position :integer
42
+
43
+ timestamps
44
+ end
45
+
46
+ before_validation(if: -> { poll.present? }) do
47
+ self.position ||= (poll.poll_questions.map { |obj| obj.position }.compact.max || -1) + 1
48
+ end
49
+
50
+ scope :deep, -> { with_rich_text_body_and_embeds.includes(:poll_question_options) }
51
+ scope :sorted, -> { order(:position) }
52
+
53
+ validates :title, presence: true
54
+ validates :category, presence: true, inclusion: { in: CATEGORIES }
55
+ validates :position, presence: true
56
+ validates :poll_question_options, presence: true, if: -> { poll_question_option? }
57
+
58
+ # Create choose_one? and select_all_that_apply? methods for each category
59
+ CATEGORIES.each do |category|
60
+ define_method(category.parameterize.underscore + '?') { self.category == category }
61
+ end
62
+
63
+ def to_s
64
+ title.presence || 'New Poll Question'
65
+ end
66
+
67
+ def poll_question_option?
68
+ WITH_OPTIONS_CATEGORIES.include?(category)
69
+ end
70
+
71
+ def category_partial
72
+ category.to_s.parameterize.underscore
73
+ end
74
+
75
+ end
76
+ end
@@ -0,0 +1,26 @@
1
+ module Effective
2
+ class PollQuestionOption < ActiveRecord::Base
3
+ belongs_to :poll_question
4
+
5
+ effective_resource do
6
+ title :text
7
+ position :integer
8
+
9
+ timestamps
10
+ end
11
+
12
+ before_validation(if: -> { poll_question.present? }) do
13
+ self.position ||= (poll_question.poll_question_options.map { |obj| obj.position }.compact.max || -1) + 1
14
+ end
15
+
16
+ scope :sorted, -> { order(:position) }
17
+
18
+ validates :title, presence: true
19
+ validates :position, presence: true
20
+
21
+ def to_s
22
+ title.presence || 'New Poll Question Option'
23
+ end
24
+
25
+ end
26
+ end
@@ -0,0 +1,55 @@
1
+ = effective_form_with(model: [:admin, poll_notification], engine: true) do |f|
2
+ - if inline_datatable?
3
+ = f.hidden_field :poll_id
4
+ - else
5
+ = f.select :poll_id, Effective::Poll.sorted.editable.all
6
+
7
+ = f.select :category, Effective::PollNotification::CATEGORIES,
8
+ label: 'Send an email notification'
9
+
10
+ = f.show_if :category, 'Upcoming reminder' do
11
+
12
+ = f.select :reminder, Effective::PollNotification::UPCOMING_REMINDERS, label: false,
13
+ hint: "before the poll starts on #{f.object.poll&.start_at&.strftime('%F')}."
14
+
15
+ .alert.alert-warning
16
+ %strong Upcoming Reminders
17
+ are sent to the entire audience before the poll starts.
18
+ %br
19
+ Use upcoming reminders to advertise, prepare, or communicate to your audience before the poll begins.
20
+
21
+ = f.show_if :category, 'When poll starts' do
22
+ .alert.alert-warning
23
+ %strong When poll starts
24
+ notifications will be sent within 10 minutes of the poll start time.
25
+
26
+ = f.show_if :category, 'Reminder' do
27
+
28
+ = f.select :reminder, Effective::PollNotification::REMINDERS, label: false,
29
+ hint: "after the poll starts on #{f.object.poll&.start_at&.strftime('%F')}."
30
+
31
+ .alert.alert-warning
32
+ %strong Reminders
33
+ are sent to users in the audience who have not yet completed their ballot.
34
+ %br
35
+ Use reminders to boost your audience participation and remind people to vote.
36
+
37
+ = f.show_if :category, 'When poll ends' do
38
+ .alert.alert-warning
39
+ %strong When poll ends
40
+ notifications will be sent within 10 minutes of the poll end time.
41
+
42
+ = f.text_field :subject, label: 'Email Subject'
43
+ = f.text_area :body, label: 'Email Body'
44
+
45
+ .alert.alert-warning
46
+ Email content must not contain any personal details.
47
+ %br
48
+ This content will be sent in a mass email with hundreds of bcc'd recipients.
49
+
50
+ - if poll_notification.poll.present?
51
+ %br
52
+ The url for this poll will be:
53
+ = effective_polls.poll_url(poll_notification.poll)
54
+
55
+ = effective_submit(f)
@@ -0,0 +1,100 @@
1
+ = effective_form_with(model: [:admin, poll_question], engine: true) do |f|
2
+ - if inline_datatable?
3
+ = f.hidden_field :poll_id
4
+ - else
5
+ = f.select :poll_id, Effective::Poll.sorted.editable.all
6
+
7
+ = f.text_field :title, label: 'Question Title'
8
+ = f.rich_text_area :body, label: 'Body (optional)'
9
+
10
+ = f.check_box :required, hint: 'A response to this question will be required'
11
+ = f.select :category, Effective::PollQuestion::CATEGORIES
12
+
13
+ = f.show_if :category, 'Choose one' do
14
+ .card
15
+ .card-body
16
+ %h5 Choose one
17
+ %p Display radio buttons to choose one option
18
+
19
+ = f.show_if :category, 'Select all that apply' do
20
+ .card
21
+ .card-body
22
+ %h5 Select all that apply
23
+ %p Display checkboxes to select all options that apply
24
+
25
+ = f.show_if :category, 'Select upto 1' do
26
+ .card
27
+ .card-body
28
+ %h5 Select upto 1 (one)
29
+ %p Display checkboxes to select upto 1 option
30
+
31
+ = f.show_if :category, 'Select upto 2' do
32
+ .card
33
+ .card-body
34
+ %h5 Select upto 2 (two)
35
+ %p Display checkboxes to select upto 2 options
36
+
37
+ = f.show_if :category, 'Select upto 3' do
38
+ .card
39
+ .card-body
40
+ %h5 Select upto 3 (three)
41
+ %p Display checkboxes to select upto 3 options
42
+
43
+ = f.show_if :category, 'Select upto 4' do
44
+ .card
45
+ .card-body
46
+ %h5 Select upto 4 (four)
47
+ %p Display checkboxes to select upto 4 options
48
+
49
+ = f.show_if :category, 'Select upto 5' do
50
+ .card
51
+ .card-body
52
+ %h5 Select upto 5 (five)
53
+ %p Display checkboxes to select upto 5 options
54
+
55
+ = f.show_if :category, 'Short Answer' do
56
+ .card
57
+ .card-body
58
+ %h5 Short Answer
59
+ %p Display a text field to enter a short text answer
60
+
61
+ = f.show_if :category, 'Long Answer' do
62
+ .card
63
+ .card-body
64
+ %h5 Long Answer
65
+ %p Display a textarea to enter a long text answer
66
+
67
+ = f.show_if :category, 'Date' do
68
+ .card
69
+ .card-body
70
+ %h5 Date
71
+ %p Display a date field to enter a date
72
+
73
+ = f.show_if :category, 'Email' do
74
+ .card
75
+ .card-body
76
+ %h5 Email
77
+ %p Display an email field to enter an email
78
+
79
+ = f.show_if :category, 'Number' do
80
+ .card
81
+ .card-body
82
+ %h5 Number
83
+ %p Display a number field to enter an integer number
84
+
85
+ = f.show_if :category, 'Upload File' do
86
+ .card
87
+ .card-body
88
+ %h5 Upload File
89
+ %p Display a file field to upload a file
90
+
91
+ = f.show_if_any :category, Effective::PollQuestion::WITH_OPTIONS_CATEGORIES do
92
+ .mt-3.card
93
+ .card-body
94
+ %h5 Options
95
+ %p Display the following options:
96
+
97
+ = f.has_many :poll_question_options, class: 'tight' do |fa|
98
+ = fa.text_field :title, label: false
99
+
100
+ = effective_submit(f)
@@ -0,0 +1,20 @@
1
+ = tabs do
2
+ = tab 'Poll' do
3
+ = render 'admin/polls/form_poll', poll: poll
4
+
5
+ - if poll.persisted?
6
+ = tab 'Content' do
7
+ = render 'admin/polls/form_content', poll: poll
8
+
9
+ = tab 'Questions' do
10
+ - datatable = Admin::EffectivePollQuestionsDatatable.new(poll_id: poll.id)
11
+ = render_datatable(datatable, inline: true, simple: true)
12
+
13
+ = tab 'Notifications' do
14
+ %p
15
+ The following email notification will be sent to all
16
+ = pluralize(poll.users.count, 'users')
17
+ in the audience.
18
+
19
+ - datatable = Admin::EffectivePollNotificationsDatatable.new(poll_id: poll.id)
20
+ = render_datatable(datatable, inline: true, simple: true)
@@ -0,0 +1,27 @@
1
+ = effective_form_with(model: [:admin, poll], engine: true) do |f|
2
+ .card.mb-4
3
+ .card-body
4
+ %h5.card-title All Steps Content
5
+ = f.rich_text_area :all_steps_content, label: false, hint: 'displayed on all ballot steps'
6
+
7
+ .card.mb-4
8
+ .card-body
9
+ %h5.card-title Start Step
10
+ = f.rich_text_area :start_content, label: false, hint: 'displayed on the start step only'
11
+
12
+ .card.mb-4
13
+ .card-body
14
+ %h5.card-title Vote Step
15
+ = f.rich_text_area :vote_content, label: false, hint: 'displayed on the vote step only'
16
+
17
+ .card.mb-4
18
+ .card-body
19
+ %h5.card-title Review and Submit Step
20
+ = f.rich_text_area :submit_content, label: false, hint: 'displayed on the review and submit step only'
21
+
22
+ .card.mb-4
23
+ .card-body
24
+ %h5.card-title Complete Step
25
+ = f.rich_text_area :complete_content, label: false, hint: 'displayed on the complete step only'
26
+
27
+ = f.submit