Rails 4 query, scoped by condition from has many through attributes - ruby-on-rails

The question is : "given a company, how should I build the query that returns all events created by all employees during their respective employments periods ?"
Ex:
#company = Company.create
# Welcome John
#john = User.create
#event_a = #john.events.create date: Date.new(2013,6,1)
#event_b = #john.events.create date: Date.new(2014,1,1)
#company.employments.create user:#john, since: Date.new(2013,12,20)
# Welcome Jack
#jack = User.create
#event_c = #jack.events.create date: Date.new(2012,1,1)
#event_d = #jack.events.create date: Date.new(2013,1,1)
#company.employments.create user:#jack,
since: Date.new(2011,12,20), till: Date.new(2012,12,31)
#company.events
=> [#event_b, #event_c]
# #event_a is not returned because it was created prior to John's hiring
# #event_d is not returned because it was created after Jack's departure
I came up with a solution but I would like to know if there are any ways to improve it.
class Event
belongs_to :user
# attributes
# date: datetime
# …
end
class Employment
belongs_to :user
belongs_to :event
# attributes
# since: date
# till: date
# …
end
class Company
has_many :employments
def events
Event.where employments.map do |e|
if e.since && e.till
"(user_id = '#{e.user_id}' AND date BETWEEN '#{e.since}' AND '#{e.till}')"
elsif !e.since.nil?
"(user_id = '#{e.user_id}' AND date > '#{e.since}')"
else
"(user_id = '#{e.user_id}')"
end
end.join(' OR ')
end
end
Do you see any other way to do that ?

Events.joins(user: :employments).where(company_id: id)I believe you could use something like this:
def events
Event.where employments.map do |e|
user=User.find(e.user_id)
since = e.since
till = e.till || Date.parse('3100-12-31') # a day in the distant future if nil
user.events.where('(date is null) or (date > ? and date < ?)', since, till)
end
end
2nd try, after using the appropriate joins to connect Event and Employment model, you can use the final where clause to implement your filter.
The full method, after you've added the right set of joins and filters (thanks), is as follows.
def events
Events.joins(user: :employments).where(company_id: id). # <-This comes from the author, not me.
where('employments.since is null OR
(employments.since < events.date AND employments.till is null) OR
(employments.since < events.date AND employments.till > events.date)')
end

how can I query all events created by all employees during their
employment for a given company
You may wish to use an ActiveRecord Association Extension, which basically appends a new method to your association, allowing you to define filtration or some other specifications:
#app/models/company.rb
Class Company < ActiveRecord::Base
has_many :employments
has_many :events do
def by_date(since, till)
... logic here
end
end
end
This will allow you to call:
#app/controllers/events_controller.rb
Class EventsController < ApplicationController
def action
company = Company.find params[:id]
#events = company.events.by_date("01-01-2014", "04-01-2014")
end
end
If you let me know in the comments, I'll have a go at populating the method for you

Related

Active Record callbacks throw "undefined method" error in production with classes using STI

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.

Ruby on Rails search with multiple parameters

For example in my Car model i have such fields:
color, price, year
and in form partial i generate form with all this fields. But how to code such logic:
user could enter color and year and i must find with this conditions, user could enter just year or all fields in same time...
And how to write where condition? I could write something like:
if params[:color].present?
car = Car.where(color: params[:color])
end
if params[:color].present? && params[:year].present?
car = Car.where(color: params[:color], year: params[:year])
end
and so over....
But this is very ugly solution, i'm new to rails, and want to know: how is better to solve my problem?
Check out the has_scope gem: https://github.com/plataformatec/has_scope
It really simplifies a lot of this:
class Graduation < ActiveRecord::Base
scope :featured, -> { where(:featured => true) }
scope :by_degree, -> degree { where(:degree => degree) }
scope :by_period, -> started_at, ended_at { where("started_at = ? AND ended_at = ?", started_at, ended_at) }
end
class GraduationsController < ApplicationController
has_scope :featured, :type => :boolean
has_scope :by_degree
has_scope :by_period, :using => [:started_at, :ended_at], :type => :hash
def index
#graduations = apply_scopes(Graduation).all
end
end
Thats it from the controller side
I would turn those into scopes on your Car model:
scope :by_color, lambda { |color| where(:color => color)}
scope :by_year, lambda { |year| where(:year => year)}
and in your controller you would just conditionally chain them like this:
def index
#cars = Car.all
#cars = #cars.by_color(params[:color]) if params[:color].present?
#cars = #cars.by_year(params[:year]) if params[:year].present?
end
user_params = [:color, :year, :price]
cars = self
user_params.each do |p|
cars = cars.where(p: params[p]) if params[p].present?
end
The typical (naive, but simple) way I would do this is with a generic search method in my model, eg.
class Car < ActiveRecord::Base
# Just pass params directly in
def self.search(params)
# By default we return all cars
cars = all
if params[:color].present?
cars = cars.where(color: params[:color])
end
if params[:price1].present? && params[:price2].present?
cars = cars.where('price between ? and ?', params[:price1], params[:price2])
end
# insert more fields here
cars
end
end
You can easily keep chaining wheres onto the query like this, and Rails will just AND them all together in the SQL. Then you can just call it with Car.search(params).
I think you could use params.permit
my_where_params = params.permit(:color, :price, :year).select {|k,v| v.present?}
car = Car.where(my_where_params)
EDIT: I think this only works in rails 4, not sure what version you're using.
EDIT #2 excerpt from site I linked to:
Using permit won't mind if the permitted attribute is missing
params = ActionController::Parameters.new(username: "john", password: "secret")
params.permit(:username, :password, :foobar)
# => { "username"=>"john", "password"=>"secret"}
as you can see, foobar isn't inside the new hash.
EDIT #3 added select block to where_params as it was pointed out in the comments that empty form fields would trigger an empty element to be created in the params hash.

How to specify a record with a date greater than another date?

I have a record called Feeds that contains the field 'last_visited' and 'last_modified', both are timestamps.
I'm trying to render a list in a view of alls Feeds where last_modified > last_visited.
I currently have this:
Controller
#feeds = #user.feeds
#feeds_hot = #feeds.where(['#feeds.last_modified > ?', #feeds.last_visited])
Have a feeling I'm way off track here. Should I also being using a model class to do this?
Any helps is greatly appreciated.
Edit:
Here's my model
class Feed < ActiveRecord::Base
attr_accessible :feed_id, :feed_url, :last_modified, :title, :url, :last_visited, :user_id
belongs_to :user
scope :hottest, lambda {where('last_modified > ?', :last_visited)}
def fetch_feed!
feed = Feedzirra::Feed.fetch_and_parse(feed_url) # probably want some eror handling here
self.title = feed.title
self.url = feed.url
self.last_modified = feed.last_modified
self.last_visited = feed.last_modified
self #or nil if you like
end
def self.check_for_update(feed)
fetched_feed = Feedzirra::Feed.fetch_and_parse(feed.feed_url)
entry = fetched_feed.entries.first
feed.last_modified = entry.published
end
def update_visit_date!
date = Time.now
update_attribute(:last_visited, date)
self
end
end
Edit
Updated code
Controller
def home
#user = current_user
#feeds = #user.feeds
#feeds_hot = #feeds.hottest
end
Model
attr_accessible :feed_id, :feed_url, :last_modified, :title, :url, :last_visited, :user_id
belongs_to :user
scope :hottest, lambda {where("'last_modified' > ?", 'last_visited')}
View
%ol.feeds.feeds_hot
- #feeds_hot.each do |feed|
%li.feed.hot[feed]
= render :partial => 'feeds/feed_link', :locals => {:feed => feed}
Unfortunately it's still not rendering the hot feeds in the view when I have a feed with the following data:
Last Modified 2013-06-14 23:49:07 UTC
Last Visited 2013-06-14 23:47:55 UTC
Last Modified is a few hours > than Last Visited
If you're looking to get a list of all feeds that have been modified more recently than the most recent visit on any feed, the following will work:
last_visit = Feed.order("last_visited").last.last_visited
#feeds = #user.feeds
#hot_feeds = Feed.where("last_modified > ?", last_visited)
EDIT:
Based on your comments and a re-reading of your question, the code posted above will not accomplish what you're trying to do. Since you're trying to get a list of all invites where each has been modified since it was last visited, you'll want to create a model scope to do the lookup with ActiveRecord. The following code should work:
# app/models/feed.rb
class Feed < ActiveRecord::Base
scope :hottest, lambda {where('last_modified > ?', :last_visited)}
end
Then, you can run the following in a console, controller, or view:
#user.invites.hottest
#=> array of all the user's invites that have been modified more recently than they have been viewed

Rails Way: Formatting Value Before Setting it in the Model?

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

Ruby on Rails: check the amount of products a shop owns

I'm messing around with a test/exercise project just to understand Rails better.
In my case I have three models: Shop, User and Product.
A Shop can be of three types: basic, medium, large. Basic can have 10 Products maximum, medium 50, large 100.
I'm trying to validate this kind of data, the type of Shop and check how many products it owns when creating a new product.
So far, I came up with this code (in shop.rb) but it doesn't work:
def lol
account = Shop.find_by_sql "SELECT account FROM shops WHERE user_id = 4 LIMIT 1"
products = Product.count_by_sql "SELECT COUNT(*) FROM products WHERE shop_id = 13"
if account = 1 && products >= 10
raise "message"
elsif account = 2 && products >= 50
raise "message"
else account = 3 && products >= 100
raise "message"
end
end
I don't even know if the logic behind my solution is right or what. Maybe I should validate using
has_many
and its "size" method? I don't know. :)
At least change account = 1 to account == 1. Same goes for account = 2 and account = 3.
Other than that I would recommend you looking at Rails Guides to get a feel for using Rails.
That being said, I suggest something like this:
class Shop < ActiveRecord::Base
has_many :products
validates :products_within_limit
# Instead of the 'account' column, you could make a 'max_size' column.
# Then you can simply do:
def products_within_limit
if products.size > max_size
errors.add_to_base("Shop cannot own more products than its limit")
end
end
def is_basic?
products.size >= 10 && products.size < 50
end
def is_medium?
products.size >= 50 && products.size < 100
end
def is_big?
products.size >= 100
end
def shop_size
if self.is_basic?
'basic'
elsif self.is_medium?
'medium'
elsif self.is_big?
'big'
end
end
end
This allows you to do:
# Get shop with id = 1
shop = Shop.find(1)
# Suppose shop '1' has 18 products:
shop.is_big? # output false
shop.is_medium? # output false
shop.is_basic? # output true
shop.shop_size # output 'basic'
it does not need to be so hard:
class Shop < ActiveRecord::Base
has_many :products
validate :check_nr_of_products
def check_nr_of_products
nr_of_products = products.size
errors[:base] << "Basic shops can have max 10 products" if account == 1 && nr_of_products > 10
errors[:base] << "Medium shops can have max 50 products" if account == 2 && nr_of_products > 50
errors[:base] << "Big shops can have max 100 products" if account == 3 && nr_of_products > 100
end
this validation is checked every time you save. You do not need to retrieve the "account-type", assuming it is a field of the shop. Likewise, instead of writing the query to count the nr of products, use the size function that does just that.
This is a simple solution. The STI solution suggested by #Dave_Sims is valid and more object-oriented.
Here's one possible more Rails-y way of achieving this. This is my own flavor of making Ruby imitate the behavior of an abstract class (Shop). YMMV.
EDIT: Note that I'm replacing the 'account' variable from OP's example with inheritance using ActiveRecord's Single Table Inheritance, which uses a 'type' column to perform basically the same function, but using inheritance to express the different kinds of shops and their respective product limits. OP's original example likely violates the Liskov Substitution Principle, and STI is one way of fixing that.
EDIT: As if I wasn't being pedantic enough, technically it's not really a Liskov violation as much as an Open/Closed violation. They're all variations on the same theme. You get the idea.
class Product < ActiveRecord::Base
belongs_to :shop
end
class Shop < ActiveRecord::Base
has_many :products
belongs_to :user
validates :products_within_limit
def products_within_limit
if products.count > limit
errors.add_to_base("Shop cannot own more products than its limit")
end
end
def limit
raise "limit must be overridden by a subclass of Shop."
end
end
class BasicShop < Shop
def limit
10
end
end
class MediumShop < Shop
def limit
50
end
end
class LargeShop < Shop
def limit
100
end
end
shop = BasicShop.create
10.times {Product.create(:shop => shop)}
shop.reload
shop.valid? # is true
shop.products << Product.new
shop.valid? # is false
This should help you.
Well, first of all thanks to everyone for the big help and insightful discussion. :)
I took bits from your answers in order to assemble a solution that I can understand myself. It seems when it comes to programming I can only understand if else statements, nothing more complex. :(
What I did was:
class Shop < ActiveRecord::Base
belongs_to :user
has_many :products, :dependent => :destroy
validate :is_account
def is_account
if account == 1 && products.size < 11
elsif account == 2 && products.size < 51
else account == 3 && products.size < 101
end
end
Then in products_controller.rb I put these lines:
def new
if current_user.shop.nil?
flash[:notice] = I18n.t 'shops.create.must' #this should check is the user owns a shop, otherwise can't add a product. It seems to work, so far
redirect_to :action => :index
elsif current_user.shop.valid?
flash[:notice] = I18n.t 'shops.upgrade.must'
redirect_to :action => :index
else
#product = Product.new
end
end
The shop now is a type 1 and has only 9 products but whenever I click the "New Product" link I'm redirected to /products with the shops.upgrade.must message.
I don't know, it seems that
account
in shop.rb doesn't return the correct value. That column is a int(11) type, so I guess it could only return a number, but still...
Again, thanks for the huge support. I ended up stealing bits from your solutions and implementing this code:
#in shop.rb
validate :is_account
def is_account
if account == 1
limit = 10
elsif account == 2
limit = 50
else account == 3
limit = 100
end
errors.add(:base, "Reached maximum number of items for shop") if account == account && products.size >= limit
end
#in products_controller.rb
def new
if current_user.shop.nil?
flash[:alert] = I18n.t 'shops.create.must'
redirect_to :action => :index
elsif current_user.shop.invalid?
flash[:alert] = I18n.t 'shops.upgrade.must'
redirect_to :action => :index
else
#product = Product.new
end
end
It seems to work so far. Hope I didn't make any blatant mistake.
Thanks again! :)

Resources