How does one actually use a has_many/belongs_to relationship? - ruby-on-rails

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]

Related

Devise and acts_as_votable: where votes_for voter_id => current_user.id

I'm using the devise and acts_as_votable gems to allow users to cast their votes on posts. My current objective is to query a list of the posts liked by the user.
Looking around in the Rails console I found out that when a post is voted on a votes_for table gets appended on containing information like the timestamp, vote weight and the voter_id which is what I would like to call upon.
Here's what the user controller looks like (user controller because I'm attempting to query the likes on the users/show page):
#user = current_user
#links = Link.all
#likes = #links.where.votes_for(:voter_id => #user.id) // line I'm trying to figure out, I reckon it'd look something like this
I could definitely be going about this the wrong way altogether, if that's the case I'd just appreciate some direction.
Explanation would be appreciated... I'm learning the fundamentals of Rails and finding its naming conventions convoluted and hard to follow.
If you're using Devise, I guess the current_user method should be available in your view and you shouldn't need a reassignment to #user
To understand how ActiveRecord works you might want to look into the documentation here.
As for the links and the voter_id, here's the way I think your query should be:
#likes = Link.where(votes_fors: {voter_id: current_user.id}) # OR Link.where(voter: current_user)
Basically the query will be transformed to a SQL query which says:
"SELECT * FROM links INNER JOIN votes_fors ON links.id = votes_fors.votable_type='Link' WHERE votes_fors.voter_id = ?" [voter_id, 1]
You get the idea?
A better approach would be to use the ActiveRecord methods to fetch these collections e.g
class User
has_many :votes_fors, foreign_key: :voter_id
has_many :favorites, through: :votes_fors, source: :votable # not really sure of this syntax but it should work I think.
end
With the above, you should be able to do:
current_user.favorites which would fetch you all the links voted by the user.

Setting custom permissions within a "dashboard" area in rails

I'm putting together a side project for a teacher/student type of website where the student will share a dashboard with the teacher. The teacher and student can both upload files and leave comments in the dashboard.
The part that I'm stuck on is the permission. For student, I've set up the index controller to this method
def index
#Homework = Homework.where(:user_id = current_user)
end
With this, I'm able to have the student only see the work that they have, but I'm confused on how to get the teacher to see each individual student's work?
Suggestions? Thanks
Here's a simple solution if you only ever need to support a single class in your application:
def index
if current_user.teacher?
#homeworks = Homework.all
else
#homeworks = Homework.where(user_id: current_user)
end
end
Otherwise, your Homework schema does not seem to be correctly designed. See your query below:
Homework.where(:user_id = <student_id>)
This works to retrieve a student's homeworks, but it does not work to retrieve a teacher's students' homeworks. You may need a class_id for a teacher to see each individual student's work:
Homework.where(:class_id = <class_id>, :user_id = <student_id>)
A Class has_many Teachers and has_many Students. This design will allow you to support multiple classes.
Some more guiding questions:
Is teacher/student both kept in the same User model?
How do you differentiate between teacher/student in your current User model?
Is there a "user_type" column somewhere in User?
What happens if the current_user is of the "teacher" user_type?
For complex user permissions, use CanCanCan: https://github.com/CanCanCommunity/cancancan
Don't use uppercase instance variables. ex: #Homework should be #homework
Check out the gem CanCan. Install it (follow the instructions, you should have to put something in application controller), Then, put in your ability file:
class Ability
include CanCan::Ability
def initialize(user)
can :manage, Homework, user_id: user.id
end
end
Then at the top of your StudentController put
load_and_authorize_resource
And the index action should look like:
#homework = #student.homework
Now, you didn't post your whole controller so this is a much as I can help.
I believe you may have a bigger underlying issue. You have students and teachers has_many homework i read in your comment. Then in your example you use user_id. You are likely overriding your students and teacher ownership of homework. You would need a has_and_belongs_to_many relationship OR you would need a student_id and teacher_id columns on the homework table
Cancan automatically generate a number of instance variables which can make it feel like magic. Watch the free railscasts on cancan the same guy who made the video wrote the CanCan library.

Ruby on Rails - How to Query on model/condition on controller?

