effective_events 0.19.1 → 0.20.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/effective_events/base.js +32 -0
  3. data/app/controllers/effective/event_registrants_select2_ajax_controller.rb +61 -0
  4. data/app/controllers/effective/event_registrations_controller.rb +17 -1
  5. data/app/datatables/effective_event_registrants_datatable.rb +11 -11
  6. data/app/datatables/effective_event_registrations_datatable.rb +2 -1
  7. data/app/helpers/effective_events_helper.rb +2 -19
  8. data/app/mailers/effective/events_mailer.rb +1 -1
  9. data/app/models/concerns/effective_events_event_registration.rb +141 -51
  10. data/app/models/effective/event.rb +32 -12
  11. data/app/models/effective/event_notification.rb +1 -1
  12. data/app/models/effective/event_registrant.rb +171 -45
  13. data/app/models/effective/event_ticket.rb +20 -17
  14. data/app/models/effective/event_ticket_selection.rb +28 -0
  15. data/app/views/admin/event_registrants/_form.html.haml +16 -3
  16. data/app/views/admin/event_tickets/_form.html.haml +1 -1
  17. data/app/views/effective/event_registrants/_fields.html.haml +47 -28
  18. data/app/views/effective/event_registrations/_details.html.haml +1 -0
  19. data/app/views/effective/event_registrations/_fields_event_registrants.html.haml +26 -0
  20. data/app/views/effective/event_registrations/_fields_event_ticket_selections.html.haml +85 -0
  21. data/app/views/effective/event_registrations/_form_blank_registrants.html.haml +12 -14
  22. data/app/views/effective/event_registrations/_layout.html.haml +9 -1
  23. data/app/views/effective/event_registrations/billing.html.haml +1 -1
  24. data/app/views/effective/event_registrations/details.html.haml +10 -0
  25. data/app/views/effective/event_registrations/summary.html.haml +2 -2
  26. data/app/views/effective/event_registrations/tickets.html.haml +3 -13
  27. data/app/views/effective/events/show.html.haml +34 -34
  28. data/config/effective_events.rb +11 -4
  29. data/config/routes.rb +5 -0
  30. data/db/migrate/101_create_effective_events.rb +16 -1
  31. data/lib/effective_events/version.rb +1 -1
  32. data/lib/effective_events.rb +6 -4
  33. metadata +8 -7
  34. data/app/views/effective/event_registrants/_fields_member_only.html.haml +0 -10
  35. data/app/views/effective/event_registrants/_fields_member_or_non_member.html.haml +0 -34
  36. data/app/views/effective/event_registrants/_fields_questions.html.haml +0 -8
  37. data/app/views/effective/event_registrants/_fields_regular.html.haml +0 -8
  38. data/app/views/effective/event_registrations/_event_tickets.html.haml +0 -63
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b5b3feac5b9f393375a9e93df6bed8aaa3db611ced3dcb8dfa9a2a585ec6cfb5
4
- data.tar.gz: 7e071a9e0906b0fef5856be8d89b9de74dd201506d8ef740ed8df5b4f92a8d5f
3
+ metadata.gz: eaa9c21f0fef7ddb63d6ed3eead8cef3e567d56adca7086a450edd80f17c52d9
4
+ data.tar.gz: '05839b258bb697ee0320b4137c307ae89cc1c930e5d1a684f12dcf21f686e748'
5
5
  SHA512:
