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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +132 -0
- data/Rakefile +32 -0
- data/app/assets/config/effective_polls_manifest.js +1 -0
- data/app/assets/stylesheets/effective_polls.scss +0 -0
- data/app/controllers/admin/poll_notifications_controller.rb +15 -0
- data/app/controllers/admin/poll_questions_controller.rb +15 -0
- data/app/controllers/admin/polls_controller.rb +23 -0
- data/app/controllers/effective/ballots_controller.rb +70 -0
- data/app/controllers/effective/polls_controller.rb +19 -0
- data/app/datatables/admin/effective_poll_notifications_datatable.rb +43 -0
- data/app/datatables/admin/effective_poll_questions_datatable.rb +28 -0
- data/app/datatables/admin/effective_poll_results_datatable.rb +50 -0
- data/app/datatables/admin/effective_polls_datatable.rb +29 -0
- data/app/datatables/effective_polls_datatable.rb +30 -0
- data/app/helpers/effective_polls_helper.rb +23 -0
- data/app/mailers/effective/polls_mailer.rb +23 -0
- data/app/models/effective/access_denied.rb +17 -0
- data/app/models/effective/ballot.rb +73 -0
- data/app/models/effective/ballot_response.rb +92 -0
- data/app/models/effective/ballot_response_option.rb +6 -0
- data/app/models/effective/poll.rb +149 -0
- data/app/models/effective/poll_notification.rb +144 -0
- data/app/models/effective/poll_question.rb +76 -0
- data/app/models/effective/poll_question_option.rb +26 -0
- data/app/views/admin/poll_notifications/_form.html.haml +55 -0
- data/app/views/admin/poll_questions/_form.html.haml +100 -0
- data/app/views/admin/polls/_form.html.haml +20 -0
- data/app/views/admin/polls/_form_content.html.haml +27 -0
- data/app/views/admin/polls/_form_poll.html.haml +32 -0
- data/app/views/admin/polls/_poll.html.haml +2 -0
- data/app/views/admin/polls/results.html.haml +6 -0
- data/app/views/effective/ballot_responses/_ballot_response.html.haml +2 -0
- data/app/views/effective/ballot_responses/_fields.html.haml +14 -0
- data/app/views/effective/ballot_responses/fields/_choose_one.html.haml +1 -0
- data/app/views/effective/ballot_responses/fields/_date.html.haml +1 -0
- data/app/views/effective/ballot_responses/fields/_email.html.haml +1 -0
- data/app/views/effective/ballot_responses/fields/_long_answer.html.haml +1 -0
- data/app/views/effective/ballot_responses/fields/_number.html.haml +1 -0
- data/app/views/effective/ballot_responses/fields/_select_all_that_apply.html.haml +1 -0
- data/app/views/effective/ballot_responses/fields/_select_upto_1.html.haml +1 -0
- data/app/views/effective/ballot_responses/fields/_select_upto_2.html.haml +1 -0
- data/app/views/effective/ballot_responses/fields/_select_upto_3.html.haml +1 -0
- data/app/views/effective/ballot_responses/fields/_select_upto_4.html.haml +1 -0
- data/app/views/effective/ballot_responses/fields/_select_upto_5.html.haml +1 -0
- data/app/views/effective/ballot_responses/fields/_short_answer.html.haml +1 -0
- data/app/views/effective/ballot_responses/fields/_upload_file.html.haml +1 -0
- data/app/views/effective/ballot_responses/responses/_choose_one.html.haml +1 -0
- data/app/views/effective/ballot_responses/responses/_date.html.haml +1 -0
- data/app/views/effective/ballot_responses/responses/_email.html.haml +1 -0
- data/app/views/effective/ballot_responses/responses/_long_answer.html.haml +1 -0
- data/app/views/effective/ballot_responses/responses/_number.html.haml +1 -0
- data/app/views/effective/ballot_responses/responses/_select_all_that_apply.html.haml +5 -0
- data/app/views/effective/ballot_responses/responses/_select_upto_1.html.haml +1 -0
- data/app/views/effective/ballot_responses/responses/_select_upto_2.html.haml +5 -0
- data/app/views/effective/ballot_responses/responses/_select_upto_3.html.haml +5 -0
- data/app/views/effective/ballot_responses/responses/_select_upto_4.html.haml +5 -0
- data/app/views/effective/ballot_responses/responses/_select_upto_5.html.haml +5 -0
- data/app/views/effective/ballot_responses/responses/_short_answer.html.haml +1 -0
- data/app/views/effective/ballot_responses/responses/_upload_file.html.haml +4 -0
- data/app/views/effective/ballots/_ballot.html.haml +14 -0
- data/app/views/effective/ballots/complete.html.haml +11 -0
- data/app/views/effective/ballots/start.html.haml +17 -0
- data/app/views/effective/ballots/submit.html.haml +17 -0
- data/app/views/effective/ballots/vote.html.haml +19 -0
- data/app/views/effective/poll_results/_poll_result.html.haml +2 -0
- data/app/views/effective/poll_results/_results.html.haml +38 -0
- data/app/views/effective/poll_results/results/_choose_one.html.haml +2 -0
- data/app/views/effective/poll_results/results/_date.html.haml +14 -0
- data/app/views/effective/poll_results/results/_email.html.haml +14 -0
- data/app/views/effective/poll_results/results/_long_answer.html.haml +2 -0
- data/app/views/effective/poll_results/results/_number.html.haml +14 -0
- data/app/views/effective/poll_results/results/_poll_question_option.html.haml +17 -0
- data/app/views/effective/poll_results/results/_select_all_that_apply.html.haml +2 -0
- data/app/views/effective/poll_results/results/_select_upto_1.html.haml +2 -0
- data/app/views/effective/poll_results/results/_select_upto_2.html.haml +2 -0
- data/app/views/effective/poll_results/results/_select_upto_3.html.haml +2 -0
- data/app/views/effective/poll_results/results/_select_upto_4.html.haml +2 -0
- data/app/views/effective/poll_results/results/_select_upto_5.html.haml +2 -0
- data/app/views/effective/poll_results/results/_short_answer.html.haml +2 -0
- data/app/views/effective/poll_results/results/_upload_file.html.haml +3 -0
- data/app/views/effective/polls/_form.html.haml +10 -0
- data/app/views/effective/polls/_poll.html.haml +2 -0
- data/app/views/layouts/effective_polls_mailer_layout.html.haml +7 -0
- data/config/effective_polls.rb +59 -0
- data/config/routes.rb +23 -0
- data/db/migrate/01_create_effective_polls.rb.erb +92 -0
- data/lib/effective_polls.rb +50 -0
- data/lib/effective_polls/engine.rb +11 -0
- data/lib/effective_polls/version.rb +3 -0
- data/lib/generators/effective_polls/install_generator.rb +46 -0
- data/lib/generators/templates/effective_polls_mailer_preview.rb +35 -0
- data/lib/tasks/effective_polls_tasks.rake +24 -0
- metadata +276 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
class Admin::EffectivePollsDatatable < Effective::Datatable
|
|
2
|
+
filters do
|
|
3
|
+
scope :upcoming
|
|
4
|
+
scope :available
|
|
5
|
+
scope :completed
|
|
6
|
+
scope :all
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
datatable do
|
|
10
|
+
order :start_at, :desc
|
|
11
|
+
|
|
12
|
+
col :id, visible: false
|
|
13
|
+
col :created_at, visible: false
|
|
14
|
+
col :updated_at, visible: false
|
|
15
|
+
|
|
16
|
+
col :title
|
|
17
|
+
col :start_at
|
|
18
|
+
col :end_at
|
|
19
|
+
col :audience
|
|
20
|
+
|
|
21
|
+
col :poll_questions, visible: false
|
|
22
|
+
|
|
23
|
+
actions_col
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
collection do
|
|
27
|
+
Effective::Poll.all.deep
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Displays available polls that the current_user may complete
|
|
2
|
+
|
|
3
|
+
class EffectivePollsDatatable < Effective::Datatable
|
|
4
|
+
datatable do
|
|
5
|
+
order :start_at
|
|
6
|
+
|
|
7
|
+
col :start_at, visible: false
|
|
8
|
+
|
|
9
|
+
col :title
|
|
10
|
+
col :available_date, label: 'Date'
|
|
11
|
+
|
|
12
|
+
actions_col(actions: []) do |poll|
|
|
13
|
+
ballot = poll.ballots.where(user: current_user).first
|
|
14
|
+
|
|
15
|
+
if ballot.blank?
|
|
16
|
+
dropdown_link_to('Start', effective_polls.poll_ballot_build_path(poll, :new, :start))
|
|
17
|
+
elsif ballot.completed?
|
|
18
|
+
'Complete'
|
|
19
|
+
else
|
|
20
|
+
dropdown_link_to('Continue', effective_polls.poll_ballot_build_path(poll, ballot, ballot.next_step))
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
collection do
|
|
26
|
+
raise('expected a current_user') unless current_user.present?
|
|
27
|
+
Effective::Poll.available.select { |poll| poll.available_for?(current_user) }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module EffectivePollsHelper
|
|
2
|
+
|
|
3
|
+
# Used by admin/polls form
|
|
4
|
+
def effective_polls_audience_scope_collection
|
|
5
|
+
# Normalize the collection into [[label, value], [label, value]]
|
|
6
|
+
scopes = Array(EffectivePolls.audience_user_scopes).map do |key, value|
|
|
7
|
+
(key.present? && value.present?) ? [key, value] : [key.to_s.titleize, key]
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# Makes sure the User model responds to all values
|
|
11
|
+
scopes.each do |_, scope|
|
|
12
|
+
unless defined?(User) && User.respond_to?(scope)
|
|
13
|
+
raise("invalid effective_polls config.audience_user_scopes value. The user model must respond to the scope User.#{scope}")
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Append the number of users in this scope
|
|
18
|
+
scopes.map! do |label, scope|
|
|
19
|
+
["#{label} (#{pluralize(User.send(scope).count, 'user')})", scope]
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module Effective
|
|
2
|
+
class PollsMailer < ActionMailer::Base
|
|
3
|
+
default from: EffectivePolls.mailer[:default_from]
|
|
4
|
+
layout EffectivePolls.mailer[:layout].presence || 'effective_polls_mailer_layout'
|
|
5
|
+
|
|
6
|
+
def poll_upcoming_reminder(poll_notification, bcc)
|
|
7
|
+
mail(bcc: bcc, body: poll_notification.body, subject: poll_notification.subject)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def poll_start(poll_notification, bcc)
|
|
11
|
+
mail(bcc: bcc, body: poll_notification.body, subject: poll_notification.subject)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def poll_reminder(poll_notification, bcc)
|
|
15
|
+
mail(bcc: bcc, body: poll_notification.body, subject: poll_notification.subject)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def poll_end(poll_notification, bcc)
|
|
19
|
+
mail(bcc: bcc, body: poll_notification.body, subject: poll_notification.subject)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
unless defined?(Effective::AccessDenied)
|
|
2
|
+
module Effective
|
|
3
|
+
class AccessDenied < StandardError
|
|
4
|
+
attr_reader :action, :subject
|
|
5
|
+
|
|
6
|
+
def initialize(message = nil, action = nil, subject = nil)
|
|
7
|
+
@message = message
|
|
8
|
+
@action = action
|
|
9
|
+
@subject = subject
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def to_s
|
|
13
|
+
@message || I18n.t(:'unauthorized.default', :default => 'Access Denied')
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
module Effective
|
|
2
|
+
class Ballot < ActiveRecord::Base
|
|
3
|
+
attr_accessor :current_user
|
|
4
|
+
attr_accessor :current_step
|
|
5
|
+
|
|
6
|
+
belongs_to :poll
|
|
7
|
+
belongs_to :user
|
|
8
|
+
|
|
9
|
+
has_many :ballot_responses, dependent: :destroy
|
|
10
|
+
accepts_nested_attributes_for :ballot_responses
|
|
11
|
+
|
|
12
|
+
acts_as_tokened
|
|
13
|
+
|
|
14
|
+
acts_as_wizard(
|
|
15
|
+
start: 'Start',
|
|
16
|
+
vote: 'Ballot',
|
|
17
|
+
submit: 'Review', # They submit on this step
|
|
18
|
+
complete: 'Complete'
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
effective_resource do
|
|
22
|
+
# Acts as tokened
|
|
23
|
+
token :string, permitted: false
|
|
24
|
+
|
|
25
|
+
# Acts as Wizard
|
|
26
|
+
wizard_steps :text, permitted: false
|
|
27
|
+
|
|
28
|
+
# More fields
|
|
29
|
+
completed_at :datetime, permitted: false
|
|
30
|
+
|
|
31
|
+
current_step permitted: true
|
|
32
|
+
timestamps
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
scope :deep, -> { includes(:poll, :user, ballot_responses: [:poll, :poll_question, :poll_question_options]) }
|
|
36
|
+
scope :sorted, -> { order(:id) }
|
|
37
|
+
scope :completed, -> { where.not(completed_at: nil) }
|
|
38
|
+
|
|
39
|
+
before_validation(if: -> { new_record? }) do
|
|
40
|
+
self.user ||= current_user
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
validates :user_id, uniqueness: {
|
|
44
|
+
scope: :poll_id, allow_blank: true, message: 'ballot already exists for this poll'
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
# I seem to need this even tho I accept_nested_attributes
|
|
48
|
+
validates :ballot_responses, associated: true
|
|
49
|
+
|
|
50
|
+
def to_s
|
|
51
|
+
'ballot'
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Find or build
|
|
55
|
+
def ballot_response(poll_question)
|
|
56
|
+
ballot_response = ballot_responses.find { |br| br.poll_question_id == poll_question.id }
|
|
57
|
+
ballot_response ||= ballot_responses.build(poll: poll_question.poll, poll_question: poll_question)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# This is the review step where they click Submit Ballot
|
|
61
|
+
def submit!
|
|
62
|
+
wizard_steps[:complete] ||= Time.zone.now
|
|
63
|
+
self.completed_at ||= Time.zone.now
|
|
64
|
+
|
|
65
|
+
save!
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def completed?
|
|
69
|
+
completed_at.present?
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
module Effective
|
|
2
|
+
class BallotResponse < ActiveRecord::Base
|
|
3
|
+
belongs_to :ballot
|
|
4
|
+
|
|
5
|
+
belongs_to :poll
|
|
6
|
+
belongs_to :poll_question
|
|
7
|
+
|
|
8
|
+
has_many :ballot_response_options, dependent: :delete_all
|
|
9
|
+
has_many :poll_question_options, through: :ballot_response_options
|
|
10
|
+
|
|
11
|
+
has_one_attached :upload_file
|
|
12
|
+
|
|
13
|
+
effective_resource do
|
|
14
|
+
# The response
|
|
15
|
+
date :date
|
|
16
|
+
email :string
|
|
17
|
+
number :integer
|
|
18
|
+
long_answer :text
|
|
19
|
+
short_answer :text
|
|
20
|
+
|
|
21
|
+
timestamps
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
scope :completed, -> { where(ballot: Effective::Ballot.completed) }
|
|
25
|
+
scope :deep, -> { includes(:ballot, :poll, :poll_question, :poll_question_options) }
|
|
26
|
+
|
|
27
|
+
validates :poll, presence: true
|
|
28
|
+
validates :ballot, presence: true
|
|
29
|
+
validates :poll_question, presence: true
|
|
30
|
+
|
|
31
|
+
# Sanity check. These shouldn't happen.
|
|
32
|
+
validate(if: -> { poll.present? && ballot.present? }) do
|
|
33
|
+
self.errors.add(:ballot, 'must match poll') unless ballot.poll_id == poll.id
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
validate(if: -> { poll.present? && poll_question.present? }) do
|
|
37
|
+
self.errors.add(:poll_question, 'must match poll') unless poll_question.poll_id == poll.id
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
validates :date, presence: true, if: -> { poll_question&.required? && poll_question.date? }
|
|
41
|
+
validates :email, presence: true, email: true, if: -> { poll_question&.required? && poll_question.email? }
|
|
42
|
+
validates :number, presence: true, if: -> { poll_question&.required? && poll_question.number? }
|
|
43
|
+
validates :long_answer, presence: true, if: -> { poll_question&.required? && poll_question.long_answer? }
|
|
44
|
+
validates :short_answer, presence: true, if: -> { poll_question&.required? && poll_question.short_answer? }
|
|
45
|
+
validates :upload_file, presence: true, if: -> { poll_question&.required? && poll_question.upload_file? }
|
|
46
|
+
validates :poll_question_option_ids, presence: true, if: -> { poll_question&.required? && poll_question.poll_question_option? }
|
|
47
|
+
|
|
48
|
+
validates :poll_question_option_ids, if: -> { poll_question&.choose_one? },
|
|
49
|
+
length: { maximum: 1, message: 'please choose 1 option only' }
|
|
50
|
+
|
|
51
|
+
validates :poll_question_option_ids, if: -> { poll_question&.select_upto_1? },
|
|
52
|
+
length: { maximum: 1, message: 'please select 1 option or fewer' }
|
|
53
|
+
|
|
54
|
+
validates :poll_question_option_ids, if: -> { poll_question&.select_upto_2? },
|
|
55
|
+
length: { maximum: 2, message: 'please select 2 options or fewer' }
|
|
56
|
+
|
|
57
|
+
validates :poll_question_option_ids, if: -> { poll_question&.select_upto_3? },
|
|
58
|
+
length: { maximum: 3, message: 'please select 3 options or fewer' }
|
|
59
|
+
|
|
60
|
+
validates :poll_question_option_ids, if: -> { poll_question&.select_upto_4? },
|
|
61
|
+
length: { maximum: 4, message: 'please select 4 options or fewer' }
|
|
62
|
+
|
|
63
|
+
validates :poll_question_option_ids, if: -> { poll_question&.select_upto_5? },
|
|
64
|
+
length: { maximum: 5, message: 'please select 5 options or fewer' }
|
|
65
|
+
|
|
66
|
+
def to_s
|
|
67
|
+
'ballot reponse'
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def response
|
|
71
|
+
return nil unless poll_question.present?
|
|
72
|
+
|
|
73
|
+
return date if poll_question.date?
|
|
74
|
+
return email if poll_question.email?
|
|
75
|
+
return number if poll_question.number?
|
|
76
|
+
return long_answer if poll_question.long_answer?
|
|
77
|
+
return short_answer if poll_question.short_answer?
|
|
78
|
+
return upload_file if poll_question.upload_file?
|
|
79
|
+
|
|
80
|
+
return poll_question_options.first if poll_question.choose_one?
|
|
81
|
+
return poll_question_options.first if poll_question.select_upto_1?
|
|
82
|
+
return poll_question_options if poll_question.poll_question_option?
|
|
83
|
+
|
|
84
|
+
raise('unknown response for unexpected poll question category')
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def category_partial
|
|
88
|
+
poll_question&.category_partial
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
module Effective
|
|
2
|
+
class Poll < ActiveRecord::Base
|
|
3
|
+
attr_accessor :skip_started_validation
|
|
4
|
+
|
|
5
|
+
has_rich_text :all_steps_content
|
|
6
|
+
has_rich_text :start_content
|
|
7
|
+
has_rich_text :vote_content
|
|
8
|
+
has_rich_text :submit_content
|
|
9
|
+
has_rich_text :complete_content
|
|
10
|
+
|
|
11
|
+
has_many :poll_notifications, -> { order(:id) }, inverse_of: :poll, dependent: :destroy
|
|
12
|
+
accepts_nested_attributes_for :poll_notifications, allow_destroy: true
|
|
13
|
+
|
|
14
|
+
has_many :poll_questions, -> { order(:position) }, inverse_of: :poll, dependent: :destroy
|
|
15
|
+
accepts_nested_attributes_for :poll_questions, allow_destroy: true
|
|
16
|
+
|
|
17
|
+
has_many :ballots
|
|
18
|
+
has_many :ballot_responses
|
|
19
|
+
|
|
20
|
+
# For the poll_results screens
|
|
21
|
+
has_many :completed_ballots, -> { Effective::Ballot.completed }, class_name: 'Effective::Ballot'
|
|
22
|
+
has_many :completed_ballot_responses, -> { where(ballot: Effective::Ballot.completed) }, class_name: 'Effective::BallotResponse'
|
|
23
|
+
|
|
24
|
+
acts_as_tokened
|
|
25
|
+
|
|
26
|
+
AUDIENCES = ['All Users', 'Individual Users', 'Selected Users']
|
|
27
|
+
|
|
28
|
+
effective_resource do
|
|
29
|
+
# Acts as tokened
|
|
30
|
+
token :string, permitted: false
|
|
31
|
+
|
|
32
|
+
title :string
|
|
33
|
+
|
|
34
|
+
start_at :datetime
|
|
35
|
+
end_at :datetime
|
|
36
|
+
|
|
37
|
+
audience :string
|
|
38
|
+
audience_scope :text # An Array of user_ids or named scopes on the User model
|
|
39
|
+
|
|
40
|
+
timestamps
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
serialize :audience_scope, Array
|
|
44
|
+
|
|
45
|
+
scope :deep, -> {
|
|
46
|
+
includes(poll_questions: :poll_question_options)
|
|
47
|
+
.with_rich_text_all_steps_content
|
|
48
|
+
.with_rich_text_start_content
|
|
49
|
+
.with_rich_text_vote_content
|
|
50
|
+
.with_rich_text_submit_content
|
|
51
|
+
.with_rich_text_complete_content
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
scope :deep_results, -> {
|
|
55
|
+
includes(poll_questions: :poll_question_options)
|
|
56
|
+
.includes(ballots: [ballot_responses: [:poll, :poll_question, :poll_question_options]])
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
scope :sorted, -> { order(:start_at) }
|
|
60
|
+
scope :editable, -> { upcoming }
|
|
61
|
+
|
|
62
|
+
scope :upcoming, -> { where('start_at > ?', Time.zone.now) }
|
|
63
|
+
scope :available, -> { where('start_at <= ? AND (end_at > ? OR end_at IS NULL)', Time.zone.now, Time.zone.now) }
|
|
64
|
+
scope :completed, -> { where('end_at < ?', Time.zone.now) }
|
|
65
|
+
|
|
66
|
+
validates :title, presence: true
|
|
67
|
+
validates :start_at, presence: true
|
|
68
|
+
|
|
69
|
+
validates :audience, inclusion: { in: AUDIENCES }
|
|
70
|
+
validates :audience_scope, presence: true, unless: -> { audience == 'All Users' }
|
|
71
|
+
|
|
72
|
+
validate(if: -> { started? }, unless: -> { skip_started_validation }) do
|
|
73
|
+
self.errors.add(:base, 'has already started. a poll cannot be changed after it has started.')
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
validate(if: -> { start_at.present? && !started? }, unless: -> { skip_started_validation }) do
|
|
77
|
+
self.errors.add(:start_at, 'must be a future date') if start_at < Time.zone.now
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
validate(if: -> { start_at.present? && end_at.present? }) do
|
|
81
|
+
self.errors.add(:end_at, 'must be after the start date') unless end_at > start_at
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def to_s
|
|
85
|
+
title.presence || 'New Poll'
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def available_for?(user)
|
|
89
|
+
raise('expected a user') unless user.kind_of?(User)
|
|
90
|
+
available? && users.include?(user)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def users
|
|
94
|
+
case audience
|
|
95
|
+
when 'All Users'
|
|
96
|
+
User.all
|
|
97
|
+
when 'Individual Users'
|
|
98
|
+
User.where(id: audience_scope)
|
|
99
|
+
when 'Selected Users'
|
|
100
|
+
collection = User.none
|
|
101
|
+
audience_scope.each { |scope| collection = collection.or(User.send(scope)) }
|
|
102
|
+
collection
|
|
103
|
+
else
|
|
104
|
+
raise('unexpected audience')
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def emails(exclude_completed: false)
|
|
109
|
+
if exclude_completed
|
|
110
|
+
users.where.not(id: completed_ballots.select('user_id as id')).pluck(:email)
|
|
111
|
+
else
|
|
112
|
+
users.pluck(:email)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def available?
|
|
117
|
+
started? && !ended?
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def started?
|
|
121
|
+
start_at_was.present? && Time.zone.now >= start_at_was
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def ended?
|
|
125
|
+
end_at.present? && end_at < Time.zone.now
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def available_date
|
|
129
|
+
if start_at && end_at && start_at.to_date == end_at.to_date
|
|
130
|
+
"#{start_at.strftime('%F at %H:%M')} to #{end_at.strftime('%H:%M')}"
|
|
131
|
+
elsif start_at && end_at
|
|
132
|
+
"#{start_at.strftime('%F at %H:%M')} to #{end_at.strftime('%F %H:%M')}"
|
|
133
|
+
elsif start_at
|
|
134
|
+
"#{start_at.strftime('%F at %H:%M')}"
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def audience_scope
|
|
139
|
+
Array(super) - [nil, '']
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Returns all completed_ballot_responses
|
|
143
|
+
def poll_results(poll_question: nil)
|
|
144
|
+
return completed_ballot_responses if poll_question.nil?
|
|
145
|
+
completed_ballot_responses.select { |br| br.poll_question_id == poll_question.id }
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
end
|
|
149
|
+
end
|