• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

sleede / fab-manager / #106

pending completion
#106

push

coveralls-ruby

sylvainbx
Merge branch 'dev' for release 6.0.0

704 of 704 new or added lines in 168 files covered. (100.0%)

7919 of 13474 relevant lines covered (58.77%)

15.29 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

75.89
/app/models/availability.rb
1
# frozen_string_literal: true
2

3
# Availability stores time slots that are available to reservation for an associated reservable
4
# Eg. a 3D printer will be reservable on thursday from 9 to 11 pm
5
# Availabilities may be subdivided into Slots (of 1h), for some types of reservables (eg. Machine)
6
class Availability < ApplicationRecord
1✔
7
  # elastic initialisations
8
  include Elasticsearch::Model
1✔
9
  index_name 'fablab'
1✔
10
  document_type 'availabilities'
1✔
11

12
  has_many :machines_availabilities, dependent: :destroy
1✔
13
  has_many :machines, through: :machines_availabilities
1✔
14
  accepts_nested_attributes_for :machines, allow_destroy: true
1✔
15

16
  has_many :trainings_availabilities, dependent: :destroy
1✔
17
  has_many :trainings, through: :trainings_availabilities
1✔
18

19
  has_many :spaces_availabilities, dependent: :destroy
1✔
20
  has_many :spaces, through: :spaces_availabilities
1✔
21

22
  has_many :slots, dependent: :destroy
1✔
23
  has_many :slots_reservations, through: :slots
1✔
24
  has_many :reservations, through: :slots
1✔
25

26
  has_one :event, dependent: :destroy
1✔
27

28
  has_many :availability_tags, dependent: :destroy
1✔
29
  has_many :tags, through: :availability_tags
1✔
30
  accepts_nested_attributes_for :tags, allow_destroy: true
1✔
31

32
  has_many :plans_availabilities, dependent: :destroy
1✔
33
  has_many :plans, through: :plans_availabilities
1✔
34
  accepts_nested_attributes_for :plans, allow_destroy: true
1✔
35

36
  scope :machines, -> { where(available_type: 'machines') }
1✔
37
  scope :trainings, -> { includes(:trainings).where(available_type: 'training') }
1✔
38
  scope :spaces, -> { includes(:spaces).where(available_type: 'space') }
1✔
39

40
  validates :start_at, :end_at, presence: true
1✔
41
  validate :length_must_be_slot_multiple, unless: proc { end_at.blank? or start_at.blank? }
42✔
42
  validate :should_be_associated
1✔
43

44
  # cache
45
  after_update :refresh_places_cache
1✔
46

47
  ## elastic callbacks
48
  after_save { AvailabilityIndexerWorker.perform_async(:index, id) }
40✔
49
  after_destroy { AvailabilityIndexerWorker.perform_async(:delete, id) }
8✔
50

51
  # elastic mapping
52
  settings index: { number_of_replicas: 0 } do
1✔
53
    mappings dynamic: 'true' do
1✔
54
      indexes 'available_type', analyzer: 'simple'
1✔
55
      indexes 'subType', index: 'not_analyzed'
1✔
56
    end
57
  end
58

59
  def safe_destroy
1✔
60
    case available_type
6✔
61
    when 'machines'
62
      reservations = find_reservations('Machine', machine_ids)
4✔
63
    when 'training'
64
      reservations = find_reservations('Training', training_ids)
2✔
65
    when 'space'
66
      reservations = find_reservations('Space', space_ids)
×
67
    when 'event'
68
      reservations = find_reservations('Event', [event&.id])
×
69
    else
70
      Rails.logger.warn "[safe_destroy] Availability with unknown type #{available_type}"
×
71
      reservations = []
×
72
    end
73
    if reservations.size.zero?
6✔
74
      # this update may not call any rails callbacks, that's why we use direct SQL update
75
      update_column(:destroying, true) # rubocop:disable Rails/SkipsModelValidations
6✔
76
      destroy
6✔
77
    else
78
      false
×
79
    end
80
  end
81

82
  # @param reservable_type [String]
83
  # @param reservable_ids [Array<Integer>]
84
  def find_reservations(reservable_type, reservable_ids)
1✔
85
    Reservation.where(reservable_type: reservable_type, reservable_id: reservable_ids)
6✔
86
               .joins(:slots)
87
               .where(slots: { availability_id: id })
88
  end
89

90
  ## compute the total number of places over the whole space availability
91
  def available_space_places
1✔
92
    return unless available_type == 'space'
×
93

94
    duration = slot_duration || Setting.get('slot_duration').to_i
×
95
    ((end_at - start_at) / duration.minutes).to_i * nb_total_places
