How to group Rails ActiveRecord results by a column? - ruby-on-rails

I'm working on a little time tracking application and ran into a problem I don't know how to solve. I have a Task model and a Client model. Each task belongs to a client.
class Task < ActiveRecord::Base
belongs_to :client
attr_accessible :client_id, :description, :start, :end
scope :yesterday, -> {
where('start > ?', Date.yesterday.to_time).where('start < ?', Date.today.to_time)
}
end
class Client < ActiveRecord::Base
attr_accessible :name
has_many :tasks
end
Right now I'm displaying a list of tasks scoped by the day the tasks were completed, and ordered by the time they were completed. I would like to display that same list, but grouped by the client, and sorted by the client name. Here's what I would like to do:
<div id="yesterday_summary">
<% #yesterday_clients.each do |client| %>
<h2><%= client.name %></h2>
<ul>
<% client.tasks.each do |task| %>
<li><%= task.description %></li>
<% end %>
</ul>
<% end %>
</div>
In my controller I currently have:
#tasks_yesterday = Task.yesterday
#yesterday_clients = group_tasks_by_client #tasks_yesterday
And in the group_tasks_by_client method, I have some pretty ugly code that isn't even working at the moment:
def group_tasks_by_client(tasks)
clients = []
tasks.collect(&:client).each do |client|
clients << {client.id => client} unless clients.has_key? client.id
end
clients_with_tasks = []
clients.each do |client|
c = Struct.new(:name, :tasks)
cl = c.new(client.name, [])
tasks.each do |task|
cl.tasks << task if task.client_id = client.id
end
clients_with_tasks << cl
end
clients_with_tasks
end
I'm sure there is a clean, simple, rails-way to do this but I'm not sure how. How can this be done?

You can have the database do this for you like so:
#yesterdays_clients = Client.includes(:tasks).merge(Task.yesterday).order(:name)
Besides being cleaner, it's more efficient since it gets all your clients and tasks in one pass. The original code was subject to N+1 queries because there was no eager loading.
BTW, you can make your scope simpler as well:
scope :yesterday, -> { where(:start => (Date.yesterday.to_time...Date.today.to_time)) }

Related

Adding a way to view assignments due within the next seven days in my Rails app

