effective_events 0.19.2 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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 -68
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8e67fe64e6e9affe8d6961f6bd8d6feea7f2795ee0f11087af84f896fe53457e
4
- data.tar.gz: e43145e482ec697303b2aa0b6e0224f588339134b24259e8ee8c49f1c664e8a4
3
+ metadata.gz: eaa9c21f0fef7ddb63d6ed3eead8cef3e567d56adca7086a450edd80f17c52d9
4
+ data.tar.gz: '05839b258bb697ee0320b4137c307ae89cc1c930e5d1a684f12dcf21f686e748'
5
5
  SHA512:
6
- metadata.gz: b6cd3b5b07d8215f1e29ce434bfa19210d6ab8aa0ebfe8df4cf8a749b7732f76d97d1fb8863aa62a97853888c5915e7c76780624f4a2589edcc2efbe55858ea3
7
- data.tar.gz: 84e7ba3b6ebc7dda5da0d751142acce2cc7dc95e93cf994a01dd427608d43516be81d5debac947b7fc975f8369ec6970e0982adbfa6d6661f2a6ce5bc3cd72af
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)