6
- metadata.gz: 9b251b9c7e3334462eb15e80cc208b78ec2173437ad31462e906da59ab027d4140179b6cbf525c5abd4c48be53e3843182a2c4b50f1b5c22acf4876becd600c3
7
- data.tar.gz: 880ce99932a1e32bf8a6f3953aed66b4b98b0ae8af33af19d81e0a927d2cc9f676d39450db8e96f9f47c6f2f5b262e90e967faee49f463f511409d62be0336ad
6
+ metadata.gz: bbaf326950e23dbf0052418436863a79b87628c7dd6cfd79bf57eeb884fb9b6be4a3088b490620a98568c610068048f25844eb9f866bf255cadf2f1d02761bde
7
+ data.tar.gz: 82199ab2e8c26b8f6fbfc4ad3b27f2865b1a64d3ffca1a565d7866c02447a7f41b508a83177ff7fbab6e063774dc732dedd7624e014bc193b7d96a11f3c24658
@@ -0,0 +1,32 @@
1
+ // For the Ticket Details screen
2
+ $(document).on('select2:select', '[data-event-registrant-user-search]', function(event) {
3
+ var data = event.params.data['data'];
4
+ var $form = $(event.currentTarget).closest('.event-registrant-user-fields')
5
+
6
+ // Set the organization_id
7
+ $form.find('input[type="hidden"][name$="[organization_id]"]').val(data.organization_id || '')
8
+ $form.find('input[type="hidden"][name$="[organization_type]"]').val(data.organization_type || '')
9
+
10
+ // Disable everything else
11
+ $form.find('input[name$="[first_name]"]').val(data.first_name || '').prop('disabled', true)
12
+ $form.find('input[name$="[last_name]"]').val(data.last_name || '').prop('disabled', true)
13
+ $form.find('input[name$="[email]"]').val(data.email || '').prop('disabled', true)
14
+
15
+ $form.find('select[name$="[organization_id]"]').val(data.organization_id || '').trigger('change').prop('disabled', true)
16
+ $form.find('input[name$="[company]"]').val(data.company || '').prop('disabled', true)
17
+ });
18
+
19
+ $(document).on('select2:unselect', '[data-event-registrant-user-search]', function(event) {
20
+ var $form = $(event.currentTarget).closest('.event-registrant-user-fields')
21
+
22
+ // Unset the organization_id
23
+ $form.find('input[type="hidden"][name$="[organization_id]"]').val('')
24
+
25
+ // Enable everything else
26
+ $form.find('input[name$="[first_name]"]').val('').prop('disabled', false)
27
+ $form.find('input[name$="[last_name]"]').val('').prop('disabled', false)
28
+ $form.find('input[name$="[email]"]').val('').prop('disabled', false)
29
+
30
+ $form.find('select[name$="[organization_id]"]').val('').trigger('change').prop('disabled', false)
31
+ $form.find('input[name$="[company]"]').val('').prop('disabled', false)
32
+ });
@@ -0,0 +1,61 @@
1
+ module Effective
2
+ class EventRegistrantsSelect2AjaxController < ApplicationController
3
+ before_action(:authenticate_user!) if defined?(Devise)
4
+
5
+ include Effective::Select2AjaxController
6
+
7
+ def users
8
+ authorize! :users, Effective::EventRegistrant
9
+
10
+ with_organizations = current_user.class.try(:effective_memberships_organization_user?)
11
+
12
+ collection = current_user.class.all
13
+ collection = collection.includes(:organizations) if with_organizations
14
+
15
+ respond_with_select2_ajax(collection, skip_authorize: true) do |user|
16
+ data = { first_name: user.first_name, last_name: user.last_name, email: user.email }
17
+
18
+ if with_organizations
19
+ data[:company] = user.organizations.first.try(:to_s)
20
+ data[:organization_id] = user.organizations.first.try(:id)
21
+ data[:organization_type] = user.organizations.first.try(:class).try(:name)
22
+ end
23
+
24
+ {
25
+ id: user.to_param,
26
+ text: to_select2(user, with_organizations),
27
+ data: data
28
+ }
29
+ end
30
+ end
31
+
32
+ def organizations
33
+ raise('expected EffectiveEvents.organization_enabled?') unless EffectiveEvents.organization_enabled?
34
+
35
+ klass = EffectiveMemberships.Organization
36
+ raise('an EffectiveMemberships.Organization is required') unless klass.try(:effective_memberships_organization?)
37
+
38
+ collection = klass.all
39
+
40
+ # Authorize
41
+ EffectiveResources.authorize!(self, :member_organizations, collection.klass)
42
+
43
+ respond_with_select2_ajax(collection, skip_authorize: true) do |organization|
44
+ { id: organization.to_param, text: organization.to_s }
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ def to_select2(resource, with_organizations)
51
+ organizations = Array(resource.try(:organizations)).join(', ') if with_organizations
52
+
53
+ [
54
+ "<span>#{resource}</span>",
55
+ "<small>&lt;#{resource.try(:public_email).presence || resource.email}&gt;</small>",
56
+ ("<small>#{organizations}</small>" if organizations.present?)
57
+ ].compact.join(' ')
58
+ end
59
+
60
+ end
61
+ end
@@ -7,6 +7,7 @@ module Effective
7
7
  include Effective::WizardController
8
8
 
9
9
  before_action :redirect_unless_registerable, only: [:new, :show]