I'm using rails 3.2.3 and have a questions about queries.
I've read that it is favorable using arel instead of named scopes.
Basically in my app, when a user logs in, I want him to see the products that he created.
So instead of having in my controllers index:
products=Product.find(:all)
I was looking for something like
products=Product.find(:all, :conditions....)
The thing is, my User and Product models have a HABTM relation (it really has to be) and I don't know how to join those tables so that only the products inserted by the current_user are displayed (the insertion is working correctly)
Do I have to add a search method in my Product model?
Or this can be accomplished by passing :conditions in the index controller?
Basically the logic is:
->Get all the products
->inner joining with the products_users HABTM table and get all the products where products_users.user_id = current_user.id. Return the results.
I don't know if I'm missing something here...any tips on how to code this? I'm kind of confused.
if user_sighed_in?
#products = current_user.products
else
#products = Product.scoped
end
ofc u have to define association in User model
has_many :products
If you have associated User and Products models - this code #products = current_user.products will return products of current_user.
To find all the products of current user this will do the trick
current_user.products

Rails 3 nested attributes: How can I assign a matching record to the parent model instead of creating a new record every time?

Here's the basic setup:
I have an Order model. An Order has one Address and it accepts_nested_attributes_for :address.
I have a basic order form where I ask a user to input her address. This is handled with nested_fields_for. Everything works great - new addresses are validated and assigned nicely.
However, the problem is that it creates a new Address every time, even if an Address already exists with identical attributes.
I would like to modify the behavior so that if the user-inputted address matches all the attributes for an existing Address, the order assigns the existing Address to itself rather than creating a new one.
The methods I have tried are:
In the controller, try to find an existing Address record with the nested attributes (params[:order][:address_attributes]). If a match exists, delete all the nested attributes and replace them with params[:order][:address_id].
Don't use nested_attributes_for at all and instead override the address= method in the model, then just use the controller to create a new Address based on the parameters and then hand it off to the model.
Both of these solutions seem various degrees of messy. Could somebody please enlighten me on whether this is a controller or model responsibility, and perhaps suggest an elegant way to accomplish this?
Thanks in advance.
Have you tried something like this?
class Order < ActiveRecord::Base
# [..]
before_save :replace_existing_address!
def replace_existing_address!
db_address = Address.where(:city => self.address.city,
:street => self.address.street,
:number => self.address.number).first
self.address = db_address if db_address
end
end
Since I'm asking this as a survey of good ways to do this, I figured I'd offer the solution I'm currently using as well as a basis for comment.
In the Controller:
#new_address = Address.new( params[:order][:address] )
#order.address = new_address
#order.update_attributes( params[:order] )
In the Model:
def address=( address )
return unless address and address.is_a? Address
duplicate_address = Address.where( address_1: address.address_1,
address_2: address.address_2,
[etc. etc.] ).first
if duplicate_address
self.address_id = duplicate_address.id
else
address.save
self.address_id = address.id
end
end
I it's truly a :has_one relationship as you say and not a :has_many, you don't need to explicitly assign the address like you do in your own answer. That's what accepts_nested_attributes is for, after all. This line by itself should work:
#order.update_attributes( params[:order] )
That should create a new address if none exists, and update an existing one.
Your solution may work, but it a) doesn't take advantage of accepts_nested_attributes and b) will leave lots of orphaned addresses in your database.

Access join table data in rails :through associations

I have three tables/models. User, Alliance and Alliance_Membership. The latter is a join table describing the :Alliance has_many :Users through :Alliance_Membership relationship. (:user has one :alliance)
Everything works ok, but Alliance_Membership now has an extra field called 'rank'. I was thinking of the best way to access this little piece of information (the rank).
It seems that when i do "alliance.users", where alliance is the user's current alliance object, i get all the users information, but i do not get the rank as well. I only get the attributes of the user model. Now, i can create a helper or function like getUserRole to do this for me based on the user, but i feel that there is a better way that better works with the Active Record associations. Is there really a better way ?
Thanx for reading :)
Your associations are all wrong - they shouldn't have capital letters. These are the rules, as seen in my other answer where i told you how to set this up yesterday :)
Class names: Always camelcase like AllianceMembership (NOT Alliance_Membership!)
table names, variable names, methods and associations: always underscored and lower case:
has_many :users, :through => :alliance_memberships
To find the rank for a given user of a given alliance (held in #alliance and #user), do
#membership = #alliance.alliance_memberships.find_by_user_id(#user.id)
You could indeed wrap this in a method of alliance:
def rank_for_user(user)
self.alliance_memberships.find_by_user_id(user.id).rank
end

Resources