I have a set of nested resources consisting of users, books, and chapters. Here's how it looks.
Models
class User
has_many :books, dependent: :destroy
accepts_nested_attributes_for :books, allow_destroy: true
end
class Book
belongs_to :user
has_many :chapters, dependent: :destroy
accepts_nested_attributes_for :chapters, allow_destroy: true
end
class Chapter
belongs_to :book
end
Chapter Controller
def create
#chapter = #book.chapters.build(params[:chapter])
if #chapter.save
flash[:success] = "A new chapter created!"
redirect_to blah blah
else
render 'new'
end
end
protected
def get_book
#book = Book.find(params[:chapter][:book_id]) ||
Book.find(params[:book_id])
end
You might be wondering why I have that protected method. I'm trying to let users create chapters and books in separate pages and still have the convenience of having nested resources. So a user can create a chapter on the chapter creation page and associate the chapter with the right book via association form.
Currently I'm stuck because the chapter resource is not getting the user id it needs. I'm very new to web development so I might be doing some crazy things here. Any help would be greatly appreciated. I really want to get this to work.
EDIT: To give more detail on what I meant by "the chapter resource is not getting the user id it needs" - in the chapter model I wrote *validates :user_id, presence: true*. When I press the submit button on the chapter creation page, it gives an error saying user_id cannot be blank.
In order to be sure that the current user owns the chapter, and therefore the book, change the get_book method to
def get_book
#book = current_user.books.find(params.fetch(:chapter, {})[:book_id] || params[:book_id])
end
params.fetch makes sure that you don't get an exception when params[:chapter] is nil
I don't think the Chapter model should check that the user_id is present. Instead, the controller should have a before_filter that checks if the action is authorized for the current user.
Something like this:
class ChaptersController < ApplicationController
before_filter :authorized?, only: [:create]
def create
...
end
private
def authorized?
current_user && current_user.owns? Chapter.find(params[:id])
end
end
owns? would then be implemented on the User model, and current_user would be implemented in the ApplicationController.
Related
Im trying to make a program in which when a user is created, automatically a corporateprofile is automatically created along with it. However upon trying to view the corporate profile i keep running into the below error..
NameError in CorporateprofilesController#show
uninitialized constant User::Corporateprofiles
I have read through my code over and over for spelling mistakes however no avail, also please note I am using nested routes in which each user has on corporateprofile.
Routes.rb
Myapp::Application.routes.draw do
resources :users do
resources :searches
resources :corporateprofiles
end
end
Corporateprofiles Controller
class CorporateprofilesController < ApplicationController
before_action :require_user
def show
#corporateprofile = current_user.corporateprofiles.find(params[:id])
end
def edit
#corporateprofile = current_user.corporateprofiles.find(params[:id])
end
def update
#corporateprofile = Corporateprofile.find(current_user.corporateprofile.id)
if #corporateprofile.update_attributes(corporateprofile_params)
flash[:success] = "Profile Updated"
redirect_to current_user
else
flash.now[:error] = "Something went wrong"
render edit_user_corporateprofiles_path
end
end
private
def profile_params
params.require(:corporateprofile).permit(:companyname, :companylogo,:companybanner,:companywebsite,:companyindustry,:companytype, :companyheadquarters,:companysize,:companyvideo,:aboutus,:city,:state,:country)
end
end
Corporate Profile Model
class Corporateprofile < ActiveRecord::Base
belongs_to :user
end
User Model
class User < ActiveRecord::Base
after_create :build_profile
has_many :searches, dependent: :destroy
has_one :corporateprofiles, dependent: :destroy
def build_profile
Corporateprofile.create(user: self) # Associations must be defined correctly for this syntax, avoids using ID's directly.
end
has_secure_password
end
Have read all the similar stack overflow posts even the error checking ones however I'm still unable to figure out whats causing the error.
Any help would be so much appreciated
You have an error, because you broke rails naming conventions. So, for table name corporate_profiles:
Model file name should be corporate_profile.rb.
Model class name should be CorporateProfile.
Controller class name should be CorporateProfilesController.
has_one association name should be corporate_profile.
You are getting this error because you set up has_one :corporateprofiles. You used the plural of your model name, but rails expects singular. Therefor it is looking for the class Corporateprofiles.
First off - to fix this, change your Naming
It is convention to name classes CamelCase - so change your model to CorporateProfile. You will have to change your files to corporate_profile(_controller).rb as well.
This also makes your relations much easier to read and write.
class User
has_one :corporate_profile, dependent: :destroy
# ... other stuff
end
Secondly - I would suggest you improve your after_create callback. I find it much nicer to directly access self than calling the other model and defining user: self
def build_profile
self.corporate_profile = CorporateProfile.create
end
I'll ask my question first:
Will this code logically work and it is the right thing to do (best practices perspective)? First off, it looks strange having a user being passed to a static subscription method
User and magazine have a many to many relationship through subscriptions (defined below). Also you can see, I've used through joins instead of the has and belongs to many so that we can define a subscription model.
after creating a user they need to have default subscriptions. Following the single responsibility principle, I don't think a user should have to know what default magazines to subscribe to. So how, after a user has been created can I create default subscriptions. The user.likes_sports? user.likes_music? should define which subscriptions methods we want.
Am I on the right track? I don't have anyone to review my code, any code suggestions highly appreciated.
class User < ActiveRecord::Base
after_create create_default_subscriptions
has_many :magazines, :through => :subscriptions
has_many :subscriptions
def create_default_subscriptions
if self.likes_sports?
Subscription.create_sports_subscription(self)
end
end
end
class Subscription < ActiveRecord::Base
belongs_to :user
belongs_to :magazine
#status field defined in migration
def self.create_sports_subscription(user)
Magazine.where("category = 'sports'").find_each do |magazine|
user.subscriptions << Subscription.create(:user => user, :magazine => magazine, :status=>"not delivered")
end
end
.
.
end
class Magazine < ActiveRecord::Base
has_many :users, :through => :subscriptions
has_many :subscriptions
end
The code is too coupled in my view. This can get out of hand really easily.
The right way to do this in my view would be to create a new service/form that takes care of creating the user for you
class UserCreationService
def perform
begin
create_user
# we should change this to only rescue exceptions like: ActiveRecord::RecordInvalid or so.
rescue => e
false
end
end
private
def create_user
user = nil
# wrapping all in a transaction makes the code faster
# if any of the steps fail, the whole user creation will fail
User.transaction do
user = User.create
create_subscriptions!(user)
end
user
end
def create_subscriptions!(user)
# your logic here
end
end
Then call the code in your controller like so:
def create
#user = UserCreationService.new.perform
if #user
redirect_to root_path, notice: "success"
else
redirect_to root_path, notice: "erererererooooor"
end
end
I'm learning Rails (4.2 installed) and working on a social network simulation application.
I have setup an one to many relation between Users and Posts and now I'm trying to add also Comments to posts. After multiple tries and following the documentation on rubyonrails.org I ended up with the following setup:
User Model
has_many :posts, dependent: :destroy
has_many :comments, through: :posts
Post Model
belongs_to :user
has_many :comments
Comment Model
belongs_to :user
The comment is initiated from the Post show page, so the
Post Controller has:
def show
#comment = Comment.new
end
Now the question is: in Comments Controller , what is the correct way to create a new record.
I tried the below and many others, but without success.
def create
#comment = current_user.posts.comment.new(comment_params)
#comment.save
redirect_to users_path
end
(current_user is from Devise)
Also, afterwards, how can I select the post corresponding to a comment?
Thank you
You'll want to create a relation on Post, letting each Comment know "which Post it relates to." In your case, you'll likely want to create a foreign key on Comment for post_id, then each Comment will belong_to a specific Post. So, you'd add belongs_to :post on your Comment model.
So, your models become:
class User < ActiveRecord::Base
has_many :posts, dependent: :destroy
has_many :comments, through: :posts
end
class Post < ActiveRecord::Base
belongs_to :user
has_many :comments
end
class Comments < ActiveRecord::Base
belongs_to :user
belongs_to :post
end
Then, to create a Comment, you would want to do one of two things in your controller:
Load the Post corresponding to the Comment you are creating via the URI as a parameter.
Pass the Post ID along in the form in which calls the create method on Comment.
I personally prefer loading the Post from a parameter in the URI, as you'll have less checking to do as far as authorization for can the Comment in question be added to the Post - e.g. think of someone hacking the form and changing the ID for the Post that the form originally sets.
Then, your create method in the CommentsController would look like this:
def create
#post = Post.find(post_id_param)
# You'll want authorization logic here for
# "can the current_user create a Comment
# for this Post", and perhaps "is there a current_user?"
#comment = #post.comments.build(comment_params)
if #comment.save
redirect_to posts_path(#post)
else
# Display errors and return to the comment
#errors = #comment.errors
render :new
end
end
private
def post_id_param
params.require(:post_id)
end
def comment_params
params.require(:comment).permit(...) # permit acceptable Comment params here
end
Change your Comment model into:
class Comment < ActiveRecord::Base
belongs_to :post
belongs_to :user, through: :post
end
Pass the post_id from view template:
<%= hidden_field_tag 'post_id', #post.id %>
Then change your create action:
def create
#post = Post.find(params[:post_id])
#comment = #post.comments.build(comment_params)
#comment.user = current_user
redirect_to post_path(#post)
end
I'm trying to add a 'Collections' model to group Posts so that any user can add any Post they like to any Collection they've created. The Posts will have already been created by a different user. We are just letting other users group these posts in their own Collections. Basically like bookmarking.
What is the cleanest, and most Rails-ey-way of doing this?
I've created the model and run through the migration and what not. Also I've already created proper views for Collection.
rails g model Collection title:string user_id:integer
collections_controller.rb
class CollectionsController < ApplicationController
def index
#collections = current_user.collections.all
end
def show
#collection = Collection.all
end
def new
#collection = Collection.new
end
def create
#collection = current_user.collections.build(collection_params)
if #collection.save
redirect_to #collection, notice: 'saved'
else
render action: 'new'
end
end
def update
end
private
def collection_params
params.require(:collection).permit(:title)
end
end
collection.rb
class Collection < ActiveRecord::Base
belongs_to :user
has_many :posts
validates :title, presence: true
end
post.rb
has_many :collections
It seems like has_many or has_and_belongs_to_many associations are not correct? Should I be creating another model to act as an intermediary to then use
has_many :collections :through :collectionList?
If my association is wrong, can you explain what I need to change to make this work?
Also the next part in this is since this is not being created when the Post or Collection is created, I'm not sure the best way to handle this in the view. What is the best way to handle this, keeping my view/controller as clean as possible? I just want to be able to have a button on the Post#Show page that when clicked, allows users to add that post to a Collection of their own.
In such case you should use or has_and_belongs_to_many or has_many :through association. The second one is recommended, because it allows more flexibility. So now you should:
Create new model PostsCollections
rails g model PostsCollections post_id:integer collection_id:integer
and migrate it
Set correct model associations:
Something like:
class Post < ActiveRecord::Base
has_many :posts_collections
has_many :categories, through: :posts_collections
end
class Collection < ActiveRecord::Base
has_many :posts_collections
has_many :posts, through: :posts_collections
end
class PostsCollections < ActiveRecord::Base
belongs_to :post
belongs_to :collection
end
Then you'll be able to use
#collection.first.posts << #post
And it will add #post to #collection's posts
To add a post to a collection from view
Add a new route to your routes.rb, something like:
resources :collections do # you should have this part already
post :add_post, on: :member
end
In your Collections controller add:
def add_post
#post = Post.find(params[:post_id])
#collection = Collection.find(params[:id])
#collection.posts << #post
respond_to do |format|
format.js
end
end
As for views, you'll have to create a form to show a collection select and button to add it. That form should make POST method request to add_post_collection_path(#collection) with :post_id parameter.
You can read more explanations of how rails associations work in Michael Hartl's tutorial, because that subject is very wide, and can't be explained with short answer.
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