I am building a basic student planner application using Ruby on Rails and I am trying to implement a way for a student to see which assignments are due within the next seven days. I would like to use a scope method, something like due_soon that can be called within a view.
Currently when I run my code I'm getting an ArgumentError in Assignments#index and it's telling me that the comparison of Date with :due_date failed
My Assignments model:
class Assignment < ActiveRecord::Base
belongs_to :course
belongs_to :student
# accepts_nested_attributes_for :course
validates :course_id, :student_id, :due_date, :title, presence: true
scope :due_soon, -> { where(Date.current + 7.days >= :due_date) }
def course_attributes=(attributes)
binding.pry
if attributes[:course_name].blank?
self.course = Course.find_by(id: params[:course_id])
else
self.course = Course.find_or_create_by(attributes)
self.course
end
end
end
In my view:
<h3>Due Soon</h3>
<% Assignment.due_soon.each do |assignment| %>
<% if assignment.course %>
<tr>
<td><%= assignment.title %></td>
<td><%= assignment.course.course_name %></td>
<td><%= assignment.due_date %></td>
<td><%= link_to "View/Edit Assignment", student_assignment_path(#student, assignment) %></td>
</tr>
<% end %>
<% end %>
Any pointers would be much appreciated as I try and continue to familiarize myself with rails! Thanks!
The issue you are having here is related to your usage of where:
Assignment.where(Date.current + 7.days >= :due_date)
Is not valid for ActiveRecord's where method.
What you need is to either use ActiveRecord's lib to generate SQL (1), or write the SQL yourself (2):
# (1) Use ActiveRecord combined with a Ruby Range
Assignment.where(due_date: Date.current..7.days.from_now)
# (2) Assignment.where('due_date between ? and ?', Date.current, 7.days.from_now)
You have a syntax error there.
One way is...
scope :due_soon, -> { where('due_date =< ?', Date.current + 7.days) }
https://apidock.com/rails/ActiveRecord/NamedScope/ClassMethods/scope

How to create sum calculation on update after saving, before redirect, after all actions?

I have a nested form:
<%= form_for shop_product do |f| %>
...
<%= f.fields_for :shop_product_print_files do |ff| %>
<%= ff.object.print_location.title %>
<%= ff.hidden_field :print_location_id %>
<%= ff.select :image_file_id, options_for_select(#image_files.map { |image| [image.id, {'data-img-src'=>image.image_file_url(:thumb)}]}), {:include_blank => 'Noneblank'}, class: "image-picker" %>
...
Which on update, will reject and delete if the :image_file_id is blank:
shop_product model
accepts_nested_attributes_for :shop_product_print_files, reject_if: :reject_file, :allow_destroy => true
def reject_file(attributes)
if attributes[:image_file_id].blank?
if attributes[:id].present?
attributes.merge!({:_destroy => 1}) && false
else
true
end
end
end
In the update method, where the issue is:
#shop_product.price = (#shop_product.print_locations.to_a.sum { |sp| sp.price } + #shop_product.product.price)
The delete is happening AFTER the update and seems to be the last action that happens before the redirect of the successful update.
How can I create this calculation after it's updated and the records are destroyed?
I tried:
after_commit :calculate_price
def calculate_price
self.price = (self.print_locations.to_a.sum { |sp| sp.price } + self.product.price)
end
end
after_updated
after_save
Not working
To clarify as well, this calculation works on create. the issue is because of deleting the records on update as mentioned, it kicks in before the database is changed which is why i figured after_commit would work.
Attempt in ShopProductPrintFile model:
after_commit :calculate_sp, on: [:update, :destroy]
def calculate_sp
shop_product = ShopProduct.find(self.shop_product.id)
price = (shop_product.print_locations.to_a.sum { |sp| sp.price } + shop_product.product)
shop_product.update_attributes!(price: price)
end
end
Still not working but seems like this should? Is my syntax not correct in writing?
SOLUTION:
after_commit :calculate_sp
def calculate_sp
shop_product = ShopProduct.find(self.shop_product.id)
price = (shop_product.print_locations.to_a.sum { |sp| sp.price } + shop_product.product.price)
shop_product.price = price
shop_product.save!
end
try to use after_commit :update_product_price, on: :destroy in your ShopProductPrintFile model. It will trigger when shop_product_print_files destroyed and you need to recalculate the price for product.
And in your example, you need to add save! to your after_commit or it won't save to database. But you should be careful that it will trigger another update to product model and trigger after_commit again. so you have to add a condition to detect if after_commit should be triggered.
This works with putting in my ShopProductPrintFile model.
On update, create, and destroy.
after_commit :calculate_sp
def calculate_sp
shop_product = ShopProduct.find(self.shop_product.id)
price = (shop_product.print_locations.to_a.sum { |sp| sp.price } + shop_product.product.price)
shop_product.price = price
shop_product.save!
end
I will not be accepting this response as I am sure there is a better way or syntax for doing this. If you have this, please respond with it and why it is a more efficient or better approach and I will accept yours

Ruby on Rails - show current date only

I'm creating a small lesson app, with a different lesson for each day. I'm wanting to show the current days lesson only on the index, and can't figure out how. I googled and found some info that came close, but still couldn't fix the problem. I only have one controller, DaysController. No user controller.
For my model(day.rb) I've tried this
class Day < ActiveRecord::Base
validates :description, :date, :link_to, :presence => true
scope :created_on, lambda {|date| {:conditions => ['created_at >= ? AND created_at <= ?', date.beginning_of_day, date.end_of_day]}}
def self.today
self.created_on(Date.today)
end
end
And for my index I've tried these
<% #day.created_on(Date.today) %>
<% #day.today %>
any advice??
If I understand correctly and for simplicity sake is this essentially what you are trying to achieve?
Controller:
def index
#days = Day.all
end
View (index.html.erb):
<% #days.each do |day| %>
<% if day.created_at == Date.today %>
<%= day.field_name %>
<% end %>
I would change the scope to just use the Date object and I assume that you want your condition to use the newer syntax:
scope :created_on, ->(date) { where(created_at: date) }
Then the Day.today method should work without any change.
You can do the following:
class Day < ActiveRecord::Base
scope :today, lambda { where('CAST(created_at AS date) = ?', Date.today) }
And use it like this:
#days = Day.today
# returns a list of `Day` records where the `created_at`'s date is equal to today's date

mutliple select dropdown company and save perk and respective company

here is my code:
Perk not save on multiple select,when multiple true/false. perk save and habtm working.
class Perk < ActiveRecord::Base
has_and_belongs_to_many :companies
end
class Company < ActiveRecord::Base
has_and_belongs_to_many :perks
end
view perk/new.html.erb
<%= select_tag "company_id", options_from_collection_for_select(Company.all, 'id', 'name',#perk.companies.map{ |j| j.id }), :multiple => true %>
<%= f.text_field :name %>
Controller's code:
def new
#perk = Perk.new
respond_with(#perk)
end
def create
#perk = Perk.new(perk_params)
#companies = Company.where(:id => params[:company_id])
#perk << #companies
respond_with(#perk)
end
Your select_tag should return an array of company_ids:
<%= select_tag "company_ids[]", options_from_collection_for_select(Company.all, 'id', 'name',#perk.companies.map{ |j| j.id }), :multiple => true %>
http://apidock.com/rails/ActionView/Helpers/FormTagHelper/select_tag#691-sending-an-array-of-multiple-options
Then, in your controller, reference the company_ids param:
#companies = Company.where(:id => params[:company_ids])
(I assume that you've intentionally left out the #perk.save call in your create action... Otherwise, that should be included as well. Model.new doesn't store the record.)
It sounds like you may not have included company_id in the perk_params method in your controller. Rails four uses strong pramas this means you need to state the params you are allowing to be set.However it is difficult to say for sure without seeing more of the code.
In your controller you should see a method like this (there may be more options that just :name):
def perk_params
params.require(:perk).permit(:name)
end
You should try adding :company_id to it so it looks something like this:
def perk_params
params.require(:perk).permit(:name, :company_id)
end
if there are other params int your method leave them in and just added :company_id
EDIT to original answer
The above will only work on a one-to-many or one-to-one because you are using has_and_belongs_to_many you will need to add companies: [] to the end of your params list like this
def perk_params
params.require(:perk).permit(:name, companies: [] )
end
or like this
def perk_params
params.require(:perk).permit(:name, companies_ids: [] )
end
See these links for more details:
http://edgeapi.rubyonrails.org/classes/ActionController/StrongParameters.html
http://edgeguides.rubyonrails.org/action_controller_overview.html#strong-parameters

Is there a better way to write this named_scope? [Rails]

I am using this named_scope to search for products that have a description matching any word the user inputs.
E.g., Product.description_like_any("choc pret")
Will return products with names like
"Chocolate Bar"
"Chocolate Covered Pretzels"
"Miniature Chocolate Ponies"
Here's the named_scope I've written (which works)
named_scope :description_like_any, (lambda do |query|
return {} unless query
conditions = []
values = []
for q in query.split(/\s+/)
conditions << "(`products`.description LIKE ?)"
values << "%#{q}%"
end
{ :conditions => [conditions.join(' AND '), *values] }
end)
Is there a better way to write this? Perhaps I'm missing a Rubyism/Railism or two?
Solution
Using scope_procedure in conjunction with Searchlogic, this can be done in an even easier way. Note, the solution before even leverages Searchlogic's _or_ syntax for connecting two scopes together. The :keywords scope_procedure finds products matching product.description, or product.vendor.name; All with one text field!
Model
# app/models/product.rb
class Product < ActiveRecord::Base
scope_procedure :keywords, lambda |query|
description_like_any_or_vendor_name_like_any(query.split(/\s+/))
end
end
Controller
# app/controllers/products_controller.rb
class ProductsController < ApplicationController
def index
#search = Product.search(params[:search])
#products = #search.all
end
end
Views
# app/views/products/index.html.erb
<% form_for #search do |f| %>
<%= f.label :keywords, "Quick Search" %>
<%= f.input :keywords %>
<%= f.submit, "Go" %>
<% end %>
The most Railsy thing to do is to not write this yourself. :-) Use the excellent Searchlogic gem which will create the description_like_any scope for you.
Edit: If you want your user to be able to enter search terms in a free text field like this, you can define your own scope:
class Product < ActiveRecord::Base
# ...
scope_procedure :description_like_any_term, lambda { |terms|
name_like_any(terms.split(/\s+/))
}
# ...
end

Resources