10
+ before_action :expire_ticket_selection_window, only: [:show]
10
11
 
11
12
  resource_scope -> {
12
13
  event = Effective::Event.find(params[:event_id])
@@ -18,12 +19,27 @@ module Effective
18
19
  return if resource.blank?
19
20
  return if resource.was_submitted?
20
21
  return if resource.event.blank?
21
- return if resource.event.registerable?
22
22
  return if resource.submit_order&.deferred?
23
+ return if resource.event.registerable? && !resource.event.sold_out?(except: resource)
23
24
 
24
25
  flash[:danger] = "Your selected event is no longer available for registration. This event registration is no longer available."
25
26
  return redirect_to('/dashboard')
26
27
  end
27
28
 
29
+ def expire_ticket_selection_window
30
+ return if resource.blank?
31
+ return if resource.was_submitted?
32
+ return if resource.event.blank?
33
+ return if resource.selection_not_expired?
34
+
35
+ resource.ticket_selection_expired!
36
+
37
+ flash[:danger] = "Your ticket reservation window has expired. Your tickets are no longer reserved. Please start over."
38
+
39
+ return redirect_to(wizard_path(:start))
40
+ end
41
+
42
+ # TODO: Add better permitted params
43
+
28
44
  end
29
45
  end
@@ -6,8 +6,11 @@ class EffectiveEventRegistrantsDatatable < Effective::Datatable
6
6
 
7
7
  col :name do |er|
8
8
  if er.first_name.present?
9
- email = (er.user.present? ? masked_email(er.user) : er.email)
10
- "#{er.first_name} #{er.last_name}<br><small>#{email}</small>"
9
+ [
10
+ "#{er.first_name} #{er.last_name}",
11
+ ("<small>#{er.organization || er.company}</small>" if er.organization || er.company.present?),
12
+ ("<small>#{er.email}</small>" if er.email.present?)
13
+ ].compact.join('<br>').html_safe
11
14
  elsif er.owner.present?
12
15
  er.owner.to_s + ' - GUEST'
13
16
  else
@@ -18,14 +21,12 @@ class EffectiveEventRegistrantsDatatable < Effective::Datatable
18
21
  col :id, visible: false
19
22
 
20
23
  col :event_ticket, search: :string, label: 'Ticket' do |er|
21
- [
22
- er.event_ticket.to_s,
23
- (content_tag(:span, 'Waitlist', class: 'badge badge-warning') if er.waitlisted_not_promoted?),
24
- (content_tag(:span, 'Archived', class: 'badge badge-warning') if er.event_ticket&.archived?)
25
- ].compact.join('<br>').html_safe
24
+ [er.event_ticket.to_s, er.details.presence].compact.join('<br>').html_safe
26
25
  end
27
26
 
28
27
  col :user, label: 'Member', visible: false
28
+ col :organization, visible: false
29
+
29
30
  col :first_name, visible: false
30
31
  col :last_name, visible: false
31
32
  col :email, visible: false
@@ -35,14 +36,13 @@ class EffectiveEventRegistrantsDatatable < Effective::Datatable
35
36
  col :response2, visible: false
36
37
  col :response3, visible: false
37
38
 
38
- col :details do |registrant|
39
+ col :responses, label: 'Details' do |registrant|
39
40
  [registrant.response1.presence, registrant.response2.presence, registrant.response3.presence].compact.map do |response|
40
41
  content_tag(:div, response)
41
42
  end.join.html_safe
42
43
  end
43
44
 
44
- # This is the non-waitlisted full price
45
- col :event_ticket_price, as: :price, label: 'Price'
45
+ col :price, as: :price
46
46
  col :archived, visible: false
47
47
 
48
48
  # no actions_col
@@ -56,7 +56,7 @@ class EffectiveEventRegistrantsDatatable < Effective::Datatable
56
56
  end
57
57
 
58
58
  if event_registration.present?
59
- scope = scope.where(event_registration_id: event_registration)
59
+ scope = scope.where(event_registration_id: event_registration).sorted
60
60
  end
61
61
 
62
62
  scope
@@ -17,6 +17,7 @@ class EffectiveEventRegistrationsDatatable < Effective::Datatable
17
17
  col :owner, visible: false, search: :string
18
18
  col :status, visible: false
19
19
  col :event_registrants, label: 'Registrants', search: :string
20
+
20
21
  col :event_addons, label: 'Add-ons', search: :string
21
22
  col :orders, action: :show, visible: false, search: :string
22
23
 
@@ -31,7 +32,7 @@ class EffectiveEventRegistrationsDatatable < Effective::Datatable
31
32
  end
32
33
 
33
34
  # Register Again
34
- if er.event.registerable?
35
+ if er.event.registerable? && !er.event.sold_out?
35
36
  url = er.event.external_registration_url.presence || effective_events.new_event_event_registration_path(er.event)
36
37
  dropdown_link_to('Register Again', url)
37
38
  end
@@ -52,6 +52,8 @@ module EffectiveEventsHelper
52
52
 
53
53
  tickets.map do |ticket|
54
54
  title = ticket.to_s
55
+ title = "#{title} (archived)" if ticket.archived?
56
+
55
57
  price = effective_events_ticket_price(event, ticket)
56
58
 
57
59
  label = [title, price].compact.join(' - ')
@@ -81,23 +83,4 @@ module EffectiveEventsHelper
81
83
  end
82
84
  end
83
85
 
84
- def effective_events_event_registrant_user_collection(event_registrant)
85
- raise("expected an Effective::EventRegistrant") unless event_registrant.kind_of?(Effective::EventRegistrant)
86
-
87
- Array(event_registrant.event_registration&.event_ticket_member_users).map do |user|
88
- ["<span>#{user}</span> <small>#{user.email}</small>", user.to_param]
89
- end
90
- end
91
-
92
- def effective_events_event_registrant_user_hint
93
- url = if current_user.class.try(:effective_memberships_organization_user?)
94
- organization = current_user.membership_organizations.first || current_user.organizations.first
95
- effective_memberships.edit_organization_path(organization, anchor: 'tab-representatives') if organization
96
- end
97
-
98
- return if url.blank?
99
-
100
- "Can't find the member you need? <a href='#{url}' target='blank'>Click here</a> to add them to your #{EffectiveResources.etd(EffectiveMemberships.Organization)}."
101
- end
102
-
103
86
  end
@@ -8,7 +8,7 @@ module Effective
8
8
  raise('expected an Effective::EventRegistrant') unless resource.kind_of?(Effective::EventRegistrant)
9
9
 
10
10
  @assigns = assigns_for(resource)
11
- mail(to: resource.member_email, **headers_for(resource, opts))
11
+ mail(to: resource.email, **headers_for(resource, opts))
12
12
  end
13
13
 
14
14
  protected
@@ -15,12 +15,13 @@ module EffectiveEventsEventRegistration
15
15
 
16
16
  module ClassMethods
17
17
  def effective_events_event_registration?; true; end
18
+
19
+ def selection_window
20
+ 30.minutes
21
+ end
18
22
  end
19
23
 
20
24
  included do
21
- # Needs to be first up here. Before the acts_as_purchasable_parent one voids the order
22
- around_destroy :around_destroy_deferred_event_registration, if: -> { submit_order&.deferred? }
23
-
24
25
  acts_as_purchasable_parent
25
26
  acts_as_tokened
26
27
 
@@ -33,6 +34,7 @@ module EffectiveEventsEventRegistration
33
34
  acts_as_wizard(
34
35
  start: 'Start',
35
36
  tickets: 'Tickets',
37
+ details: 'Ticket Details',
36
38
  addons: 'Add-ons',
37
39
  summary: 'Review',
38
40
  billing: 'Billing Address',
@@ -52,10 +54,13 @@ module EffectiveEventsEventRegistration
52
54
  # Effective Namespace
53
55
  belongs_to :event, class_name: 'Effective::Event'
54
56
 
55
- has_many :event_registrants, -> { order(:id) }, class_name: 'Effective::EventRegistrant', inverse_of: :event_registration, dependent: :destroy
57
+ has_many :event_ticket_selections, -> { order(:id) }, class_name: 'Effective::EventTicketSelection', inverse_of: :event_registration, dependent: :destroy
58
+ accepts_nested_attributes_for :event_ticket_selections, reject_if: :all_blank, allow_destroy: true
59
+
60
+ has_many :event_registrants, -> { order(:event_ticket_id, :id) }, class_name: 'Effective::EventRegistrant', inverse_of: :event_registration, dependent: :destroy
56
61
  accepts_nested_attributes_for :event_registrants, reject_if: :all_blank, allow_destroy: true
57
62
 
58
- has_many :event_addons, -> { order(:id) }, class_name: 'Effective::EventAddon', inverse_of: :event_registration, dependent: :destroy
63
+ has_many :event_addons, -> { order(:event_product_id, :id) }, class_name: 'Effective::EventAddon', inverse_of: :event_registration, dependent: :destroy
59
64
  accepts_nested_attributes_for :event_addons, reject_if: :all_blank, allow_destroy: true
60
65
 
61
66
  has_many :orders, -> { order(:id) }, as: :parent, class_name: 'Effective::Order', dependent: :nullify
@@ -83,9 +88,10 @@ module EffectiveEventsEventRegistration
83
88
  timestamps
84
89
  end
85
90
 
86
- scope :deep, -> {
91
+ scope :deep, -> {
87
92
  includes(:owner)
88
93
  .includes(event: [:rich_texts, event_products: :purchased_event_addons, event_tickets: :purchased_event_registrants])
94
+ .includes(event_ticket_selections: [:event_ticket])
89
95
  .includes(event_registrants: [event_ticket: :purchased_event_registrants])
90
96
  .includes(event_addons: [event_product: :purchased_event_addons])
91
97
  }
@@ -106,18 +112,32 @@ module EffectiveEventsEventRegistration
106
112
 
107
113
  # All Steps
108
114
  validate(if: -> { event.present? }) do
109
- self.errors.add(:base, "cannot register for an external registration event") if event.external_registration?
115
+ errors.add(:base, "cannot register for an external registration event") if event.external_registration?
110
116
  end
111
117
 
112
118
  # Tickets Step
113
119
  validate(if: -> { current_step == :tickets }) do
114
- self.errors.add(:event_registrants, "can't be blank") unless present_event_registrants.present?
120
+ if event_ticket_selections.all? { |selection| selection.quantity.to_i == 0 }
121
+ errors.add(:event_ticket_selections, "Please select one or more tickets")
122
+ event_ticket_selections.each { |ets| ets.errors.add(:quantity, "can't be blank") }
123
+ end
115
124
  end
116
125
 
117
126
  # Validate all tickets are available for registration
118
127
  validate(if: -> { current_step == :tickets }) do
119
128
  unavailable_event_tickets.each do |event_ticket|
120
129
  errors.add(:base, "The requested number of #{event_ticket} tickets are not available")
130
+ event_ticket_selections.find { |ets| ets.event_ticket == event_ticket }.errors.add(:quantity, "not available")
131
+ end
132
+ end
133
+
134
+ # Validate the same registrant user isn't being registered twice
135
+ validate(if: -> { current_step == :details }) do
136
+ present_event_registrants.group_by { |er| er.user }.each do |user, event_registrants|
137
+ if user.present? && event_registrants.length > 1
138
+ errors.add(:base, "Unable to register #{user} more than once")
139
+ event_registrants.each { |er| er.errors.add(:user_id, "cannot be registered more than once") }
140
+ end
121
141
  end
122
142
  end
123
143
 
@@ -131,19 +151,6 @@ module EffectiveEventsEventRegistration
131
151
  # If we're submitted. Try to move to completed.
132
152
  before_save(if: -> { submitted? }) { try_completed! }
133
153
 
134
- def around_destroy_deferred_event_registration
135
- raise('expecting a deferred submit order') unless submit_order&.deferred?
136
-
137
- waitlisted_event_tickets_was = event_tickets().select(&:waitlist?)
138
- yield
139
- waitlisted_event_tickets_was.each { |event_ticket| event_ticket.update_waitlist! }
140
- true
141
- end
142
-
143
- def delayed_payment_date_upcoming?
144
- event&.delayed_payment_date_upcoming?
145
- end
146
-
147
154
  def can_visit_step?(step)
148
155
  return false if step == :complete && !completed?
149
156
  return true if step == :complete && completed?
@@ -164,17 +171,24 @@ module EffectiveEventsEventRegistration
164
171
 
165
172
  def required_steps
166
173
  return self.class.test_required_steps if Rails.env.test? && self.class.test_required_steps.present?
167
- event&.event_products.unarchived.present? ? wizard_step_keys : (wizard_step_keys - [:addons])
168
- end
169
174
 
170
- def find_or_build_submit_fees
171
- with_outstanding_coupon_fees(submit_fees)
175
+ with_addons = event.event_products.any? { |event_product| event_product.archived? == false }
176
+
177
+ with_addons ? wizard_step_keys : (wizard_step_keys - [:addons])
172
178
  end
173
179
 
174
180
  def delayed_payment_attributes
175
181
  { delayed_payment: event&.delayed_payment, delayed_payment_date: event&.delayed_payment_date }
176
182
  end
177
183
 
184
+ def delayed_payment_date_upcoming?
185
+ event&.delayed_payment_date_upcoming?
186
+ end
187
+
188
+ def find_or_build_submit_fees
189
+ with_outstanding_coupon_fees(submit_fees)
190
+ end
191
+
178
192
  # All Fees and Orders
179
193
  def submit_fees
180
194
  if defined?(EffectiveMemberships)
@@ -217,7 +231,95 @@ module EffectiveEventsEventRegistration
217
231
  completed?
218
232
  end
219
233
 
234
+ def display_countdown?
235
+ return false if done?
236
+ return false unless selected_at.present?
237
+ return false unless current_step.present?
238
+
239
+ [:start, :tickets, :submitted, :complete].exclude?(current_step)
240
+ end
241
+
242
+ # When we make a ticket selection, we assign the selected_at to all tickets
243
+ # So the max or the min should be the same here.
244
+ def selected_at
245
+ event_registrants.map(&:selected_at).compact.max
246
+ end
247
+
248
+ def selected_expires_at
249
+ selected_at + EffectiveEvents.EventRegistration.selection_window
250
+ end
251
+
252
+ def selected_expired?
253
+ return false if selected_at.blank?
254
+ Time.zone.now >= selected_expires_at
255
+ end
256
+
257
+ def selection_not_expired?
258
+ return true if selected_at.blank?
259
+ Time.zone.now < selected_expires_at
260
+ end
261
+
262
+ # Called by a before_action on the event registration show action
263
+ def ticket_selection_expired!
264
+ raise("unexpected submitted registration") if was_submitted?
265
+ raise("unexpected purchased order") if submit_order&.purchased?
266
+ raise("unexpected deferred order") if submit_order&.deferred?
267
+
268
+ event_registrants.each { |er| er.assign_attributes(selected_at: nil) }
269
+ event_ticket_selections.each { |ets| ets.assign_attributes(quantity: 0) }
270
+ assign_attributes(current_step: nil, wizard_steps: {}) # Reset all steps
271
+
272
+ save!
273
+ end
274
+
275
+ # This considers the event_ticket_selection and builds the appropriate event_registrants
276
+ def update_event_registrants
277
+ event_ticket_selections.each do |event_ticket_selection|
278
+ event_ticket = event_ticket_selection.event_ticket
279
+ quantity = event_ticket_selection.quantity.to_i
280
+
281
+ # All the registrants for this event ticket
282
+ registrants = event_registrants.select { |er| er.event_ticket == event_ticket }
283
+
284
+ # Delete over quantity
285
+ if (diff = registrants.length - quantity) > 0
286
+ registrants.last(diff).each { |er| er.mark_for_destruction }
287
+ end
288
+
289
+ # Create upto quantity
290
+ if (diff = quantity - registrants.length) > 0
291
+ diff.times { build_event_registrant(event_ticket: event_ticket) }
292
+ end
293
+ end
294
+
295
+ event_registrants
296
+ end
297
+
298
+ # Assigns the selected at time to start the reservation window
299
+ def select_event_registrants
300
+ now = Time.zone.now
301
+ present_event_registrants.each { |er| er.assign_attributes(selected_at: now) }
302
+ end
303
+
304
+ # Looks at any unselected event registrants and assigns a waitlist value
305
+ def waitlist_event_registrants
306
+ present_event_registrants.group_by { |er| er.event_ticket }.each do |event_ticket, event_registrants|
307
+ if event_ticket.waitlist?
308
+ capacity = event.capacity_available(event_ticket: event_ticket, event_registration: self)
309
+ event_registrants.each_with_index { |er, index| er.assign_attributes(waitlisted: index >= capacity) }
310
+ else
311
+ event_registrants.each { |er| er.assign_attributes(waitlisted: false) }
312
+ end
313
+ end
314
+ end
315
+
220
316
  def tickets!
317
+ assign_attributes(current_step: :tickets) # Ensure the unavailable tickets validations are run
318
+
319
+ update_event_registrants
320
+ select_event_registrants
321
+ waitlist_event_registrants
322
+
221
323
  after_commit do
222
324
  update_submit_fees_and_order! if submit_order.present?
223
325
  update_deferred_event_registration! if submit_order&.deferred?
@@ -252,27 +354,26 @@ module EffectiveEventsEventRegistration
252
354
  true
253
355
  end
254
356
 
255
- # Find or build
256
- def event_registrant(event_ticket:, first_name:, last_name:, email:)
257
- registrant = event_registrants.find { |er| er.event_ticket == event_ticket && er.first_name == first_name && er.last_name == last_name && er.email == email }
258
- registrant || event_registrants.build(event: event, event_ticket: event_ticket, owner: owner, first_name: first_name, last_name: last_name, email: email)
357
+ def build_event_registrant(event_ticket:)
358
+ event_registrants.build(event: event, event_ticket: event_ticket, owner: owner)
259
359
  end
260
360
 
361
+ # Find or build - # Used for testing
362
+ # def event_registrant(event_ticket:, first_name:, last_name:, email:)
363
+ # registrant = event_registrants.find { |er| er.event_ticket == event_ticket && er.first_name == first_name && er.last_name == last_name && er.email == email }
364
+ # registrant || event_registrants.build(event: event, event_ticket: event_ticket, owner: owner, first_name: first_name, last_name: last_name, email: email)
365
+ # end
366
+
261
367
  # Find or build. But it's not gonna work with more than 1. This is for testing only really.
262
368
  def event_addon(event_product:, first_name:, last_name:, email:)
263
369
  addon = event_addons.find { |er| er.event_product == event_product && er.first_name == first_name && er.last_name == last_name && er.email == email }
264
370
  addon || event_addons.build(event: event, event_product: event_product, owner: owner, first_name: first_name, last_name: last_name, email: email)
265
371
  end
266
372
 
267
- # This builds the default event registrants used by the wizard form
268
- def build_event_registrants
269
- if event_registrants.blank?
270
- raise('expected owner and event to be present') unless owner && event
271
-
272
- event_registrants.build()
273
- end
274
-
275
- event_registrants
373
+ # Find or build
374
+ def event_ticket_selection(event_ticket:, quantity: 0)
375
+ selection = event_ticket_selections.find { |ets| ets.event_ticket == event_ticket }
376
+ selection || event_ticket_selections.build(event_ticket: event_ticket, quantity: quantity)
276
377
  end
277
378
 
278
379
  # This builds the default event addons used by the wizard form
@@ -298,9 +399,7 @@ module EffectiveEventsEventRegistration
298
399
  unavailable = []
299
400
 
300
401
  present_event_registrants.map(&:event_ticket).group_by { |t| t }.each do |event_ticket, event_tickets|
301
- unless event_ticket.waitlist? || event.event_ticket_available?(event_ticket, quantity: event_tickets.length)
302
- unavailable << event_ticket
303
- end
402
+ unavailable << event_ticket unless event.event_ticket_available?(event_ticket, except: self, quantity: event_tickets.length)
304
403
  end
305
404
 
306
405
  unavailable
@@ -316,13 +415,6 @@ module EffectiveEventsEventRegistration
316
415
  unavailable
317
416
  end
318
417
 
319
- def event_ticket_member_users
320
- raise("expected owner to be a user") if owner.class.try(:effective_memberships_organization?)
321
- users = [owner] + (owner.try(:organizations).try(:flat_map, &:users) || [])
322
-
323
- users.select { |user| user.is_any?(:member) }.uniq
324
- end
325
-
326
418
  def update_blank_registrants!
327
419
  # This method is called by the user on a submitted or completed event registration.
328
420
  # Allow them to update blank registrants
@@ -343,6 +435,7 @@ module EffectiveEventsEventRegistration
343
435
  raise('unable to make changes to event addons while updating blank registrants')
344
436
  end
345
437
 
438
+ assign_attributes(current_step: :details) if current_step.blank? # Enables validations
346
439
  save!
347
440
 
348
441
  update_submit_fees_and_order! if submit_order.present? && !submit_order.purchased?
@@ -358,9 +451,6 @@ module EffectiveEventsEventRegistration
358
451
  # Mark registered anyone who hasn't been registered yet. They are now!
359
452
  event_registrants.reject(&:registered?).each { |event_registrant| event_registrant.registered! }
360
453
 
361
- # Update the waitlist for any event tickets
362
- event_tickets.select(&:waitlist?).each { |event_ticket| event_ticket.update_waitlist! }
363
-
364
454
  true
365
455
  end
366
456
 
@@ -15,7 +15,7 @@ module Effective
15
15
  has_many :event_products, -> { EventProduct.sorted }, inverse_of: :event, dependent: :destroy
16
16
  accepts_nested_attributes_for :event_products, allow_destroy: true
17
17
 
18
- has_many :event_registrants, -> { order(:event_ticket_id).order(:id) }, inverse_of: :event
18
+ has_many :event_registrants, -> { order(:event_ticket_id, :id) }, inverse_of: :event
19
19
  accepts_nested_attributes_for :event_registrants, allow_destroy: true
20
20
 
21
21
  has_many :event_addons, -> { order(:event_product_id).order(:id) }, inverse_of: :event
@@ -190,11 +190,10 @@ module Effective
190
190
  event_tickets.any? { |et| et.waitlist? }
191
191
  end
192
192
 
193
+ # No longer includes sold_out? we check that separately
193
194
  def registerable?
194
195
  return false unless published?
195
196
  return false if closed?
196
- return false if sold_out?
197
-
198
197
  (external_registration? && external_registration_url.present?) || event_tickets.present?
199
198
  end
200
199
 
@@ -203,10 +202,13 @@ module Effective
203
202
  registration_end_at < Time.zone.now
204
203
  end
205
204
 
206
- def sold_out?
205
+ def sold_out?(except: nil)
206
+ raise('expected except to be an EventRegistration') if except && !except.class.try(:effective_events_event_registration?)
207
+
207
208
  return false unless event_tickets.present?
208
209
  return false if any_waitlist?
209
- event_tickets.none? { |event_ticket| event_ticket_available?(event_ticket, quantity: 1) }
210
+
211
+ event_tickets.none? { |event_ticket| event_ticket_available?(event_ticket, except: except, quantity: 1) }
210
212
  end
211
213
 
212
214
  def early_bird?
@@ -250,19 +252,37 @@ module Effective
250
252
  start_at
251
253
  end
252
254
 
255
+ # The amount of tickets that can be purchased except ones from an event registration
256
+ def capacity_selectable(event_ticket:, event_registration: nil)
257
+ return 0 if event_ticket.archived?
258
+ return 100 if event_ticket.capacity.blank?
259
+ return 100 if event_ticket.waitlist?
260
+
261
+ event_ticket.capacity_selectable(except: event_registration)
262
+ end
263
+
264
+ # The amount of tickets that can be purchased except ones from an event registration
265
+ def capacity_available(event_ticket:, event_registration: nil)
266
+ event_ticket.capacity_available(except: event_registration)
267
+ end
268
+
269
+ # Just used in tests so far
270
+ def capacity_taken(event_ticket:, event_registration: nil)
271
+ event_ticket.capacity_taken(except: event_registration)
272
+ end
273
+
253
274
  # Can I register/purchase this many new event tickets?
254
- def event_ticket_available?(event_ticket, quantity:)
275
+ def event_ticket_available?(event_ticket, except: nil, quantity: 0)
255
276
  raise('expected an EventTicket') unless event_ticket.kind_of?(Effective::EventTicket)
277
+ raise('expected except to be an EventRegistration') if except && !except.class.try(:effective_events_event_registration?)
256
278
  raise('expected quantity to be greater than 0') unless quantity.to_i > 0
257
279
 
258
280
  return false if event_ticket.archived?
259
- return true if event_ticket.capacity.blank? # No capacity enforced for this ticket
281
+ return true if event_ticket.capacity.blank? # No capacity enforced
282
+ return true if event_ticket.waitlist? # Always available for waitlist
260
283
 
261
- # Total number already sold
262
- registered = registered_event_registrants.count { |r| r.event_ticket_id == event_ticket.id }
263
-
264
- # If there's capacity for this many more
265
- (registered + quantity) <= event_ticket.capacity
284
+ # Do we have any tickets available left?
285
+ event_ticket.capacity_available(except: except) >= quantity.to_i
266
286
  end
267
287
 
268
288
  # Can I register/purchase this many new event products?
@@ -150,7 +150,7 @@ module Effective
150
150
  update_column(:started_at, Time.zone.now)
151
151
 
152
152
  event_registrants.each do |event_registrant|
153
- next if event_registrant.member_email.blank?
153
+ next if event_registrant.email.blank?
154
154
 
155
155
  begin
156
156
  EffectiveEvents.send_email(email_template, event_registrant, email_notification_params)