effective_polls 0.0.1

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 (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