Skip to content
This repository was archived by the owner on Jul 24, 2020. It is now read-only.

Commit 9b3674c

Browse files
committed
Merge pull request #644 from YaleSTC/573_validations_lol
Fundamental Rewrite of Validation System
2 parents aacd293 + e614951 commit 9b3674c

21 files changed

+568
-683
lines changed

app/controllers/application_controller.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ def update_cart
131131
end
132132

133133
# validate
134-
errors = Reservation.validate_set(cart.reserver, cart.prepare_all)
134+
errors = cart.validate_dates.concat(cart.validate_dates_and_items)
135135
# don't over-write flash if invalid date was set above
136136
flash[:error] ||= errors.to_sentence
137137
flash[:notice] = "Cart updated."

app/controllers/catalog_controller.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ def search
5656
# (or displays the appropriate errors)
5757
def change_cart(action, item)
5858
cart.send(action, item)
59-
errors = Reservation.validate_set(cart.reserver, cart.prepare_all)
59+
errors = cart.validate_items.concat(cart.validate_dates_and_items)
6060
flash[:error] = errors.to_sentence
6161
flash[:notice] = "Cart updated."
6262

app/controllers/reservations_controller.rb

+3-7
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ def new
4949
redirect_to catalog_path
5050
else
5151
# error handling
52-
@errors = Reservation.validate_set(cart.reserver, cart.prepare_all)
52+
@errors = cart.validate_all
5353
unless @errors.empty?
5454
if can? :override, :reservation_errors
5555
flash[:error] = 'Are you sure you want to continue? Please review the errors below.'
@@ -69,7 +69,7 @@ def create
6969
Reservation.transaction do
7070
begin
7171
cart_reservations = cart.prepare_all
72-
@errors = Reservation.validate_set(cart.reserver, cart_reservations)
72+
@errors = cart.validate_all
7373
if @errors.empty?
7474
# If the reservation is a finalized reservation, save it as auto-approved ...
7575
params[:reservation][:approval_status] = "auto"
@@ -87,8 +87,6 @@ def create
8787
cart_reservations.each do |cart_res|
8888
@reservation = Reservation.new(params[:reservation])
8989
@reservation.equipment_model = cart_res.equipment_model
90-
# TODO: is this line needed? it's ugly. we should refactor if it's necessary.
91-
@reservation.bypass_validations = true
9290
@reservation.save!
9391
successful_reservations << @reservation
9492
end
@@ -381,10 +379,8 @@ def renew
381379

382380
def review
383381
set_reservation
384-
array_for_validation = []
385-
array_for_validation << @reservation
386382
@all_current_requests_by_user = @reservation.reserver.reservations.requested.delete_if{|res| res.id == @reservation.id}
387-
@errors = Reservation.validate_set(@reservation.reserver, array_for_validation)
383+
@errors = @reservation.validate
388384
end
389385

390386
def approve_request

app/models/blackout.rb

+6-2
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,12 @@ class Blackout < ActiveRecord::Base
1414
validate :validate_end_date_before_start_date
1515
# this only matters if a user tries to inject into params because the datepicker
1616
# doesn't allow form submission of invalid dates
17-
18-
scope :active, where("end_date >= ?", Date.today)
17+
18+
scope :active, where("end_date >= ?", Date.today)
19+
scope :for_date, lambda { |date| where("end_date >= ? and start_date <= ?", date, date) }
20+
scope :hard, where(blackout_type: 'hard')
21+
scope :soft, where(blackout_type: 'notice only')
22+
1923
def self.blackouts_on_date(date) # Returns the blackout object that blacks out the day if the day is blacked out. Otherwise, returns nil.
2024
blackouts = []
2125
Blackout.all.each do |blackout|

app/models/cart.rb

+13-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
class Cart
22
include ActiveModel::Validations
3+
include CartValidations
34
validates :reserver_id, :start_date, :due_date, presence: true
45

56
attr_accessor :items, :start_date, :due_date, :reserver_id
@@ -18,17 +19,27 @@ def persisted?
1819

1920
## Item methods
2021

22+
def get_items
23+
# Used in cart_validations
24+
# Return items where the key is the full equipment model object
25+
# uses 1 database call
26+
full_hash = Hash.new
27+
EquipmentModel.find(self.items.keys).each do |em|
28+
full_hash[em] = self.items[em.id]
29+
end
30+
full_hash
31+
end
2132
# Adds equipment model id to items hash
2233
def add_item(equipment_model)
2334
return if equipment_model.nil?
24-
key = equipment_model.id.to_s
35+
key = equipment_model.id
2536
self.items[key] = self.items[key] ? self.items[key] + 1 : 1
2637
end
2738

2839
# Remove equipment model id from items hash, or decrement its count
2940
def remove_item(equipment_model)
3041
return if equipment_model.nil?
31-
key = equipment_model.id.to_s
42+
key = equipment_model.id
3243
self.items[key] = self.items[key] ? self.items[key] - 1 : 0
3344
self.items = self.items.except(key) if self.items[key] <= 0
3445
end

app/models/cart_validations.rb

