Extending Models in Ruby on Rails - ruby-on-rails

I'm fairly new to both Ruby and Rails, picking up a side-project and working out best-practices. I'm wondering what the recommended approach to this problem might be.
I have an array of widgets with associated grades given by users. When displaying the list of widgets in the view I want to present the grade given, if any, by the logged in user.
What I've got working looks like this, but feels dirty. In my controller I grab some widgets and associated grades (all grades used elsewhere), and from that data I generate a myGrades hash:
#widgets = Widget.include(:grades).limit(20)
#myGrades = Hash[#widgets.collect{ |w| [w.id,w.grades.select{ |g| g.user_id==site_user.id}].delete_if{|k,v|v.nil?}]
In my haml view, when presenting the widgets, I have something like this:
-if #myGrades.has_key?(widget.id)
.mygrade #myGrades[widget.id].value
But, I feel I would be better off with methods like "have I graded this?" and "give me MY grade for this" on my graded widget, without the myGrades middle-man.
Eventually, though, lots of different objects will be graded, so how should I best implement these methods in Rails so they can be applied to any graded models?

You can do the following:
#my_grades = Widget.include(:grades).where(grades: {user_id: site_id}).limit(20)
As #sameera207 pointed out, you should move this complex query in your model: create a scope.
class Widget < ActiveRecord::Base
scope :for_user, lambda do |user, limit = 20|
includes(:grades).where(grades: {user_id: user.try(:id) || user}).limit(limit)
end
And use it like this:
Widget.for_user(user_id)
# works also with a user object:
Widget.for_user(user)
# or if you want a custom limit:
Widget.for_user(user_id, 50)

Related

Ideal way to get association of ActiveRecord::Relation

Consider the following:
class User < ActiveRecord::Base
has_many :posts
end
Now I want to get posts for some banned users.
User.where(is_banned: true).posts
This produces a NoMethodError as posts is not defined on ActiveRecord::Relation.
What is the slickest way of making the code above work?
I can think of
User.where(is_banned: true).map(&:posts).flatten.uniq
But this is inefficient.
I can also think of
user_scope = User.where(is_banned: true)
Post.where(user: user_scope)
This requires the user association to be set up in the Post model and it appears to generate a nested select. I don't know about the efficiency.
Ideally, I would like a technique that allows traversing multiple relations, so I can write something like:
User.where(is_banned: true).posts.comments.votes.voters
which should give me every voter (user) who has voted for a comment on a post written by a banned user.
Why not just use joins?
Post.joins(:user).where(users: {is_banned: true})
This will generate SQL to the effect of
SELECT *
FROM posts
INNER JOIN users ON posts.user_id = users.id
WHERE users.is_banned = true
This seems to be exactly what you are looking for. As far as your long chain goes you can do the same thing just with a much deeper join.
In your code:
User.where(is_banned: true)
will be and ActiveRecord::Relation and you need one record. So doing if from the User model would be more complicated. Depending on how the relationship is set up you could add a scope in your Post model.
scope :banned_users, -> { joins(:users).where('is_banned = ?', true) }
Then you would just call Post.banned_users to get all the post created by banned users.
Here's a start of a solution for your ideal technique. It probably doesn't work as written with extended chaining, and performance would probably be pretty bad. It would also require that you define the inverse_of for each association —
module LocalRelationExtensions
def method_missing(meth, *args, &blk)
if (assoc = self.klass.reflect_on_association(meth)) && (inverse = assoc.inverse_of)
assoc.klass.joins(inverse.name).merge(self)
else
super
end
end
end
ActiveRecord::Relation.include(LocalRelationExtensions)
But really you should use the comment of #engineersmnky.

How does one actually use a has_many/belongs_to relationship?

My two models are User has_many :books and Book belongs_to :user.
The users table has only the column name, while the books table has users_id and title.
Is this how I'm actually supposed to use them? With the users table populated, how do I go about adding a book with a specific user, done by searching their name and not the ID column? I know this is a simple thing, but I really cannot find it on Google, or by re-reading my books and re-watching my Lynda videos; I know the information must be in there somewhere but it is completely frying my brain right now, and I'm extremely confused. I'm very used to SQL and learning to use ActiveRecord instead feels like trying to write with my left hand.
What I want to do is the equivalent of, in SQL, INSERT INTO books (title, users_id) VALUES ("Wolves of the Calla", (SELECT id FROM users WHERE name = 'Sarah'));.
Find the user with the given name and then use the association to create a book with the found user_id
user = User.where(:name => "Sarah").first
user.books.create(:title => "Wolves of the Calla")
As explained in the Association Basics Guide, you'd need something like this:
createdBook = #user.books.create(title: "Wolves of the Calla")
This can all be found in the Rails Documentation. Its worth a read, if you haven't done so already.
In regards to this question:
...how do I go about adding a book with a specific user...
There are a number of ways you can do it, but the key thing to remember is that the has_many and belongs_to methods "create" association methods for you. In this way you can retrieve, and add to an object's associations; Rails will take care of handling the foreign keys and such, so long as they are named in accordance with its naming convention (which it seems you have done)
So as a simple example, to add a book to a user's collection of books, would be something like this:
#user = User.where(name: "Sarah").first
#user.books.create(title: "Wolves of the Calla")
#Rails 4 syntax
#Approach 1
#user = User.find_by(name: 'Sarah')
#book = #user.books.create(title: 'Wolves of the Calla')
#Alternatively
#user = User.find_by(name: 'Sarah')
#user.books << Book.create(title: 'Wolves of the Calla')
#Alternatively
#user = User.find_by(name: 'Sarah')
#book = Book.create(title: 'Wolves of the Calla')
#user.book_ids += [#book.id]

