-
Notifications
You must be signed in to change notification settings - Fork 105
/
event.rb
485 lines (391 loc) · 15.5 KB
/
event.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
# frozen_string_literal: true
# Copyright (c) 2012-2022, Jungwacht Blauring Schweiz. This file is part of
# hitobito and licensed under the Affero General Public License version 3
# or later. See the COPYING file at the top-level directory or at
# https://github.com/hitobito/hitobito.
# == Schema Information
#
# Table name: events
#
# id :integer not null, primary key
# applicant_count :integer default(0)
# application_closing_at :date
# application_conditions :text(65535)
# application_opening_at :date
# applications_cancelable :boolean default(FALSE), not null
# cost :string(255)
# description :text(65535)
# display_booking_info :boolean default(TRUE), not null
# external_applications :boolean default(FALSE)
# globally_visible :boolean
# hidden_contact_attrs :text(65535)
# location :text(65535)
# maximum_participants :integer
# minimum_participants :integer
# motto :string(255)
# name :string(255)
# notify_contact_on_participations :boolean default(FALSE), not null
# number :string(255)
# participant_count :integer default(0)
# participations_visible :boolean default(FALSE), not null
# priorization :boolean default(FALSE), not null
# required_contact_attrs :text(65535)
# requires_approval :boolean default(FALSE), not null
# shared_access_token :string(255)
# signature :boolean
# signature_confirmation :boolean
# signature_confirmation_text :string(255)
# state :string(60)
# teamer_count :integer default(0)
# training_days :decimal(5, 2)
# type :string(255)
# waiting_list :boolean default(TRUE), not null
# created_at :datetime
# updated_at :datetime
# application_contact_id :integer
# contact_id :integer
# creator_id :integer
# kind_id :integer
# updater_id :integer
#
# Indexes
#
# index_events_on_kind_id (kind_id)
# index_events_on_shared_access_token (shared_access_token)
#
# An event is any single or multi-day event that has participants and a
# leader-team. This could be anything from an internal team-meeting to a
# externally available information event for interested people.
#
# The same event may be attached to multiple groups of the same kind.
class Event < ActiveRecord::Base # rubocop:disable Metrics/ClassLength:
# This statement is required because these classes would not be loaded correctly otherwise.
# The price we pay for using classes as namespace.
require_dependency 'event/date'
require_dependency 'event/role'
require_dependency 'event/restricted_role'
require_dependency 'event/application_decorator'
require_dependency 'event/role_decorator'
require_dependency 'event/role_ability'
include Event::Participatable
include Globalized
translates :application_conditions, :description, :name, :signature_confirmation_text
### ATTRIBUTES
class_attribute :used_attributes,
:role_types,
:supports_applications,
:possible_states,
:kind_class,
:supports_invitations,
:uses_form_tabs
# All attributes actually used (and mass-assignable) by the respective STI type.
self.used_attributes = [:name, :motto, :cost, :maximum_participants, :contact_id,
:description, :location, :application_opening_at,
:application_closing_at, :application_conditions,
:external_applications, :applications_cancelable,
:signature, :signature_confirmation, :signature_confirmation_text,
:required_contact_attrs, :hidden_contact_attrs,
:participations_visible, :globally_visible,
:minimum_participants, :automatic_assignment]
# All participation roles that exist for this event
# Customize in wagons using .register_role_type / .disable_role_type
self.role_types = [Event::Role::Leader,
Event::Role::AssistantLeader,
Event::Role::Cook,
Event::Role::Helper,
Event::Role::Treasurer,
Event::Role::Speaker,
Event::Role::Participant]
# Are Event::Applications possible for this event type
self.supports_applications = false
# List of possible values for the state attribute.
self.possible_states = []
# The class used for the kind_id
self.kind_class = nil
# Are Event::Invitations possible for this event type
self.supports_invitations = true
self.uses_form_tabs = true
model_stamper
stampable stamper_class_name: :person,
deleter: false
### ASSOCIATIONS
# Autosave would change updated_at and updater on the group when creating an event.
has_and_belongs_to_many :groups, autosave: false
belongs_to :contact, class_name: 'Person'
has_many :attachments, dependent: :destroy
has_many :dates, -> { order(:start_at) }, dependent: :destroy, validate: true, inverse_of: :event
has_many :questions, dependent: :destroy, validate: true
has_many :application_questions, -> { where(admin: false) },
class_name: 'Event::Question', inverse_of: :event
has_many :admin_questions, -> { where(admin: true) },
class_name: 'Event::Question', inverse_of: :event
has_many :invitations, dependent: :destroy
has_many :participations, dependent: :destroy
has_many :people, through: :participations
has_many :subscriptions, as: :subscriber, dependent: :destroy
has_many :person_add_requests,
foreign_key: :body_id,
inverse_of: :body,
class_name: 'Person::AddRequest::Event',
dependent: :destroy
acts_as_taggable
### VALIDATIONS
validates_by_schema
# name is a translated attribute and thus needs to be validated explicitly
validates :name, presence: true
validates :dates, presence: { message: :must_exist }
validates :contact, permission: :show_full, allow_blank: true, if: :contact_id_changed?
validates :group_ids, presence: { message: :must_exist }
validates :application_opening_at, :application_closing_at,
timeliness: { type: :date, allow_blank: true, before: ::Date.new(9999, 12, 31) }
validates :description, :location, :application_conditions,
length: { allow_nil: true, maximum: 2**16 - 1 }
validate :assert_type_is_allowed_for_groups
validate :assert_application_closing_is_after_opening
validate :assert_required_contact_attrs_valid
validate :assert_hidden_contact_attrs_valid
### CALLBACKS
before_validation :set_self_in_nested
before_validation :set_signature, if: :signature_confirmation?
before_validation :prefill_shared_access_token, unless: :shared_access_token?
accepts_nested_attributes_for :dates, :application_questions, :admin_questions,
allow_destroy: true
### SERIALIZED ATTRIBUTES
serialize :required_contact_attrs, Array
serialize :hidden_contact_attrs, Array
### CLASS METHODS
class << self
# Default scope for event lists
def list
order_by_date.
includes(:translations).
preload_all_dates.
distinct
end
def preload_all_dates
all.extending(Event::PreloadAllDates)
end
def order_by_date
joins(:dates).order('event_dates.start_at')
end
# Events with at least one date in the given year
def in_year(year)
year = Time.zone.today.year if year.to_i <= 0
start_at = Time.zone.parse "#{year}-01-01"
finish_at = start_at + 1.year
joins(:dates).where(event_dates: { start_at: [start_at...finish_at] })
end
# Event with start and end-date overlay
def between(start_date, end_date)
joins(:dates).
where('event_dates.start_at <= :end_date AND event_dates.finish_at >= :start_date ' \
'OR event_dates.start_at <= :end_date AND event_dates.start_at >= :start_date',
start_date: start_date, end_date: end_date).distinct
end
def before_or_on(date)
joins(:dates).where(event_dates: { start_at: ..date.end_of_day })
end
def after_or_on(date)
joins(:dates).where(event_dates: { start_at: date.midnight.. })
end
# Events from groups in the hierarchy of the given user.
def in_hierarchy(user)
with_group_id(user.groups_hierarchy_ids)
end
# Events belonging to the given group ids
def with_group_id(group_ids)
joins(:groups).where(groups: { id: group_ids })
end
# Events running now or in the future.
def upcoming(midnight = Time.zone.now.midnight)
joins(:dates).
where('event_dates.start_at >= ? OR event_dates.finish_at >= ?', midnight, midnight)
end
# Events that are open for applications.
def application_possible
today = Time.zone.today
where('events.application_opening_at IS NULL OR events.application_opening_at <= ?', today).
where('events.application_closing_at IS NULL OR events.application_closing_at >= ?', today).
where('events.maximum_participants IS NULL OR events.maximum_participants <= 0 OR ' \
'events.participant_count < events.maximum_participants')
end
def places_available
where('(COALESCE(maximum_participants, 0) = 0) OR (participant_count < maximum_participants)')
end
# Is the given attribute used in the current STI class
def attr_used?(attr)
used_attributes.include?(attr)
end
def label
model_name.human
end
def label_plural
model_name.human(count: 2)
end
def type_name
self == base_class ? 'simple' : name.demodulize.underscore
end
def all_types
[Event] + Event.subclasses
end
# Return the event type with the given sti_name or raise an exception if not found
def find_event_type!(sti_name)
type = all_types.detect { |t| t.sti_name == sti_name }
raise ActiveRecord::RecordNotFound, "No event type '#{sti_name}' found" if type.nil?
type
end
def participant_types
role_types.select(&:participant?)
end
# Return the role type with the given sti_name or raise an exception if not found
def find_role_type!(sti_name)
type = role_types.detect { |t| t.sti_name == sti_name }
raise ActiveRecord::RecordNotFound, "No role '#{sti_name}' found" if type.nil?
type
end
# Used by wagons to register additional event roles
def register_role_type(type)
ensure_role_type!(type)
self.role_types << type
end
# Used by wagons to register additional event roles
def disable_role_type(type)
ensure_role_type!(type)
self.role_types -= [type]
end
def ensure_role_type!(type)
return if type < Event::Role
raise ArgumentError, "#{type} must be a subclass of Event::Role"
end
def tags
Event.tags_on(:tags).order(:name).pluck(:name)
end
end
### INSTANCE METHODS
delegate :participant_types, :find_role_type!, to: :singleton_class
def to_s(_format = :default)
name
end
def label_detail
"#{number} #{group_names}"
end
def group_names
groups.join(', ')
end
def supports_application_details?
participant_types.present?
end
def application_duration
Duration.new(application_opening_at, application_closing_at)
end
# May participants apply now?
def application_possible?
application_period_open? && (places_available? || waiting_list_available?)
end
def init_questions
# do nothing by default
end
def course?
is_a?(Event::Course)
end
def course_kind?
kind_class == Event::Kind && kind.present?
end
def duplicate # rubocop:disable Metrics/AbcSize,Metrics/MethodLength splitting this up does not make it better
dup.tap do |event|
event.groups = groups
event.state = nil
event.application_opening_at = nil
event.application_closing_at = nil
event.participant_count = 0
event.applicant_count = 0
event.teamer_count = 0
application_questions.each do |q|
event.application_questions << q.dup
end
admin_questions.each do |q|
event.admin_questions << q.dup
end
end
end
def attr_used?(attr)
self.class.used_attributes.include?(attr)
end
def places_available?
maximum_participants.to_i.zero? || participant_count < maximum_participants
end
def waiting_list_available?
self.class.supports_applications && attr_used?(:waiting_list) && waiting_list?
end
def globally_visible
visible = self[:globally_visible]
return Settings.event.globally_visible_by_default if visible.nil?
visible
end
def globally_visible?
!!globally_visible
end
def token_accessible?(token)
return false if shared_access_token.nil? || token.nil?
Devise.secure_compare(token, shared_access_token)
end
private
def application_period_open?
(!application_opening_at? || application_opening_at <= Time.zone.today) &&
(!application_closing_at? || application_closing_at >= Time.zone.today)
end
def assert_type_is_allowed_for_groups # rubocop:disable Metrics/CyclomaticComplexity
master = groups.try(:first)
return unless master
if groups.any? { |g| g.class != master.class }
errors.add(:group_ids, :must_have_same_type)
elsif type && !master.class.event_types.collect(&:sti_name).include?(type)
errors.add(:type, :type_not_allowed)
end
end
def assert_application_closing_is_after_opening
unless application_duration.meaningful?
errors.add(:application_closing_at, :must_be_after_opening)
end
end
def set_self_in_nested
# don't try to set self in frozen nested attributes (-> marked for destroy)
(dates + application_questions + admin_questions).each do |e|
e.event = self unless e.frozen?
end
end
def valid_contact_attr?(attr)
(
ParticipationContactData.contact_attrs +
ParticipationContactData.contact_associations
).map(&:to_s).include?(attr.to_s)
end
def assert_required_contact_attrs_valid # rubocop:disable Metrics/CyclomaticComplexity
required_contact_attrs.map(&:to_s).each do |a|
unless valid_contact_attr?(a) &&
ParticipationContactData.contact_associations.
map(&:to_s).exclude?(a)
errors.add(:base, :contact_attr_invalid, attribute: a)
end
if hidden_contact_attrs.include?(a)
errors.add(:base, :contact_attr_hidden_required, attribute: a)
end
end
end
def assert_hidden_contact_attrs_valid
hidden_contact_attrs.map(&:to_sym).each do |a|
unless valid_contact_attr?(a)
errors.add(:base, :contact_attr_invalid, attribute: a)
end
if ParticipationContactData.mandatory_contact_attrs.include?(a)
errors.add(:base, :contact_attr_mandatory, attribute: a)
end
end
end
def set_signature
self.signature = true
end
def prefill_shared_access_token
self.shared_access_token ||= Devise.friendly_token
end
end