+111
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
module CartValidations
2+
3+
# These validation methods each run several validations within them
4+
# for the sake of speed and query-saving. They are separated in 3 methods
5+
# so that the cart doesn't have to validate items when only the dates are
6+
# changed and vice versa. Each method returns an array of error messages
7+
#
8+
#
9+
# Validate Dates (when dates are changed)
10+
# ## start, end not on a blackout date
11+
# ## user has no overdue (reserver is included in the date form)
12+
#
13+
# Validate Items (when items are added/removed)
14+
# ## user doesn't have too many of each equipment model
15+
# ## or each category
16+
#
17+
# Validate Dates and Items
18+
# ## items are all available for the date range
19+
# ## the duration of the date range is short enough
20+
# ## none of the items should be renewed instead of re-reserved
21+
#
22+
# Validate All
23+
# ## just runs everything
24+
25+
26+
def validate_dates
27+
errors = []
28+
29+
# blackouts
30+
errors << "A reservation cannot start on #{self.start_date.to_date.strftime('%m/%d')}" if Blackout.hard.for_date(self.start_date).count > 0
31+
errors << "A reservation cannot end on #{self.due_date.to_date.strftime('%m/%d')}" if Blackout.hard.for_date(self.due_date).count > 0
32+
33+
# no overdue reservations
34+
errors << "This user has overdue reservations that prevent him/her from creating new ones" if Reservation.for_reserver(self.reserver_id).overdue.count > 0
35+
return errors
36+
end
37+
38+
def validate_items
39+
errors = []
40+
relevant = Reservation.for_reserver(self.reserver_id).not_returned
41+
category = Hash.new
42+
43+
# get hash of model objects and quantities
44+
models = self.get_items
45+
46+
# check max model count for each day in the range
47+
# while simultaneously building a hash of category => quantity
48+
models.each do |model, quantity|
49+
max_models = model.maximum_per_user
50+
self.start_date.to_date.upto(self.due_date.to_date) do |d|
51+
if relevant.overlaps_with_date(d).for_eq_model(model).count + quantity > max_models
52+
errors << "Cannot reserve more than #{max_models} #{model.name.pluralize}"
53+
break
54+
end
55+
end
56+
57+
if category.include?(model.category)
58+
category[model.category] += quantity
59+
else
60+
category[model.category] = quantity
61+
end
62+
63+
end
64+
65+
# similarly check category maxes using a similar method
66+
category.each do |cat, q|
67+
max_cat = cat.maximum_per_user
68+
self.start_date.to_date.upto(self.due_date.to_date) do |d|
69+
count = 0
70+
relevant.overlaps_with_date(d).each do |r|
71+
72+
count += 1 if r.equipment_model.category == cat
73+
end
74+
if count + q > max_cat
75+
errors << "Cannot reserve more than #{max_cat} #{cat.name.pluralize}"
76+
break
77+
end
78+
end
79+
end
80+
81+
return errors
82+
end
83+
84+
def validate_dates_and_items
85+
user_reservations = Reservation.for_reserver(self.reserver_id).checked_out
86+
errors = []
87+
models = self.get_items
88+
models.each do |model, quantity|
89+
90+
# check availability
91+
errors << "That many #{model.name.pluralize} is not available for the given time range" if model.num_available(self.start_date, self.due_date) < quantity
92+
93+
# check maximum checkout length
94+
max_length = model.category.max_checkout_length
95+
max_length = Float::INFINITY if max_length == 'unrestricted'
96+
errors << "#{model.name.titleize} can only be reserved for #{max_length} days" if self.duration > max_length
97+
98+
# if a reservation should be renewed instead of checked out
99+
user_reservations.for_eq_model(model).each do |r|
100+
errors << "#{model.name.titleize} should be renewed instead of re-checked out" if r.due_date == self.start_date && r.is_eligible_for_renew?
101+
end
102+
end
103+
return errors
104+
end
105+
106+
def validate_all
107+
errors = validate_dates
108+
errors.concat(validate_items.to_a).concat(validate_dates_and_items.to_a)
109+
return errors
110+
end
111+
end

app/models/equipment_model.rb

+2-6
Original file line numberDiff line numberDiff line change
@@ -169,15 +169,11 @@ def num_available(start_date, due_date)
169169
not_returned
170170
num_available_from_source(start_date, due_date, relevant_reservations)
171171
end
172+
172173
# Returns true if the reserver is ineligible to checkout the model.
173174
def model_restricted?(reserver_id)
174175
reserver = User.find(reserver_id)
175-
reqs = reserver.requirements
176-
return false if self.requirements.blank?
177-
self.requirements.each do |em_req|
178-
return false if reqs.include?(em_req)
179-
end
180-
return true
176+
!(self.requirements - reserver.requirements).empty?
181177
end
182178

183179
# Returns the number of overdue objects for a given model,

app/models/reservation.rb

+19-45
Original file line numberDiff line numberDiff line change
@@ -8,24 +8,11 @@ class Reservation < ActiveRecord::Base
88
belongs_to :checkout_handler, class_name: 'User'
99
belongs_to :checkin_handler, class_name: 'User'
1010

