Rails joining multiple methods and scopes - ruby-on-rails-4

My rails app provides a list of services which are defined like this:
service.rb
has_and_belongs_to_many :clients
has_many :translations, class_name: ServiceTranslation, dependent: :destroy
def with_translation(lang)
includes(:translations)
.where("service_translations.language_id=?", lang.id)
.references(:service_translations)
end
scope :with_clients, -> { select("services.*, count(clients_services.service_id) as clients_count")
.joins(:clients).group('services.id').order('clients_count desc') }
So the with_translation method is eager loading the translations for the service (name, content etc) and the scope with_clients is making sure that the services are ordered by the number of linked clients (also, if no client is linked to a service, the service is not showing)
Both methods are working when called separately in my services_controller:
services_controller.rb
class ServicesController < ApplicationController
def index
##services = Service.with_clients.page(params[:page])
#or
#services = Service.with_translation(#lang).page(params[:page])
end
end
All is good and dandy, but I need both of those methods working together, so when I do this:
#services = Service.with_clients.with_translation(#lang).page(params[:page])
I get this error message:
PG::Error: ERROR: column "clients_count" does not exist
LINE 1: SELECT DISTINCT "services"."id", clients_count AS alias_0 F...
I tried merging the two methods, creating the whole query in the controller, both in vain...any help would be appreciated!

