Hey. I'm starting in Rails and I guess my question is pretty easy. I have 2 models:
class Book < ActiveRecord::Base
belongs_to :owner
end
class Owner < ActiveRecord::Base
has_many :books
end
I'm trying to get the owner of the book on the show method, but everything I do says I can't find an Owner without an ID.
My controller has:
def show
#book = Book.find(params[:id])
#owner= Owner.find(params[:owner_id])
end
And my view:
<%= link_to owner.name, owner %>
Thanks!
Follow the relationship from the book to the owner, you don't even have to do this in the controller.
def show
#book = Book.find(params[:id])
end
In your views:
<%= link_to #book.owner.name, #book.owner %>
As you are using belongs_to :owner you can use it like this:
def show
#book = Book.find(params[:id])
#owner= book.owner
end
In your view you have to use these global # variables: #book, #owner. book and owner won't work.
Related
I've implemented polymorphic comments on my Rails app using this tutorial.
So I have:
class Place < ApplicationRecord
has_many :comments, as: :commentable
end
class Comment < ApplicationRecord
belongs_to :commentable, polymorphic: true
end
Now I want to index all comments and join them with the places they comment on, but I have no idea on how to achieve this.
In my comments_controller.rb I have:
class CommentsController < ApplicationController
def index
#comments = Comment.all
end
end
And in my comments index.html.erb I can access:
<%= comment.commentable_type %>
<%= comment.commentable_id %>
But how do I access the actual attributes of what the comments are on, example: <%= comment.place.name %>?
I believe it is related to this answer but I don't know how: rails joins polymorphic association
You can refer to the commentable object with just
comment.commentable
The only thing to watch out for here is that it assumes that all your commentables have the method you call on them, such as name.
comment.commentable.name
You will want to preload these in your controller to avoid an N+1 as well.
class CommentsController < ApplicationController
def index
#comments = Comment.includes(:commentable).all
end
end
mccalljt answer's above is much better
Believe I've got it:
Added to comment.rb:
belongs_to :place, foreign_key: 'commentable_id'
Added to comments_controller.rb:
#comments = Comment.where(commentable_type: "Place").joins(:place)
Though this is not reusable when there are more associations, but I could change it to:
#place_comments = Comment.where(commentable_type: "Place").joins(:place)
#other_comments = Comment.where(commentable_type: "Other").joins(:other)
I am trying to make an app in Rails 4.
I just asked this related question and got a clear answer. It seems I can't understand how to take that logic and apply it elsewhere.
Rails How to show attributes from a parent object
I have a user model, profile model a projects model and a universities model.
Associations are:
Profile belongs to university
Profile belongs to user
University has many profiles
University has many projects
Projects HABTM user
Projects belong to universities
In my projects controller, I define #creator as follows:
def create
logger.debug "xxx create project"
#authorise #project
#project = Project.new(project_params)
#project.creator_id = current_user.id
#project.users << current_user
respond_to do |format|
if #project.save
format.html { redirect_to #project }
format.json { render action: 'show', status: :created, location: #project }
else
format.html { render action: 'new' }
format.json { render json: #project.errors, status: :unprocessable_entity }
end
end
end
I try to define creator_profile like this:
def show
#authorise #project
#project = Project.find(params[:id])
#creator = User.find(#project.creator_id)
#creator_profile = #creator.profile
end
In my uni table, I have attributes called logo and name. I use avatar uploader in which i have logo defined (that's why I have two .logo below).
In my projects, show, I want to display the university that the project creator belongs to.
I've tried this:
<%= image_tag(#creator_profile.university.logo.logo) %>
<div class="generaltext"><%= #creator_profile.university.name %> </div>
I get this result: undefined method `logo' for nil:NilClass
Based on the link to my problem above
<%= image_tag(creator_profile.university.logo.logo) %>
<div class="generaltext"><%= creator_profile.university.name %> </div>
I get this result:
undefined local variable or method `creator_profile' for #<#<Class:0x007f998f17ad88>:0x007f998d1ce318>
I'm not sure I understood the very detailed explanations given in the answer to my previous question. If the first version is right, then I don't understand the explanation at all. If the second version is right, then why does this error message come up?
Im wondering if the problem arises out of there not being an association between university and user? I was hoping, based on the user who created the project, to find the uni that the creator belongs to.
That's why i tried:
<%= image_tag(creator_profile.project.university.logo.logo) %>
<div class="generaltext"><%= creator_profile.project.university.name %> </div>
I get this error:
undefined method `project' for #<Profile:0x007f998ada41b8>
I think that you need to understand some basic concepts of Ruby and Ruby and Rails to solve this question yourself.
In ruby, vars with # are instance variables and are available all over the class. That means that they will be available in your view if you declare them in your controller.
EG #creator_profile = #profile.user
On the other hand, vars without # are only available inside the same block.
An example:
#controller
#users = User.all ##users, instance variable
#view
<% #users.each do |user| %>
<h3><%= user.name %></h3> #user, local variable. This will work
<% end %>
<h3><%= user.name %></h3> #this won't work because it is outside the block
Google about ruby vars and scopes.
Also, I think that you are relying too much on 'rails magic' (or you are skipping some code lines), if you don't declare an instance var, it won't exist. Naming conventions don't work that way.
At last but not at least, having a look at your relations, I think that they need some refactor. Also the use of singular and plural is not correct. I know that it's not real code but it denotes that they don't reflect real relationships between entities.
Don't try to make 'octopus' models, where everybody belongs to everybody, and think about the relationships itself, not only trying to associate models. EG:
Profile
belongs_to :creator, class_name: 'User'
This way you can write:
#controller
#profile_creator = Profile.find(params[:id]).creator
#view
#profile_creator.university
You will understand better what you are doing.
Hope it helps.
It seems I can't understand how to take that logic and apply it elsewhere.
I don't think you appreciate how ActiveRecord associations work in Rails. I'll explain further down the page.
Your associations will be the likely cause of the problem.
Setting up complicated associations is always tricky - it's best to keep the data as separate as possible.
Here's how I'd construct the models / associations:
#app/models/university_student.rb
class UniversityStudent < ActiveRecord::Base
belongs_to :university
belongs_to :student, class_name: "User" #-> student_id
end
#app/models/user.rb
class User < ActiveRecord::Base
has_many :placements, class_name: "UniversityStudent", foreign_key: :student_id #-> user.placements
has_many :universities, through: :placements #-> user.universities
has_and_belongs_to_many :projects #-> user.projects
has_one :profile #-> user.profile (avatar etc)
has_many :created_projects, class_name: "Project", foreign_key: :creator_id
end
#app/models/profile.rb
class Profile < ActiveRecord::Base
belongs_to :user #-> store avatar here. This can be used across entire app
end
#app/models/university.rb
class University < ActiveRecord::Base
has_many :projects
has_many :students, class_name: "UniversityStudent" #-> university.students
end
#app/models/project.rb
class Project < ActiveRecord::Base
belongs_to :university
belongs_to :creator, class_name: "User" #-> creator_id
has_and_belongs_to_many :users
delegate :profile, to: :creator, prefix: true #-> #project.creator_profile
end
This allows you to do the following:
def create
#project = curent_user.created_projects.new project_params
#project.users << current_user
Because the associations actually associate your data, you'll be able to do the following:
def show
#project = Project.find params[:id]
##creator_profile = #project.creator.profile
#creator_profile = #project.creator_profile #-> if you use the delegate method outlined in the models
end
--
In my projects, show, I want to display the university that the project creator belongs to.
#app/controllers/projects_controller.rb
class ProjectsController < ApplicationController
def show
##project = Project.find params[:id]
#project = current_user.created_projects.find params[:id]
end
end
#app/views/projects/show.html.erb
<%= #project.creator.universities.first %>
My code above allows for multiple universities. Thinking about it, it should be limited to one, but I'll leave it as is for now, maybe change it later.
In my uni table, I have attributes called logo and name. I use avatar uploader in which i have logo defined (that's why I have two .logo below).
Don't use two logo method, it's an antipattern (explained below)
The fix for this is two-fold:
Firstly, make sure you're calling #creator_profile.university with the following:
<%= #creator_profile.university %>
If this works, it means you have a problem with .logo.logo (detailed below), if it doesn't, it means you've not defined #creator_profile or the university association correctly.
Secondly, you need to ensure you have the correct controller/view setup.
The problem for many people - especially beginners - is they simply don't understand the way Rails works with controllers & views. You need to appreciate that each time you render a view, the only data it has access to is that which you define in the corresponding controller action...
#app/controllers/projects_controller.rb
class ProjectsController < ApplicationController
def show
#project = Project.find params[:id]
#creator_profile = #project.creator_profile
end
end
#app/views/projects/show.html.erb
<%= content_tag :div, #creator_profile.universities.first.name, class: "generaltext" %>
Trivia
#project.creator_id = current_user.id
This should not have to be defined.
You should be able to change the foreign_key in the association, so that Rails will automagically define the creator_id for you:
#app/models/project.rb
class Project < ActiveRecord::Base
belongs_to :creator, class: "User" #-> foreign_key should be :creator_id
end
#app/controllers/projects_controller.rb
class ProjectsController < ApplicationController
def create
#project = current_user.created_projects.new project_params #-> populates foreign key automatically.
--
.logo.logo
This is an antipattern.
Calling the same method twice is simply bad practice - why are you doing it?
You either want to delegate any recursive data you're trying to access (such as the example with .creator_profile above), or you'll want to restructure that functionality.
You want the following:
If you have to delegate to an assets model, you could get away with the following:
<%= #creator_profile.university.images.logo %>
I have a tutorial model and a tutorial_category model. I have linked the two together using a has_many belongs_to relationship. In my tutorials index view, I am looping through the tutorials like so: <% #tutorials.each do |tutorial| %>. Inside that loop, I am wanting to display the category each tutorial belongs to. I'm trying to do it like <%= tutorial.tutorial_categories.title %> (title is an attr in the tutorial_category model and I also have :tutorial_id as an attribute in the tutorial_category model. And :tutorial_category_id is an attr in the tutorial model, for that matter).
Here is the index action in my tutorials controller:
def index
#tutorials = Tutorial.all
#tutorial = Tutorial.new
#tutorial_categories = TutorialCategory.select("DISTINCT title, id")
respond_to do |format|
format.html # index.html.erb
format.json { render :json => #tutorials }
end
end
I can't figure out what I'm doing wrong here. From my experience, this should all work correctly, although it's been a few months since I've written any Ruby code so I'm probably missing something stupid here. Any help would be appreciated!
Update: my models
class Tutorial < ActiveRecord::Base
attr_accessible :content, :title, :tutorial_category_id
belongs_to :tutorial_category
end
class TutorialCategory < ActiveRecord::Base
attr_accessible :title, :tutorial_id
has_many :tutorials
end
If you want to be able to list the tutorial categories next to tutorials then do the following:
def index
#tutorials = Tutorial.includes(:tutorial_category)
#tutorial = Tutorial.new
...
end
end
Then you can iterate over them
<% #tutorials.each do |tutorial| %>
<%= tutorial.tutorial_category.title %>
<% end %>
Abbreviated models:
class Event < ActiveRecord::Base
belongs_to :place
accepts_nested_attributes_for :place
end
class Place < ActiveRecord::Base
has_many :events
end
Abbreviated events controller:
def new
#event = Event.new
#event.build_place
def create
#event = Event.new(params[:event])
respond_to do |format|
if #event.save
Abbreviated view:
<%= form_for(#event) do |f| %>
<%= fields_for #event.place do |place_f| %>
Given the above...
I want a user to be able to create an event. When they create the event, they have the option of adding a place. The place may or may not exist in the database.
Right now, the place isn't associated or created on form submission, but it is definitely in the post parameters.
Any thoughts on what I'm doing wrong? This is for Rails 3.
If you dont want to let user fill place attributes, remove #event.build_place from your new action in controller like this
def new
#event = Event.new
end
and remove <%= fields_for #event.place do |place_f| %> block from your view.
The Getting Started Rails Guide kind of glosses over this part since it doesn't implement the "new" action of the Comments controller. In my application, I have a book model that has many chapters:
class Book < ActiveRecord::Base
has_many :chapters
end
class Chapter < ActiveRecord::Base
belongs_to :book
end
In my routes file:
resources :books do
resources :chapters
end
Now I want to implement the "new" action of the Chapters controller:
class ChaptersController < ApplicationController
respond_to :html, :xml, :json
# /books/1/chapters/new
def new
#chapter = # this is where I'm stuck
respond_with(#chapter)
end
What is the right way to do this? Also, What should the view script (form) look like?
First you have to find the respective book in your chapters controller to build a chapter for him. You can do your actions like this:
class ChaptersController < ApplicationController
respond_to :html, :xml, :json
# /books/1/chapters/new
def new
#book = Book.find(params[:book_id])
#chapter = #book.chapters.build
respond_with(#chapter)
end
def create
#book = Book.find(params[:book_id])
#chapter = #book.chapters.build(params[:chapter])
if #chapter.save
...
end
end
end
In your form, new.html.erb
form_for(#chapter, :url=>book_chapters_path(#book)) do
.....rest is the same...
or you can try a shorthand
form_for([#book,#chapter]) do
...same...
Try #chapter = #book.build_chapter. When you call #book.chapter, it's nil. You can't do nil.new.
EDIT: I just realized that book most likely has_many chapters... the above is for has_one. You should use #chapter = #book.chapters.build. The chapters "empty array" is actually a special object that responds to build for adding new associations.
Perhaps unrelated, but from this question's title you might arrive here looking for how to do something slightly different.
Lets say you want to do Book.new(name: 'FooBar', author: 'SO') and you want to split some metadata into a separate model, called readable_config which is polymorphic and stores name and author for multiple models.
How do you accept Book.new(name: 'FooBar', author: 'SO') to build the Book model and also the readable_config model (which I would, perhaps mistakenly, call a 'nested resource')
This can be done as so:
class Book < ActiveRecord::Base
has_one :readable_config, dependent: :destroy, autosave: true, validate: true
delegate: :name, :name=, :author, :author=, :to => :readable_config
def readable_config
super ? super : build_readable_config
end
end