11-
validates :equipment_model, presence: true
11+
validates :equipment_model, :start_date, :due_date, presence: true
12+
validate :start_date_before_due_date
13+
validate :matched_object_and_model
14+
validate :not_in_past, :available, on: :create
1215

13-
# If there is no equipment model, don't run the validations that would break
14-
with_options if: :not_empty? do |r|
15-
r.validate :start_date_before_due_date?, :matched_object_and_model?,
16-
:available?
17-
end
18-
19-
# These can't be nested with the above block because with_options clobbers
20-
# nested options that are the same (i.e., :if and :if)
21-
with_options if: Proc.new {|r| r.not_empty? && !r.bypass_validations} do |r|
22-
r.with_options on: :create do |r|
23-
r.validate :not_in_past?, :not_renewable?, :no_overdue_reservations?,
24-
:duration_allowed?, :start_date_is_not_blackout?,
25-
:due_date_is_not_blackout?, :quantity_eq_model_allowed?,
26-
:quantity_cat_allowed?
27-
end
28-
end
2916

3017
nilify_blanks only: [:notes]
3118

@@ -57,10 +44,11 @@ class Reservation < ActiveRecord::Base
5744
scope :for_reserver, lambda { |reserver| where(reserver_id: reserver) }
5845
scope :reserved_in_date_range, lambda { |start_date, end_date|
5946
where("start_date < ? and due_date > ? and (approval_status = ? or approval_status = ?)", end_date, start_date, 'auto', 'approved') }
47+
scope :overlaps_with_date, lambda{ |date| where("start_date <= ? and due_date >= ?",date.to_datetime,date.to_datetime) }
6048

6149
#TODO: Why the duplication in checkout_handler and checkout_handler_id (etc)?
62-
attr_accessible :checkout_handler, :checkout_handler_id,
63-
:checkin_handler, :checkin_handler_id, :approval_status,
50+
attr_accessible :checkout_handler_id,
51+
:checkin_handler_id, :approval_status,
6452
:checked_out, :checked_in, :equipment_object,
6553
:equipment_object_id, :notes, :notes_unsent, :times_renewed
6654

@@ -79,6 +67,18 @@ def reserver
7967
affiliation: "Deleted")
8068
end
8169

70+
def validate
71+
# Convert reservation to a cart object and run validations on it
72+
# For hard validations, use reservation.valid
73+
temp_cart = Cart.new
74+
temp_cart.start_date = self.start_date
75+
temp_cart.due_date = self.due_date
76+
temp_cart.reserver_id = self.reserver_id
77+
temp_cart.items = { self.equipment_model.id => 1 }
78+
temp_cart.validate_all
79+
end
80+
81+
8282
def status
8383
if checked_out.nil?
8484
if approval_status == 'auto' or approval_status == 'approved'
@@ -95,32 +95,6 @@ def status
9595
end
9696
end
9797

98-
## Set validation
99-
# Checks all validations for
100-
# the array of reservations passed in (use with cart.cart_reservations)
101-
# Returns an array of error messages or [] if reservations are all valid
102-
def self.validate_set(user, res_array = [])
103-
errors = []
104-
#User reservation validations
105-
errors << user.name + " has overdue reservations that prevent new ones from being created. " unless user.reservations.overdue.empty?
106-
107-
res_array.each do |res|
108-
errors << "Reservation cannot be made in the past. " unless res.not_in_past?
109-
errors << "Reservation start date must be before due date. " unless res.start_date_before_due_date?
110-
errors << "Reservation must be for a piece of equipment. " unless res.not_empty?
111-
errors << "#{res.equipment_object.name} must be of type #{res.equipment_model.name}. " unless res.matched_object_and_model?
112-
errors << "#{res.equipment_model.name} should be renewed instead of re-checked out. " unless res.not_renewable?
113-
errors << "#{res.equipment_model.name} cannot be reserved for more than #{res.equipment_model.category.maximum_checkout_length.to_s} days at a time. " unless res.duration_allowed?
114-
errors << "#{res.equipment_model.name} is not available for the full time period requested. " unless res.available?(res_array)
115-
errors << "A reservation cannot start on #{res.start_date.strftime('%m/%d')} because equipment cannot be picked up on that date. " unless res.start_date_is_not_blackout?
116-
errors << "A reservation cannot end on #{res.due_date.strftime('%m/%d')} because equipment cannot be returned on that date. " unless res.due_date_is_not_blackout?
117-
errors << "Cannot reserve more than #{res.equipment_model.maximum_per_user.to_s} #{res.equipment_model.name.pluralize}. " unless res.quantity_eq_model_allowed? res_array
118-
errors << "Cannot reserve more than #{res.equipment_model.category.maximum_per_user.to_s} #{res.equipment_model.category.name.pluralize}. " unless res.quantity_cat_allowed? res_array
119-
end
120-
errors.uniq
121-
end
122-
123-
12498
def checkout_object_uniqueness(reservations)
12599
object_ids_taken = []
126100
reservations.each do |r|

0 commit comments

Comments
 (0)