How to refactor that complex singelton model method for create nested models in Rails?

I have following complex method which I cut off from controller:
def self.create_with_company_and_employer(job_params)
company_attributes = job_params.delete(:company_attributes)
employer_attributes = job_params.delete(:employer_attributes)
new(job_params) do |job|
job.employer = Employer.find_or_create_by_email(employer_attributes)
company_attributes[:admin_id] = job.employer.id if Company.find_by_nip(company_attributes[:nip]).nil?
job.company = Company.find_or_create_by_nip(company_attributes)
Employment.create(employer_id: job.employer.id, company_id: job.company.id)
end
end
I using here two nested_attributes functionality for create company and employer.
Whole code you can find here: https://gist.github.com/2c3b52c35df763b6d9b4
company_attributes[:admin_id] = job.employer.id if Company.find_by_nip(company_attributes[:nip]).nil?
Employment.create(employer_id: job.employer.id, company_id: job.company.id)
Basically I would like to refactor that two lines:
I looked at your gist and i think this is a design issue.
your Employment and Job models seem somewhat redundant, but i don't know what are their actual purpose exactly so i can't really help for now on this matter (i have a hunch that your schema could be remodeled with the employements belonging to the jobs). However, if you really want to, you can use an after_create callback to manage the replication :
class Job < ActiveRecord::Base
after_create :create_corresponding_employment
def create_corresponding_employment
Employment.create( employer_id: employer.id, company_id: company.id )
end
end
this gets you rid of the last line of your method.
the other line you want to get rid of is tricky : you assign an admin_id to your company, but why would you want to do that ? In fact, you're just creating a 'hidden' relation between Company and Employer (a belongs_to one). Why do you need that ? Give more information and i can help.
one more thing: it is not advised to delete keys form the params, or even modify the hash directly. Use a copy.

Rails - Good way to associate units of measurement with my database columns?

I've got one model with about 50 columns of measurement data, each with a different unit of measurement (ie. grams, ounces, etc.). What is a good way to associate units of measurement with columns in my database? The primary use for this is simply for display purposes. (Ruby on Rails)
EDIT: To clarify, my model is an object, and the attributes are different measurements of that object. So, an example would be if I had the model Car and the attribute columns :power, :torque, :weight, :wheelbase, etc. I would want car.power.unit to return hp and car.weight.unit to return lbs., etc. This way, I would be able to do something like this:
<%= car.power + car.power.unit %>
and it would return
400hp
Updated Answer
Since you're storing many columns of data, but each column is only one type, and your concern is strictly presentational, I would just use a decorator to accomplish what you need. See this railscast for an example of a great way to do this using Draper.
Basically, a decorator wraps your model with presentation specific methods, so instead of:
#CarsController.rb
def show
#car = Car.find(params[:id])
end
You would use
#CarsController.rb
def show
#car = CarDecorator.find(params[:id])
end
You would define a decorator like so:
class CarDecorator < ApplicationDecorator
decorates :car
def horsepower
model.power.to_s + "hp" #call to_s just in case
end
end
Then in your view any time you called #car.horsepower you would get 123hp instead of 123. In this way you can build a big long reusable list of presentation methods. You can share methods between objects using inheritance, and you can allow methods from the original model to be called as well. See the railscast and the docs etc. You can use Draper or you could roll your own presenter class if you don't want to use a library.
Previous Answer (Abridged):
I can see two nice, easy ways to do this:
1) Just add a text column for units to your data model. IE: to get "400hp" use [data.value,data.units].join
2) You could get a little richer association by having a Units model, perhaps with help from something like ActiveEnum.
You could add a unit model with a for attribute, where you save the attribute in the messurement, you want to apply the unit to. Example:
def Unit < ActiveRecord::Base
scope :for, lambda{|messurement| find_by_for( messurement.to_s ) }
end
This allows you stuff like:
<%= #car.torque + Unit.for(:torque).symbol %>
I do not know if this is of so much advantage, but its a way to solve your problem...

Override just the default scope (specifically order) and nothing else in Rails

So basically I have two classes, Book and Author. Books can have multiple authors and authors can have multiple books. Books have the following default scope.
default_scope :order => "publish_at DESC"
On the Author show page I want to list all the books associated with that author so I say the following...
#author = Author.find(params[:id])
#books = #author.books
All is good so far. The author#show page lists all books belonging to that author ordered by publication date.
I'm also working on a gem that is able to sort by the popularity of a book.
#books = #author.books.sort_by_popularity
The problem is that whenever it tries to sort, the default_scope always gets in the way. And if I try to unscope it before it will get rid of the author relation and return every book in the database. For example
#books = #author.books.unscoped.sort_by_popularity # returns all books in database
I'm wondering if I can use the ActiveRelation except() method
to do something like this (which seems like it should work but it doesn't. It ignores order, just not when it is a default_scope order)
def sort_by_popularity
self.except(:order).do_some_joining_magic.order('popularity ASC')
# |------------| |---------------------|
end
Any ideas as to why this doesn't work? Any ideas on how to get this to work a different way? I know I can just get rid of the default_scope but I'm wondering if there another way to do this.
You should be able to use reorder to completely replace the existing ORDER BY:
reorder(*args)
Replaces any existing order defined on the relation with the specified order.
So something like this:
def self.sort_by_popularity
scoped.do_some_joining_magic.reorder('popularity ASC')
end
And I think you want to use a class method for that and scoped instead of self but I don't know the whole context so maybe I'm wrong.
I don't know why except doesn't work. The default_scope seems to get applied at the end (sort of) rather than the beginning but I haven't looked into it that much.
You can do it without losing default_scope or other ordering
#books.order_values.prepend 'popularity ASC'

Resources