×
96
  end
97

98
  def title(filter = {})
1✔
99
    case available_type
25✔
100
    when 'machines'
101
      return machines.to_ary.delete_if { |m| filter[:machine_ids].exclude?(m.id) }.map(&:name).join(' - ') if filter[:machine_ids]
32✔
102

103
      machines.map(&:name).join(' - ')
12✔
104
    when 'event'
105
      event.name
1✔
106
    when 'training'
107
      trainings.map(&:name).join(' - ')
3✔
108
    when 'space'
109
      spaces.map(&:name).join(' - ')
5✔
110
    else
111
      Rails.logger.warn "[title] Availability with unknown type #{available_type}"
×
112
      '???'
×
113
    end
114
  end
115

116
  # @return [Array<Integer>]
117
  def available_ids
1✔
118
    case available_type
72✔
119
    when 'training'
120
      training_ids
10✔
121
    when 'machines'
122
      machine_ids
49✔
123
    when 'event'
124
      [event&.id]
9✔
125
    when 'space'
126
      space_ids
4✔
127
    else
128
      []
×
129
    end
130
  end
131

132
  # check if the reservations are complete?
133
  # if a nb_total_places hasn't been defined, then places are unlimited
134
  # @return [Boolean]
135
  def full?
1✔
136
    return false if nb_total_places.blank? && available_type != 'machines'
18✔
137

138
    if available_type == 'event'
14✔
139
      event.nb_free_places.zero?
×
140
    else
141
      slots.map(&:full?).reduce(:&)
14✔
142
    end
143
  end
144

145
  # @return [Array<Integer>] Collection of User's IDs
146
  def reserved_users
1✔
147
    slots.map(&:reserved_users).flatten
×
148
  end
149

150
  # @param user_id [Integer]
151
  # @return [Boolean]
152
  def reserved_by?(user_id)
1✔
153
    reserved_users.include?(user_id)
×
154
  end
155

156
  def reserved?
1✔
157
    slots.map(&:reserved?).reduce(:&)
18✔
158
  end
159

160
  # check availability don't have any reservation
161
  def empty?
1✔
162
    slots.map(&:empty?).reduce(:&)
2✔
163
  end
164

165
  def available_places_per_slot(reservable = nil)
1✔
166
    case available_type
303✔
167
    when 'training'
168
      nb_total_places || reservable&.nb_total_places || trainings.map(&:nb_total_places).max
45✔
169
    when 'event'
170
      event.nb_total_places
9✔
171
    when 'space'
172
      nb_total_places || reservable&.default_places || spaces.map(&:default_places).max
23✔
173
    when 'machines'
174
      reservable.nil? ? machines.count : 1
226✔
175
    else
176
      raise TypeError, "unknown available type #{available_type} for availability #{id}"
×
177
    end
178
  end
179

180
  # the resulting JSON will be indexed in ElasticSearch, as /fablab/availabilities
181
  def as_indexed_json
1✔
182
    json = JSON.parse(to_json)
×
183
    json['hours_duration'] = (end_at - start_at) / (60 * 60)
×
184
    json['subType'] = case available_type
×
185
                      when 'machines'
186
                        machines_availabilities.map { |ma| ma.machine.friendly_id }
×
187
                      when 'training'
188
                        trainings_availabilities.map { |ta| ta.training.friendly_id }
×
189
                      when 'event'
190
                        [event.category.friendly_id]
×
191
                      when 'space'
192
                        spaces_availabilities.map { |sa| sa.space.friendly_id }
×
193
                      else
194
                        []
×
195
                      end
196
    json['bookable_hours'] = json['hours_duration'] * json['subType'].length
×
197
    json['date'] = start_at.to_date
×
198
    json.to_json
×
199
  end
200

201
  private
1✔
202

203
  def length_must_be_slot_multiple
1✔
204
    return unless available_type == 'machines' || available_type == 'space'
41✔
205

206
    duration = slot_duration || Setting.get('slot_duration').to_i
16✔
207
    return unless end_at < (start_at + duration.minutes)
16✔
208

209
    errors.add(:end_at, I18n.t('availabilities.length_must_be_slot_multiple', **{ MIN: duration }))
×
210
  end
211

212
  def should_be_associated
1✔
213
    return unless available_type == 'machines' && machine_ids.count.zero?
41✔
214

215
    errors.add(:machine_ids, I18n.t('availabilities.must_be_associated_with_at_least_1_machine'))
1✔
216
  end
217

218
  def refresh_places_cache
1✔
219
    slots.each do |slot|
10✔
220
      Slots::PlacesCacheService.refresh(slot)
9✔
221
    end
222
  end
223
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc