Tracking object changes rails (Active Model Dirty) - ruby-on-rails

I'm trying to track changes on a method just like we are tracking changes on record attributes using the Active Model Dirty.
At the moment I'm using this method to check changes to a set of attributes.
def check_updated_attributes
watch_attrs = ["brand_id", "name", "price", "default_price_tag_id", "terminal_usp", "primary_offer_id", "secondary_offer_id"]
if (self.changed & watch_attrs).any?
self.tag_updated_at = Time.now
end
end
However the attribute price is not a normal attribute with a column but a method defined in the model that combines two different attributes.
This is the method:
def price
if manual_price
price = manual_price
else
price = round_99(base_price)
end
price.to_f
end
Is there a way to track changes on this price method? or does this only work on normal attributes?
Edit: Base price method:
def base_price(rate = Setting.base_margin, mva = Setting.mva)
(cost_price_map / (1 - rate.to_f)) * ( 1 + mva.to_f)
end
cost_price and manual_price are attributes with columns it the terminal table.

Ok, solved it.
I had to create a custom method named price_changed? to check if the price had changed.
def price_changed?
if manual_price
manual_price_changed?
elsif cost_price_map_changed?
round_99(base_price) != round_99(base_price(cost_price = read_attribute(:cost_price_map)))
else
false
end
end
This solved the problem, although not a pretty solution if you have many custom attributes.

Related

DRY way of assigning new object's values from an existing object's values

I created a class method that is called when a new object is created and copied from an old existing object. However, I only want to copy some of the values. Is there some sort of Ruby shorthand I can use to clean this up? It's not entirely necessary, just curious to know if something like this exists?
Here is the method I want to DRY up:
def set_message_settings_from_existing existing
self.can_message = existing.can_message
self.current_format = existing.current_format
self.send_initial_message = existing.send_initial_message
self.send_alert = existing.send_alert
self.location = existing.location
end
Obviously this works perfectly fine, but to me looks a little ugly. Is there any way to clean this up? If I wanted to copy over every value that would be easy enough, but because I only want to copy these 5 (out of 20 something) values, I decided to do it this way.
def set_message_settings_from_existing(existing)
[:can_message, :current_format, :send_initial_message, :send_alert, :location].each do |attribute|
self.send("#{attribute}=", existing.send(attribute))
end
end
Or
def set_message_settings_from_existing(existing)
self.attributes = existing.attributes.slice('can_message', 'current_format', 'send_initial_message', 'send_alert', 'location')
end
a hash might be cleaner:
def set_message_settings_from_existing existing
fields = {
can_message: existing.can_message,
current_format: existing.current_format,
send_initial_message: existing.send_initial_message,
send_alert: existing.send_alert,
location: existing.location
}
self.attributes = fields
end
you can take this further by only selecting the attributes you want:
def set_message_settings_from_existing existing
fields = existing.attributes.slice(
:can_message,
:current_format,
:send_initial_message,
:send_alert,
:location
)
self.attributes = fields
end
at this point you could also have these fields defined somewhere, eg:
SUB_SET_OF_FIELDS = [:can_message, :current_format, :send_initial_message, :send_alert, :location]
and use that for your filter instance.attributes.slice(SUB_SET_OF_FIELDS)

building a simple search form in Rails?

I'm trying to build a simple search form in Ruby on Rails, my form is simple enough basically you select fields from a series of options and then all the events matching the fields are shown. The problem comes when I leave any field blank.
Here is the code responsible for filtering the parameters
Event.joins(:eventdates).joins(:categories).where
("eventdates.start_date = ? AND city = ? AND categories.name = ?",
params[:event][:date], params[:event][:city], params[:event][:category]).all
From what I get it's that it looks for events with any empty field, but since all of them have them not empty, it wont match unless all 3 are filled, another problem arises when I try to say, look events inside a range or array of dates, I'm clueless on how to pass multiple days into the search.
I'm pretty new to making search forms in general, so I don't even know if this is the best approach, also I'm trying to keep the searches without the need of a secialized model.
Below is probably what you are looking for. (Note: If all fields all blank, it shows all data in the events table linkable with eventdates and categories.)
events = Event.joins(:eventdates).joins(:categories)
if params[:event]
# includes below where condition to query only if params[:event][:date] has a value
events = events.where("eventdates.start_date = ?", params[:event][:date]) if params[:event][:date].present?
# includes below where condition to query only if params[:event][:city] has a value
events = events.where("city = ?", params[:event][:city]) if params[:event][:city].present?
# includes below where condition to query only if params[:event][:city] has a value
events = events.where("categories.name = ?", params[:event][:category]) if params[:event][:category].present?
end
To search using multiple days:
# params[:event][:dates] is expected to be array of dates.
# Below query gets converted into an 'IN' operation in SQL, something like "where eventdates.start_date IN ['date1', 'date2']"
events = events.where("eventdates.start_date = ?", params[:event][:dates]) if params[:event][:dates].present?
It will be more easy and optimised . If you use concern for filter data.
Make one concern in Model.
filterable.rb
module Filterable
extend ActiveSupport::Concern
module ClassMethods
def filter(filtering_params)
results = self.where(nil)
filtering_params.each do |key, value|
if column_type(key) == :date || column_type(key) ==
:datetime
results = results.where("DATE(#{column(key)}) = ?",
Date.strptime(value, "%m/%d/%Y")) if
value.present?
else
results = results.where("#{column(key)} Like ? ", "%#{value}%") if
value.present?
end
end
results
end
def resource_name
self.table_name
end
def column(key)
return key if key.split(".").count > 1
return "#{resource_name}.#{key}"
end
def column_type(key)
self.columns_hash[key].type
end
end
end
Include this concern in model file that you want to filter.
Model.rb
include Filterable
In your controller Add this methods
def search
#resources = Model.filter(class_search_params)
render 'index'
end
def class_search_params
params.slice(:id,:name) #Your field names
end
So, It is global solution. You dont need to use query for filter. just add this concern in your model file.
That's it.

Passing from a Controller to a Model

Pretty new to RoR. Wonder if anyone can help me with this issue.
I got a gem called "business_time" which calculates the business days between two dates. I have set up a method in the model which does all the calculations.
I have a field called "credit" which should hold the number of business days. Here's what I have:
MODEL
def self.calculate(from_date,to_date)
days = 0
date_1 = Date.parse(from_date)
date 2 = Date.parse(to_date)
days = date_1.business_days_until(date2)
days
end
CONTROLLER
def new
#vacation = current_user.vacations.build
#vacations = Vacation.calculate(:from_date, :to_date)
end
I got an error referencing something about a string.
Furthermore, how do I go about storing the data from the method into the field called "credit"?
Thanks guys.
I think there is no need for an extra method, since all attributes (from_date, end_date and credit) are stored in the same model.
I would just set from_date and end_date in the initializer and calculate credit with a callback before validation:
# in the model
before_validation :calculate_credit
private
def calculate_credit
if from_date && to_date
# `+ 1` because the user takes off both days (`from_date` and `to_date`),
# but `business_days_until` doesn't count the `from_day`.
self.credit = from_date.business_days_until(to_date) + 1
end
end
# in the controller
def new
#vacation = current_user.vacations.build
end
def create
#vacation = current_user.vacations.build(vacation_params)
if #vacation.save
# #vacation.credit would return the calculated credit at this point
else
# ...
end
end
private
def vacation_params
params.require(:vacation).permit(:from_date, :to_date)
end
What you need here is pass String objects instead of Symbol objects.
So instead of #vacations = Vacation.calculate(:from_date, :to_date), you probably need to pass params[:from_date] and params[:to_date] which should be strings like 20/01/2016, etc...
Your code should be
#vacations = Vacation.calculate(params[:from_date], params[:to_date])

Add multiple filters in Spree Commerce Rails

We need to implement custom filters for categories in spree ecommerce in latest version as seen here https://github.com/spree/spree .
We need to do it in a dynamic way because we have about 100 filters or more to make. The ideal solution would be to show all available filters in admin area and admin can activate/deactivate them for each category.
Current Scenario:
We know how to make a new filter and apply it. But it takes about four methods per filter as shown in the product_filter.rb file linked below.
Some links we have found useful:
https://gist.github.com/maxivak/cc73b88699c9c6b45a95
https://github.com/radar/spree-core/blob/master/lib/spree/product_filters.rb
Here is some code that allows you to filter by multiple properties. It is not ideal (no proper validation etc) but I guess it is better than doing multiple "in" subqueries.
def add_search_scopes(base_scope)
joins = nil
conditions = nil
product_property_alias = nil
i = 1
search.each do |name, scope_attribute|
scope_name = name.to_sym
# If method is defined in product_filters
if base_scope.respond_to?(:search_scopes) && base_scope.search_scopes.include?(scope_name.to_sym)
base_scope = base_scope.send(scope_name, *scope_attribute)
else
next if scope_attribute.first.empty?
# Find property by name
property_name = name.gsub('_any', '').gsub('selective_', '')
property = Spree::Property.find_by_name(property_name)
next unless property
# Table joins
joins = product if joins.nil?
product_property_alias = product_property.alias("filter_product_property_#{i}")
joins = joins.join(product_property_alias).on(product[:id].eq(product_property_alias[:product_id]))
i += 1
# Conditions
condition = product_property_alias[:property_id].eq(property.id)
.and(product_property_alias[:value].eq(scope_attribute))
conditions = conditions.nil? ? condition : conditions.and(condition)
end
end if search.is_a?(Hash)
joins ? base_scope.joins(joins.join_sources).where(conditions) : base_scope
end
def prepare(params)
super
#properties[:product] = Spree::Product.arel_table
#properties[:product_property] = Spree::ProductProperty.arel_table
end

In Rails, what is the best way to update a record or create a new one if it doesn't exist?

I have a create statement for some models, but it’s creating a record within a join table regardless of whether the record already exists.
Here is what my code looks like:
#user = User.find(current_user)
#event = Event.find(params[:id])
for interest in #event.interests
#user.choices.create(:interest => interest, :score => 4)
end
The problem is that it creates records no matter what. I would like it to create a record only if no record already exists; if a record does exist, I would like it to take the attribute of the found record and add or subtract 1.
I’ve been looking around have seen something called find_or_create_by. What does this do when it finds a record? I would like it to take the current :score attribute and add 1.
Is it possible to find or create by id? I’m not sure what attribute I would find by, since the model I’m looking at is a join model which only has id foreign keys and the score attribute.
I tried
#user.choices.find_or_create_by_user(:user => #user.id, :interest => interest, :score => 4)
but got
undefined method find_by_user
What should I do?
my_class = ClassName.find_or_initialize_by_name(name)
my_class.update_attributes({
:street_address => self.street_address,
:city_name => self.city_name,
:zip_code => self.zip_code
})
Assuming that the Choice model has a user_id (to associate with a user) and an interest_id (to associate with an interest), something like this should do the trick:
#user = User.find(current_user)
#event = Event.find(params[:id])
#event.interests.each do |interest|
choice = #user.choices.find_or_initialize_by_interest_id(interest.id) do |c|
c.score = 0 # Or whatever you want the initial value to be - 1
end
choice.score += 1
choice.save!
end
Some notes:
You don't need to include the user_id column in the find_or_*_by_*, as you've already instructed Rails to only fetch choices belonging to #user.
I'm using find_or_initialize_by_*, which is essentially the same as find_or_create_by_*, with the one key difference being that initialize doesn't actually create the record. This would be similar to Model.new as opposed to Model.create.
The block that sets c.score = 0 is only executed if the record does not exist.
choice.score += 1 will update the score value for the record, regardless if it exists or not. Hence, the default score c.score = 0 should be the initial value minus one.
Finally, choice.save! will either update the record (if it already existed) or create the initiated record (if it didn't).
find_or_create_by_user_id sounds better
Also, in Rails 3 you can do:
#user.choices.where(:user => #user.id, :interest => interest, :score => 4).first_or_create
If you're using rails 4 I don't think it creates the finder methods like it used to, so find_or_create_by_user isn't created for you. Instead you'd do it like this:
#user = User.find(current_user)
#event = Event.find(params[:id])
for interest in #event.interests
#user.choices.find_or_create_by(:interest => interest) do |c|
c.score ||= 0
c.score += 1
end
end
In Rails 4
You can use find_or_create_by to get an object(if not exist,it will create), then use update to save or update the record, the update method will persist record if it is not exist, otherwise update record.
For example
#edu = current_user.member_edu_basics.find_or_create_by(params.require(:member).permit(:school))
if #edu.update(params.require(:member).permit(:school, :majoy, :started, :ended))

Resources