The error suggest the output query have clients_count as a column but it is just an alias for COUNT.
Try running scopes in rails console and analysing the queries in the log one by one:
you say this works
Service.with_clients
first make sure two scopes combined without pagination work correctly
Service.with_clients.with_translation(#lang)
The resulting query should be like this:
SELECT DISTINCT "services"."id", count(clients_services.service_id) AS clients_count FROM "services"
INNER JOIN "clients_services" ON "clients_services"."service_id" = "services"."id"
INNER JOIN "clients" ON "clients"."id" = "clients_services"."client_id"
LEFT OUTER JOIN "service_translations" ON "service_translations"."service_id" = "services"."id"
WHERE (service_translations.language_id=2)
GROUP BY services.id
ORDER BY clients_count desc

Related

Rails 4 Has_Many With Finder_SQL Deprecated

I am upgrading a Rails app to 4.0. I am receiving the following deprecation warning. I have Google'd on this, but have not found anything that tells how to change this.
DEPRECATION WARNING: The :finder_sql association option is deprecated. Please find an alternative (such as using scopes)...
Here is the scope that is causing the warning:
has_many :elective_instructors,
:class_name => "Instructor",
:finder_sql => proc { "SELECT DISTINCT people.* FROM people
INNER JOIN class_sections ON class_sections.instructor_id = people.id
INNER JOIN courses ON courses.id = class_sections.course_id
INNER JOIN taken_classes ON class_sections.id = taken_classes.class_section_id
WHERE
courses.core = FALSE
AND
taken_classes.student_id = #{id}
AND
people.type = 'Instructor'
AND
people.ignore = FALSE" }
Any ideas would be greatly appreciated. Thanks!
As of 4.1, :finder_sql is not just deprecated - it has been completely REMOVED from Rails.
Here is one way to do something similar through the use of scopes. Let's say we have a User class and Job class (so a user can have many jobs). And let's say that we want to find all distinct jobs that this user holds (contrived example, but it illustrates the point) and let's say we want to use custom SQL for this. We can use find_by_sql as follows
class Job < ActiveRecord::Base
scope :distinct_user_jobs, -> (user_id){ find_by_sql(["SELECT DISTINCT jobs.* FROM jobs WHERE jobs.user_id=?", user_id]) }
end
And then pass in the user_id
user = User.first
Job.distinct_user_jobs(user.id)

Rails 4 Dynamically Generate an Extra Public-Facing URL on Create

I have a Rails app that stores stock photos in each project. Upon creating a project, I want the app to not only create the url for the project that we will view internally, which is projects#show,i.e. /projects/4 but I also want it to create another URL that we can show to the client that will be a little different. It will allow the to approve the photos, etc. I want the url to be something like /projects/st53d where the end of the url will be a random number generated with random_string = SecureRandom.urlsafe_base64(5)
This way, I can pass this url to the clients and they can view all the photos in the project and approve the ones they want us to use, but cannot change or view the other internal stuff we have on the standard product show page
Currently, I have added a client_url column to the Project model and I was thinking of generating the random number within the Project#create method and then saving it to the project's client_url column. This way I can loop through all client urls and make sure I did not generate a duplicate. But I cannot figure out how to do the route creation part.
i have yet to do the part where I check if it is random but I know how to do that, just not the route creation.
I was thinking I needed to create a new method which I named show_client and somehow create a route for that in my config/routes.rb file
class ProjectsController < ApplicationController
before_action :authenticate_user!, only: [:show,:index]
def create
#project = Project.create(project_params)
#project.creator = current_user.email
require 'securerandom'
random_string = SecureRandom.urlsafe_base64(5)
#project.client_url = random_string
#project.save
redirect_to #project
end
def show_client
#project = Project.find(params[:id])
#photos = #project.photos
end
This seems like more of a roles issue. You could check out something like CanCanCan. If you only need two kinds of users, you could also just add a boolean column for admin to your user table. The url extension doesn't seem to matter in the scope of your problem. If you want to use a different view for the visiting the user show page, something in the show action like:
unless current_user.admin?
redirect_to client_show(current_user.id)
end
I was able to figure it out.
I created the client_url column in the database and upon creating a project, in the create method of the projects_controller, I generated a random base64 number and assigned it to project.client_url
in routes.rb i did:
get 'projects/clients/:client_id' => 'projects#clients', as: 'projects_client'
in my projects_controller.rb:
def clients
#project = Project.where(client_id: params[:client_id])
end
then the link to that route is:
<%= link_to 'Client Version', projects_client_path(#project.client_url) %>

Can I make a belongs_to association use eager loading by default?

I am connecting to one of my company's SQL Server databases, and trying to set up ActiveRecord so I can treat them just the same as Rails objects.
I have these two models:
class Change < ActiveRecord::Base
belongs_to :affected_contact, class_name: "Contact"
end
class Contact
# Contact's primary key is a binary UUID; I can't change this
end
I am trying to get the affected contact of one particular change. Normally, this would be a simple case, but:
Change.first.affected_contact
Change Load (52.6ms) EXEC sp_executesql N'SELECT TOP (1) [chg].* FROM [chg] ORDER BY [chg].[id] ASC'
Contact Load (28.0ms) EXEC sp_executesql N'SELECT TOP (1) [ca_contact].* FROM [ca_contact] WHERE [ca_contact].[contact_uuid] = #0', N'#0 binary', #0 = 0xfcf9a8ac6381aa4386c9b10ee382e10b [["contact_uuid", "<16 bytes of binary data>"]]
=> nil
... that's not what I want! And yet, if I eager-load the join first, it works:
Change.eager_load(:affected_contact).first.affected_contact
SQL (34.4ms) EXEC sp_executesql N'SELECT TOP (1) holy_crap_theres_a_lot_of_columns FROM [chg] LEFT OUTER JOIN [ca_contact] ON [ca_contact].[contact_uuid] = [chg].[affected_contact] ORDER BY [chg].[id] ASC'
=> #<Contact contact_uuid: "\xFC\xF9\xA8\xACc\x81\xAAC\x86\xC9\xB1\x0E\xE3\x82\xE1\v", ... >
In fact, if I force the matching to happen in the JOIN clause in any way, it will work, but belongs_to seems to use the WHERE clause instead, and nil is the best response I can get (a lot of the time, there are conversion errors between the string and its binary type).
Is there a way to ensure eager-loading through the JOIN clause happens by default on the belongs_to association?
I found that #find_by_contact_uuid (contact_uuid being the primary key) worked, where #find didn't, for some reason. That led to this being implemented.
I have ended up essentially rewriting the association methods that Active Record supplies:
module AssociationMethods
def self.included(base)
base.reflect_on_all_associations(:belong_to).each do |a|
define_method a.name do
# #find_by_<uuid_pk> seems to work where #find doesn't
a.klass.send "find_by_#{a.association_primary_key}", self[a.foreign_key]
end
end
base.reflect_on_all_associations(:has_many).each do |a|
define_method a.name do
a.klass.where(a.foreign_key => self.send(a.association_primary_key))
end
end
end
end
class Contact
has_many :changes, foreign_key: :affected_contact_id
include AssociationMethods # include *after* all associations are defined
end
class Change
belongs_to :affected_contact, class_name: 'Contact'
include AssociationMethods
end
It doesn't cover everything that Active Record supplies when setting up the associations, but it seems to do the trick.
Using includes should resolve your problem. This is because includes will preload or eager_load depending on your other conditions.
read more here

Rails 4: strong_params,nested_attributes_for and belongs_to association trouble

I really can't get my head around Rails 4 strong parameters, belongs_to association and form with fields_for.
Imagine I have model for quoting some price:
class Quote < ActiveRecord::Base
belongs_to :fee
accepts_nested_attributes_for :fee
Now, I have seeded some fees into the db, and have put some radiobuttons on my form_for #quote using fields_for. The values of the radiobuttons are simply ids of the records.
Here is the troubling part, the controller:
def create
#quote = Quote.new(quote_params)
...
end
def quote_params
params.require(:quote).permit(:amount_from, fee_attributes: [:id])
end
From my understanding, automagically Rails should fetch fee record with some id, but there is some mystic error instead.
params hash is: "quote"=>{"amount_from"=>"1200", "fee_attributes"=>{"id"=>"1"}}
Log tail:
Completed 404 Not Found in 264ms
ActiveRecord::RecordNotFound (Couldn't find Fee with ID=1 for Quote with ID=)
app/controllers/quotes_controller.rb:14:in `create'
I really don't understand what is going on here, have read Rails association guide, googled for hour for all info, but to no avail.
What I want to achieve here is to understand the correct "Rails way" to fetch some associations for new Quote object using some params I've put in the form.
Guess I got nested_attributes_for wrong, somehow thought it would call Fee.find automagically.
I've opted for ditching fields_for helpers from the form and rendering fields manually like
radio_button_tag 'fee[id]', fee.id
Then in controller I have 2 params methods now:
def quote_params
params.require(:quote).permit(:amount_from)
end
def fee_params
params.require(:fee).permit(:id)
end
And my action looks like
def create
#quote = Quote.new(quote_params)
#quote.fee = Fee.find(fee_params[:id])
...
Any additions on best practices when one has to handle lots of different objects with not so straight init logic are welcome.

Rails: Invalid single-table inheritance type error

So, I am working on migrating this php site with an existing database which I cannot change over to Rails. There is a table: Quotes with a column named type. Whenever I try and create a model of this and set the type, it tells me the following error:
ActiveRecord::SubclassNotFound (Invalid single-table inheritance type: HOME is not a subclass of Quotes)
I don't understand why it thinks its inheriting because it's not supposed to. My create method looks like this:
quote = Quotes.create(
agent_id: agent.id,
client_id: client.id,
type: 'HOME',
status: 0,
date_created: DateTime.now
)
If I comment out the type, everything works fine. But with the Type it errors.
I resolved this by setting the models inheritance_column to nil. Active Record Models can inherit from a table through the attribute :type, setting the inheritance_column to nil removes that attribute allowing you to have a database column named type
class Quote < ActiveRecord::Base
self.inheritance_column = nil
end
I hate having potential gotchas deep in the code especially in the intial processes like generating a model. Better to just change the reserved word to something else and free yourself up to take advantage of inheritance column later if the need comes up. A cleaner solution is listed here -> rename a database column name using migration
It reads;
Execute $> rails generate migration ChangeColumnName
where, ChangeColumnName is the name of our migration. This can be any name.
Now, edit the generated migration file at db/migrate/_change_column_name.rb
class ChangeColumnName < ActiveRecord::Migration
def change
rename_column :table_name, :old_column, :new_column
end
end
$> rake db:migrate
You will have to edit controller and view files e.g. if the model name is Product then you will likely edit these files
/app/views/products/_form.html.erb
/app/views/products/show.html.erb
/app/controllers/products_controller.erb
/app/views/products/index.html.erb