I have a model with a virtual attribute for a time interval:
attr_accessible :description, :time_end, :time_start, :duration
belongs_to :timesheet
def duration
if attribute_present?("time_start") and attribute_present?("time_end")
ChronicDuration.output(self.time_end - self.time_start)
else
ChronicDuration.output(0)
end
end
def duration=(d)
self.time_end = self.time_start + d
end
However, when creating a new object, Rails tries to set duration before start, leading to an error. How can I make sure that duration is set after start?
error:
undefined method `+' for nil:NilClass
params:
{"utf8"=>"✓",
"authenticity_token"=>"dg+CysIxZORyV3cwvD+LdWckFdHgecGDFDBNOip+iKo=",
"entry"=>{"time_start"=>"now",
"duration"=>"2h",
"description"=>""},
"commit"=>"Create Entry"}
A few things
Worth reading about: and vs && in ruby - http://devblog.avdi.org/2010/08/02/using-and-and-or-in-ruby/
some alternates to using attribute_present? method
# opposite of blank? - http://api.rubyonrails.org/classes/Object.html#method-i-present-3F
if time_start.present? && time_end.present?
# short hand syntax for present?
if time_start? && time_end?
I don't think your problem is with duration being set before time_start, assuming time_start is a datetime or time database type
try this in rails console
entry = Entry.new
entry.time_start = "now"
# => "now"
entry.time_start
# => nil
you are passing strings into time objects and rails / ruby just sets the value to nil.
If time_end and time_start were strings I still don't think your code would give you the result you want?
def duration=(d)
self.time_end = self.time_start + d
end
# params: time_start = "now"
# params: duration = "2h"
# time_end would be: now2h
if I am wrong about duration= running before time_start is set, an alternative would be something like this using a before_save callback
class Entry < ActiveRecord::Base
before_save :set_time_end
attr_accessor :duration
attr_accessible :description, :time_end, :time_start, :duration
belongs_to :timesheet
def duration
if time_start? && time_end?
ChronicDuration.output(self.time_end - self.time_start)
else
ChronicDuration.output(0)
end
end
def set_time_end
return nil if time_start.blank?
self.time_end = self.time_start + self.duration
end
end
1.) Its not clever to name an attribute end because thats a keyword and it might cause some trouble.
2.) Please post your params hash
Related
I have a table column - valid_to, which should represent a date: 30 days from the time the entry was saved into database.
But how can I do such a thing in model?
E.g. in controller I can do such thing this way:
#baby = baby.create(baby_params.
merge( :valid_to => DateTime.current + 30 )
In view I can use hidden field in the form:
<%= f.hidden_field :valid_to => DateTime.current + 30 %>
so is there a way to do such a thing in model? I tried defining self.valid_to before_validation but for no avail: in irb my valid_to column is just nil. To add: I store it as datetime not string.
UPDATE
solution in the end was:
before_validation :set_valid_to, :on => :create
def set_valid_to
self[:valid_to] = 30.days.from_now
end
and lived this thing in module, but that's another story...
The below should work if you only want it done on initial record creation. If you want it updated every time it's saved use before_save instead of before_create.
class Baby < ActiveRecord::Base
before_create :set_valid_to
private
def set_valid_to
self.valid_to = 30.days.from_now
end
end
in irb:
#baby = baby.create
#baby.valid_to = Time.now + 30.days
#baby.save
I have many instances in my application where I use single table inheritance and everything works fine in my development environment. But when I release to production (using passenger) I get the following error:
undefined method `before_save' for InventoryOrder:Class
(NoMethodError)
Why would this work in my dev environment and not work in production? Both are using Rails 4.2 and Ruby 2.1.5. Could this be a problem with passenger?
Here is the InventoryOrder class:
class InventoryOrder < Order
def self.model_name
Order.model_name
end
before_save :ensure_only_feed_types
def ensure_only_feed_types
order_products.each do |op|
if !ProductTypes::is_mix?(op.product_type.type)
raise Exceptions::FailedValidations, _("Can't have an inventory order for anything but mixes")
end
end
end
def self.check_if_replenishment_order_is_needed(product_type_id)
prod_type = ProductType.find(product_type_id)
return if prod_type.nil? || prod_type.min_system_should_have_on_hand.nil? || prod_type.min_system_should_have_on_hand == 0
amount_free = Inventory::inventory_free_for_type(product_type_id)
if prod_type.min_system_should_have_on_hand > amount_free
if prod_type.is_mix?
InventoryOrder::create_replenishment_order(product_type_id, prod_type.min_system_should_have_on_hand - amount_free)
else
OrderMoreNotification.create({subject: "Running low on #{prod_type.name}", body: "Should have #{prod_type.min_system_should_have_on_hand} of unreserved #{prod_type.name} but only #{amount_free} is left"})
end
end
end
def self.create_replenishment_order(product_type_id, amount)
# first check for current inventory orders
orders = InventoryOrder.joins(:order_products).where("order_products.product_type_id = ? and status <> ? and status <> ?", product_type_id, OrderStatuses::ready[:id], OrderStatuses::completed[:id])
amount_in_current_orders = orders.map {|o| o.order_products.map {|op| op.amount }.sum }.sum
amount_left_to_add = amount - amount_in_current_orders
if amount_left_to_add > 0
InventoryOrder.create({pickup_time: 3.days.from_now, location_id: Location::get_default_location.id, order_products: [OrderProduct.new({product_type_id: product_type_id, amount: amount_left_to_add})]})
end
end
def self.create_order_from_cancelled_order_product(order_product)
InventoryOrder.create({
pickup_time: DateTime.now.change({ min: 0, sec: 0 }) + 1.days,
location_id: Location::get_default_location.id,
order_products: [OrderProduct.new({
product_type_id: order_product.product_type_id,
feed_mill_job_id: order_product.feed_mill_job_id,
ration_id: order_product.ration_id,
amount: order_product.amount
})],
description: "Client Order for #{order_product.amount}kg of #{order_product.product_type.name} was cancelled after the feed mill job started."
})
end
end
And here is it's parent class:
class Order < ActiveRecord::Base
#active record concerns
include OrderProcessingInfo
belongs_to :client
belongs_to :location
has_many :order_products
before_destroy :clear_order_products
after_save :after_order_saved
before_save :on_before_save
accepts_nested_attributes_for :order_products, allow_destroy: true
after_initialize :init #used to set default values
validate :client_order_validations
def client_order_validations
if self.type == OrderTypes::client[:id] && self.client_id.nil?
errors.add(:client_id, _("choose a client"))
end
end
...
end
Thanks,
Eric
After doing some more digging and with the help of Roman's comment I was able to figure out that this issue was a result of me using an older convention for ActiveRecord::Concerns that works fine on windows but not on unix based systems.
According to this RailsCasts you can define your concerns like this:
In ../models/concerns/order/order_processing_info.rb
class Order
module OrderProcessingInfo
extend ActiveSupport::Concern
included do
end
...
end
But according to this the right way to define the concern would be to
1) Put it in ../models/concerns/[FILENAMEHERE] instead of ../models/concerns/[CLASSNAMEHERE]/[FILENAMEHERE]
2) Define the module without wrapping it in the class like this:
module OrderProcessingInfo
extend ActiveSupport::Concern
included do
end
end
Took some digging to get to the bottom of it but hopefully this might help someone else out there.
I'm trying to write an app that calculates sick/vacation days and how much an employee has available in either category. Here's my trouble:
In my view, the duration equation works and shows the right numbers, but I've put the math in the view, which I know is bad. But when I try to use the duration equation in my employee class (so I can move the math out of the view) it doesn't work, and I think that it's because duration is saving as 'nil' for some reason. I don't know why it's doing that, as everything else has been saving in the database with whatever information I input into the form.
Maybe it's because duration isn't inputted manually in the form, but rather reacts to the date-range?
Here's where I want to call duration in the employee model to get the math out of the view:
def remaining_vacation_days
vacation_days - #furlough.duration if #furlough.description == "Vacation"
end
def remaining_sick_days
sick_days - #furlough.duration if #furlough.description == "Sick"
end
Here's the model where duration is defined:
class Furlough < ActiveRecord::Base
attr_accessible :duration # and other stuff
belongs_to :employee
validates_presence_of :duration # and other stuff
def duration
only_weekdays(date_from..date_to) - psb_holidays
end
def only_weekdays(range)
range.select { |d| (1..5).include?(d.wday) }.size
end
def psb_holidays
Holidays.between(date_from, date_to, :us, :observed).size
end
end
What's tripping me out is that in the console this is what I see:
1.9.3-p0 :008 > ryan = Furlough.find(18)
Furlough Load (0.3ms) SELECT "furloughs".* FROM "furloughs" WHERE "furloughs"."id" = ? LIMIT 1 [["id", 18]]
=> #<Furlough id: 18, duration: nil, date_from: "2013-12-20", note: "Christmas vacation!", created_at: "2013-05-08 14:33:03", updated_at: "2013-05-08 14:34:07", employee_id: 16, description: "Vacation", date_to: "2013-12-29">
See, it's nil, but then I get this:
1.9.3-p0 :009 > ryan.duration
=> 5
I'm at a loss.
You are supposed to use instance of the class, not the class itself, thats why you are getting all those errors.
def sick_days_used
Furlough.duration if Furlough.description == "Sick"
end
should be :
def sick_days_used
#furlough.duration if #furlough.description == "Sick"
end
or
def sick_days_used
self.duration if self.description == "Sick"
end
if your are defining it in model
The attributes are attributes of a Furlough instance, not the Furlough class itself.
If you are going to use the methods as class methods then you need to add 'self' to the method definition:
def self.duration
...
end
Then you can call Furlough.duration.
The other way around (def duration) you are defining an instance method, which can only be called on an instance (an specifiic Furlogh instance).
** update **
it all seems to be related to a custom validator: if I remove it, it works as expected. see code at the end
**
I have a model budget that has many multi_year_impacts
in the console, if I run:
b = Budget.find(4)
b.multi_year_impacts.size #=> 2
b.update_attributes({multi_year_impacts_attributes: {id: 20, _destroy: true} } ) #=> true
b.multi_year_impacts.size #=> 1 (so far so good)
b.reload
b.multi_year_impacts.size #=> 2 What???
and if before b.reload I do b.save (which shouldn't be needed anyway), it's the same.
Any idea why my child record doesn't get destroyed?
Some additional information, just in case:
Rails 3.2.12
in budget.rb
attr_accessible :multi_year_impacts_attributes
has_many :multi_year_impacts, as: :impactable, :dependent => :destroy
accepts_nested_attributes_for :multi_year_impacts, :allow_destroy => true
validates_with MultiYearImpactValidator # problem seems to com from here
in multi_year_impact.rb
belongs_to :impactable, polymorphic: true
in multi_year_impact_validator.rb
class MultiYearImpactValidator < ActiveModel::Validator
def validate(record)
return false unless record.amount_before && record.amount_after && record.savings
lines = record.multi_year_impacts.delete_if{|x| x.marked_for_destruction?}
%w[amount_before amount_after savings].each do |val|
if lines.inject(0){|s,e| s + e.send(val).to_f} != record.send(val)
record.errors.add(val.to_sym, " please check \"Repartition per year\" below: the sum of all lines must be equal of total amounts")
end
end
end
end
it might depend on your rails version, however, comparing your code to the current docs:
Now, when you add the _destroy key to the attributes hash, with a
value that evaluates to true, you will destroy the associated model:
member.avatar_attributes = { :id => '2', :_destroy => '1' }
member.avatar.marked_for_destruction? # => true
member.save
member.reload.avatar # => nil
Note that the model will not be destroyed until the parent is saved.
you could try with:
b.multi_year_impacts_attributes = {id: 20, _destroy: true}
b.save
So it looks like the culprit was here
if lines.inject(0){|s,e| s + e.send(val).to_f} != record.send(val)
record.errors.add(val.to_sym, " please check \"Repartition per year\" below: the sum of all lines must be equal of total amounts")
end
changing this to the slightly more complex
total = 0
lines.each do |l|
total += l.send(val).to_f unless l.marked_for_destruction?
end
if total != record.send(val)
record.errors[:amount_before] << " please check \"Repartition per year\" below: the sum of all lines must be equal of total amounts"
end
solved the problem.
I have form fields where the user enters in:
percents: 50.5%
money: $144.99
dates: Wednesday, Jan 12th, 2010
...
The percent and money type attributes are saved as decimal fields with ActiveRecord, and the dates are datetime or date fields.
It's easy to convert between formats in javascript, and you could theoretically convert them to the activerecord acceptable format onsubmit, but that's not a decent solution.
I would like to do something override the accessors in ActiveRecord so when they are set it converts them from any string to the appropriate format, but that's not the best either.
What I don't want is to have to run them through a separate processor object, which would require something like this in a controller:
def create
# params == {:product => {:price => "$144.99", :date => "Wednesday, Jan 12, 2011", :percent => "12.9%"}}
formatted_params = Product.format_params(params[:product])
# format_params == {:product => {:price => 144.99, :date => Wed, 12 Jan 2011, :percent => 12.90}}
#product = Product.new(format_params)
#product.save
# ...
end
I would like for it to be completely transparent. Where is the hook in ActiveRecord so I can do this the Rails Way?
Update
I am just doing this for now: https://gist.github.com/727494
class Product < ActiveRecord::Base
format :price, :except => /\$/
end
product = Product.new(:price => "$199.99")
product.price #=> #<BigDecimal:10b001ef8,'0.19999E3',18(18)>
You could override the setter or getter.
Overriding the setter:
class Product < ActiveRecord::Base
def price=(price)
self[:price] = price.to_s.gsub(/[^0-9\.]/, '')
end
end
Overriding the getter:
class Product < ActiveRecord::Base
def price
self[:price].to_s.gsub(/[^0-9\.]/, ''))
end
end
The difference is that the latter method still stores anything the user entered, but retrieves it formatted, while the first one, stores the formatted version.
These methods will be used when you call Product.new(...) or update_attributes, etc...
You can use a before validation hook to normalize out your params such as before_validation
class Product < ActiveRecord::Base
before_validation :format_params
.....
def format_params
self.price = price.gsub(/[^0-9\.]/, "")
....
end
Use monetize gem for parsing numbers.
Example
Monetize.parse(val).amount