-
Notifications
You must be signed in to change notification settings - Fork 57
Fundamental Rewrite of Validation System #644
Changes from 45 commits
050e5f6
83f1fc5
713774c
fbf0ea5
e6e2cc5
a8e53a7
4ccf7df
cd922e2
2a6ccfd
ead90de
a381359
4d3f10c
79f3311
9cac163
66b271f
6e6669b
3d7e684
50929dc
ef96a84
38f077c
732c089
520db6a
289d331
79a3246
8d1a50c
13a269a
a93b4dc
0fc4537
f81b282
04fe394
da72d24
9822540
68509c6
882c9dd
c97a030
15384b2
5a2bc17
57c31db
c0dfabf
29193f0
c14851c
4719214
13cb8f0
7116b9d
761e8c4
469f641
e614951
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
module CartValidations | ||
|
||
# These validation methods each run several validations within them | ||
# for the sake of speed and query-saving. They are separated in 3 methods | ||
# so that the cart doesn't have to validate items when only the dates are | ||
# changed and vice versa. Each method returns an array of error messages | ||
# | ||
# | ||
# Validate Dates (when dates are changed) | ||
# ## start, end not on a blackout date | ||
# ## user has no overdue (reserver is included in the date form) | ||
# | ||
# Validate Items (when items are added/removed) | ||
# ## user doesn't have too many of each equipment model | ||
# ## or each category | ||
# | ||
# Validate Dates and Items | ||
# ## items are all available for the date range | ||
# ## the duration of the date range is short enough | ||
# ## none of the items should be renewed instead of re-reserved | ||
# | ||
# Validate All | ||
# ## just runs everything | ||
|
||
|
||
def validate_dates | ||
errors = [] | ||
|
||
# blackouts | ||
errors << "A reservation cannot start on #{self.start_date.to_date.strftime('%m/%d')}" if Blackout.hard.for_date(self.start_date).count > 0 | ||
errors << "A reservation cannot end on #{self.due_date.to_date.strftime('%m/%d')}" if Blackout.hard.for_date(self.due_date).count > 0 | ||
|
||
# no overdue reservations | ||
errors << "This user has overdue reservations that prevent him/her from creating new ones" if Reservation.for_reserver(self.reserver_id).overdue.count > 0 | ||
return errors | ||
end | ||
|
||
def validate_items | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Perhaps a more descriptive name: |
||
errors = [] | ||
relevant = Reservation.for_reserver(self.reserver_id).not_returned | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is just an AR Relation? So line 25 then builds on this, chains two more scopes, and counts however many reservations remain? (Sorry for the stupid question, but I've read this code over three times and it only clicked now) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes |
||
category = Hash.new | ||
|
||
# get hash of model objects and quantities | ||
models = self.get_items | ||
|
||
# check max model count for each day in the range | ||
# while simultaneously building a hash of category => quantity | ||
models.each do |model, quantity| | ||
max_models = model.maximum_per_user | ||
self.start_date.to_date.upto(self.due_date.to_date) do |d| | ||
if relevant.overlaps_with_date(d).for_eq_model(model).count + quantity > max_models | ||
errors << "Cannot reserve more than #{max_models} #{model.name.pluralize}" | ||
break | ||
end | ||
end | ||
|
||
if category.include?(model.category) | ||
category[model.category] += quantity | ||
else | ||
category[model.category] = quantity | ||
end | ||
|
||
end | ||
|
||
# similarly check category maxes using a similar method | ||
category.each do |cat, q| | ||
max_cat = cat.maximum_per_user | ||
self.start_date.to_date.upto(self.due_date.to_date) do |d| | ||
count = 0 | ||
relevant.overlaps_with_date(d).each do |r| | ||
|
||
count += 1 if r.equipment_model.category == cat | ||
end | ||
if count + q > max_cat | ||
errors << "Cannot reserve more than #{max_cat} #{cat.name.pluralize}" | ||
break | ||
end | ||
end | ||
end | ||
|
||
return errors | ||
end | ||
|
||
def validate_dates_and_items | ||
user_reservations = Reservation.for_reserver(self.reserver_id).checked_out | ||
errors = [] | ||
models = self.get_items | ||
models.each do |model, quantity| | ||
|
||
# check availability | ||
errors << "That many #{model.name.titleize} is not available for the given time range" if model.num_available(self.start_date, self.due_date) < quantity | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the construct "many [singular noun] is" might come across as a bit odd. Maybe we should pluralize this, on the grounds that the Add To Cart button shouldn't be visible/active if not a single item is available? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good idea There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That seems fine, we could also use the quantity in the error message and pluralize accordingly. |
||
|
||
# check maximum checkout length | ||
max_length = model.category.max_checkout_length | ||
max_length = Float::INFINITY if max_length == 'unrestricted' | ||
errors << "#{model.name.titleize} can only be reserved for #{max_length} days" if self.duration > max_length | ||
|
||
# if a reservation should be renewed instead of checked out | ||
user_reservations.for_eq_model(model).each do |r| | ||
errors << "#{model.name.titleize} should be renewed instead of re-checked out" if r.due_date == self.start_date && r.is_eligible_for_renew? | ||
end | ||
end | ||
return errors | ||
end | ||
|
||
def validate_all | ||
errors = validate_dates | ||
errors.concat(validate_items.to_a).concat(validate_dates_and_items.to_a) | ||
return errors | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,24 +8,11 @@ class Reservation < ActiveRecord::Base | |
belongs_to :checkout_handler, class_name: 'User' | ||
belongs_to :checkin_handler, class_name: 'User' | ||
|
||
validates :equipment_model, presence: true | ||
validates :equipment_model, :start_date, :due_date, presence: true | ||
validate :start_date_before_due_date | ||
validate :matched_object_and_model | ||
validate :not_in_past, :available, on: :create | ||
|
||
# If there is no equipment model, don't run the validations that would break | ||
with_options if: :not_empty? do |r| | ||
r.validate :start_date_before_due_date?, :matched_object_and_model?, | ||
:available? | ||
end | ||
|
||
# These can't be nested with the above block because with_options clobbers | ||
# nested options that are the same (i.e., :if and :if) | ||
with_options if: Proc.new {|r| r.not_empty? && !r.bypass_validations} do |r| | ||
r.with_options on: :create do |r| | ||
r.validate :not_in_past?, :not_renewable?, :no_overdue_reservations?, | ||
:duration_allowed?, :start_date_is_not_blackout?, | ||
:due_date_is_not_blackout?, :quantity_eq_model_allowed?, | ||
:quantity_cat_allowed? | ||
end | ||
end | ||
|
||
nilify_blanks only: [:notes] | ||
|
||
|
@@ -57,10 +44,11 @@ class Reservation < ActiveRecord::Base | |
scope :for_reserver, lambda { |reserver| where(reserver_id: reserver) } | ||
scope :reserved_in_date_range, lambda { |start_date, end_date| | ||
where("start_date < ? and due_date > ? and (approval_status = ? or approval_status = ?)", end_date, start_date, 'auto', 'approved') } | ||
scope :overlaps_with_date, lambda{ |date| where("start_date <= ? and due_date >= ?",date.to_datetime,date.to_datetime) } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This will pick reservations where There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, it selects any reservation where the reservation is 'on' that date. (where the start date is before or on the specified date and the due date is after or on said date) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ohhh. |
||
|
||
#TODO: Why the duplication in checkout_handler and checkout_handler_id (etc)? | ||
attr_accessible :checkout_handler, :checkout_handler_id, | ||
:checkin_handler, :checkin_handler_id, :approval_status, | ||
attr_accessible :checkout_handler_id, | ||
:checkin_handler_id, :approval_status, | ||
:checked_out, :checked_in, :equipment_object, | ||
:equipment_object_id, :notes, :notes_unsent, :times_renewed | ||
|
||
|
@@ -79,6 +67,18 @@ def reserver | |
affiliation: "Deleted") | ||
end | ||
|
||
def validate | ||
# Convert reservation to a cart object and run validations on it | ||
# For hard validations, use reservation.valid | ||
temp_cart = Cart.new | ||
temp_cart.start_date = self.start_date | ||
temp_cart.due_date = self.due_date | ||
temp_cart.reserver_id = self.reserver_id | ||
temp_cart.items = { self.equipment_model.id => 1 } | ||
temp_cart.validate_all | ||
end | ||
|
||
|
||
def status | ||
if checked_out.nil? | ||
if approval_status == 'auto' or approval_status == 'approved' | ||
|
@@ -95,32 +95,6 @@ def status | |
end | ||
end | ||
|
||
## Set validation | ||
# Checks all validations for | ||
# the array of reservations passed in (use with cart.cart_reservations) | ||
# Returns an array of error messages or [] if reservations are all valid | ||
def self.validate_set(user, res_array = []) | ||
errors = [] | ||
#User reservation validations | ||
errors << user.name + " has overdue reservations that prevent new ones from being created. " unless user.reservations.overdue.empty? | ||
|
||
res_array.each do |res| | ||
errors << "Reservation cannot be made in the past. " unless res.not_in_past? | ||
errors << "Reservation start date must be before due date. " unless res.start_date_before_due_date? | ||
errors << "Reservation must be for a piece of equipment. " unless res.not_empty? | ||
errors << "#{res.equipment_object.name} must be of type #{res.equipment_model.name}. " unless res.matched_object_and_model? | ||
errors << "#{res.equipment_model.name} should be renewed instead of re-checked out. " unless res.not_renewable? | ||
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? | ||
errors << "#{res.equipment_model.name} is not available for the full time period requested. " unless res.available?(res_array) | ||
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? | ||
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? | ||
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 | ||
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 | ||
end | ||
errors.uniq | ||
end | ||
|
||
|
||
def checkout_object_uniqueness(reservations) | ||
object_ids_taken = [] | ||
reservations.each do |r| | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I really like that you've been documenting when you're making DB calls, let's try to make that the case across our codebase. It will not only be good for documentation purposes but could also potentially uncover some other inefficiencies.