How to define params with has_many through? - ruby-on-rails

I'm using has_many through relationship and I don't really get what should I do else to make it work.
I suppose there is something about parameters that I don't understand and omit. If so, please tell me where and how to write it, because I'm confused a little bit because of all these params.
book.rb:
class Book < ActiveRecord::Base
has_many :user_books
has_many :users, through: :user_books
end
user.rb:
class User < ActiveRecord::Base
devise :database_authenticatable, :registerable, :confirmable,
:recoverable, :rememberable, :trackable, :validatable
validates :full_name, presence: true
has_many :user_books
has_many :books, through: :user_books #books_users - book_id user_id
end
and books_controller.rb:
class BooksController < ApplicationController
before_action :is_admin?, except: [:show_my_books, :book_params]
before_filter :authenticate_user!
expose(:book, attributes: :book_params)
expose(:user_book)
expose(:user_books)
expose(:books){current_user.books}
def create
if book.save
redirect_to(book)
else
render :new
end
end
def update
if book.save
redirect_to(book)
else
render :edit
end
end
def show
end
def is_admin?
if current_user.admin?
true
else
render :text => 'Who are you to doing this? :)'
end
end
def book_params
params.require(:book).permit(:name, :author, :anotation, user:[:name])
end
end
When I create new book it gives me an error
Couldn't find Book with 'id'=27 [WHERE "user_books"."user_id" = ?]
<%=book.name%>
Sorry for a silly question, but I couldn't find a proper example to understand it myself that's why I ask you for help. Every help would be appreciated, thank you!

To setup a relation via a form you usually use a select or checkbox and pass the ID(s) of related item(s):
For a one to one relation the request would look like this:
POST /books { book: { name: 'Siddharta', author: 'Herman Hesse', user_id: 1 } }
For many to many or one to many you can use _ids:
POST /books { book: { name: 'Siddharta', author: 'Herman Hesse', user_ids: [1,2,3] } }
ActiveRecord creates a special relation_name_ids= setter and getter for has_many and HABTM relations. It lets you modify the relations of an object by passing an array of IDs.
You can create the form inputs like so:
<%= form_for(#book) do |f| %>
<%= f.collection_select(:author_ids, User.all, :id, :name, multiple: true) %>
OR
<%= f.collection_checkboxes(:author_ids, User.all, :id, :name) %>
<% end %>
To whitelist the user_ids params which should permit an array of scalar values and not a nested hash we pass an empty array:
def book_params
params.require(:book).permit(:name, :author, :anotation, user_ids: [])
end
On the other hand if you want to assign records to the current user it is better to get the user from the session or a token and avoid passing the param at all:
def create
#book = current_user.books.new(book_params)
# ...
end
This lets you avoid a pretty simple hack where a malicious user passes another users id or takes control of a resource by passing his own id.
As to your other error why it would try to create a strange query some sort of stack trace or log is needed.
However if you are new to Rails you might want to hold off a bit on the decent exposure gem. It obscures away a lot of important concepts in "magic" - and you'll spend more time figuring out how it works that might be better spent learning how good rails apps are built.

Related

With Ruby on Rails what's the best way to display data from a form field on a show page from another Ruby class?

I'm still somewhat new to Ruby and am having trouble displaying data on the show page from another class. I have two classes, Company and Job. On the Job show page I would like to display the Company's name, website and description from the Company form fields that created/posted the job when a job applicant views the respective job.
Was receiving an error when tinkering with the Job show controller action. Not entirely sure if the company is not being assigned an id when being created or if there's an issue with the show action login in the controller or a model association error on my end. Any help and explanation to resolve this issue is greatly appreciated.
Screenshot for Error Received on Job Show Page
Models
class Company < ApplicationRecord
has_many :jobs
has_many :job_applications, through: :jobs
class Job < ApplicationRecord
belongs_to :user
belongs_to :company, optional: true
has_many :job_applications, dependent: :destroy
class JobApplication < ApplicationRecord
belongs_to :user
belongs_to :job
Controllers
class CompaniesController < ApplicationController
private
# Use callbacks to share common setup or constraints between actions.
def set_company
#company = Company.find(params[:id])
# #company = self.create_company
end
# Only allow a list of trusted parameters through.
def company_params
params.require(:company).permit(:name, :website, :about, :user_id, :avatar)
end
class JobsController < ApplicationController
# GET /jobs/1 or /jobs/1.json
def show
#company = Company.find(params[:user_id])
# #company = Company.all
# #job = Job.find(params[:id])
end
Routes
resources :companies
resources :jobs
resources :jobs do
resources :job_applications
end
Job Show Page
<%= #company.name %>
<%= #company.website %>
<%= #company.about %>
I believe the problem lies in your show method in the JobsController.
It should look something like this:
class JobsController < ApplicationController
# GET /jobs/1 or /jobs/1.json
def show
#job = Job.find(params[:id])
#company = #job.company
end
This might throw some errors since you have optional: true in your relation. Also, I didn't care of n+1 queries since it's just a record, but this could be improved to be only 1 SQL query to the database.

Split table into two associated tables

I have an app that has a blog feature. Originally when I set this up a post post consisted of a title, body, keywords, and image link. I want to be able to filter posts based on keywords and I think the cleanest way to do this is to move keyword to their own table and associate the two tables. So each posts can have multiple keywords and each keyword can have multiple posts.
I created a migrations and model for keywords.
class CreateKeywords < ActiveRecord::Migration[6.1]
def change
create_table :keywords do |t|
t.string :keyword
t.timestamps
end
end
end
class Keyword < ApplicationRecord
has_and_belongs_to_many :posts
end
I associated that with the posts table and changed the posts model.
class CreatePostsKeywordsJoinTable < ActiveRecord::Migration[6.1]
def change
create_join_table :posts, :keywords do |t|
t.index [:post_id, :keyword_id]
t.index [:keyword_id, :post_id]
end
end
end
class Post < ApplicationRecord
belongs_to :user
has_and_belongs_to_many :keywords
validates :title, presence: true
validates :body, presence: true
accepts_nested_attributes_for :keywords
# validates :keywords, presence: true
end
For now I just commented out the keywords in the Post model. I'm not exactly sure if I need to remove it or not. I already have existing posts that I don't want to lose as part of this switchover so I'm trying to keep that I mind as I figure out how to make this work. Where I'm really confused is what I need to change in the controller.
This is my Post Controller:
require 'pry'
class Api::V1::PostController < ApiController
before_action :authorize_user, except: [:index, :show]
# INDEX /post
def index
render json: Post.all, each_serializer: PostSerializer
end
# SHOW /post/1
def show
render json: Post.find(params[:id]), serializer: PostShowSerializer
end
# CREATE /post/new
def create
binding.pry
post = Post.new(post_params)
post.user = current_user
if post.save
render json: post
else
render json: { errors: post.errors.full_messages }
end
end
# UPDATE /post/update
def update
post = Post.find(params[:id])
if post.update(post_params)
render json: post
else
render json: { errors: post.errors.full_messages }
end
end
# DESTROY /post/destroy
def destroy
post = Post.find(params[:id])
if post.destroy
render json: {destroyed: true}
end
end
protected
def post_params
params.require(:post).permit([:title, :body, :image, :keywords])
end
def authorize_user
if !user_signed_in? || current_user.role != "admin"
redirect_to root_path
end
end
end
In the above state when I get to this part post = Post.new(post_params) I get an error saying NoMethodError (undefined method 'each' for "authorlife":String). If I remove keywords from the post_params I get this error Unpermitted parameter: :keywords
I feel like I am missing one or more steps here but it's been awhile since I've done anything with associated tables like this.
UPDATE:
Followed some of the advice below and I updated the above code to how it currently looks. Current issue is that when I check post_parms in the #create method I'm no longer receiving keywords at all. I checked the frontend and it's sending keywords. I'm assuming it's my post_params that's causing the problem. I've tried adding the keywords nested attribute like this but keywords still isn't showing up in the post_params
def post_params
params.require(:post).permit(:title, :body, :image, :keywords_attributes => [:id, :keyword])
end
This is the WIP for the code I'm trying to implement. I'm not sure what the keywords part is supposed to look like once I get the params situation figured out.
params = { post: {title: post_params["title"], body: post_params["body"], image: post_params["image"], keywords_attributes: [{ keyword: 'keyword title' },]
}}
In the above state when I get to this part post = Post.new(post_params) I get an error saying NoMethodError (undefined method 'each' for "authorlife":String). If I remove keywords from the post_params I get this error Unpermitted parameter: :keywords
You need to setup nested attributes for keywords if you want to update them through a post.
class Post < ApplicationRecord
belongs_to :user
has_and_belongs_to_many :keywords
validates :title, presence: true
validates :body, presence: true
accepts_nested_attributes_for :keywords
end
You can then pass in params structured like this in your controller
params = { post: {
title: 'title', body: "body", keywords_attributes: [
{ text: 'keyword title' },
]
}}
post = Post.create(params[:post])
https://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html
I already have existing posts that I don't want to lose as part of this switchover so I'm trying to keep that I mind as I figure out how to make this work.
It's good practice to remove this not used data anymore. You should write a data migration which moves the existing keywords from the posts table to the keywords table. Something like this
class KeywordsMigrator
def run
Post.all.each do |post|
keyword = Keyword.find_or_create_by(title: post.keyword)
post.keywords << keyword
end
end
end
Finally you can drop the keyword column from post.
You haven't really mentioned the current structure of the posts table so I assume you have a keyword column there. If you have a keywords column you have to name your association different until you remove the column otherwise you will run into troubles. For example you rename it to keywords_v1 and specify the class_name.
class Post < ApplicationRecord
belongs_to :user
has_and_belongs_to_many :keywords_v1, class_name: "Keyword"
validates :title, presence: true
validates :body, presence: true
end
Or you rename the column first to something like deprecated_keywords.

Rails create action with multiple belongs_to

Trying to figure out a better way of assigning a review it's associated models.
I have the following classes:
class User < ActiveRecord::Base
has_many :reviews, dependent: :destroy
end
class Review < ActiveRecord::Base
belongs_to :user
belongs_to :restaurant
end
class Restaurant < ActiveRecord::Base
has_many :reviews, dependent: :destroy
end
Pretty straightforward stuff. A review must have a restaurant and a user. My create action looks like this:
def create
#restaurant = Restaurant.find(params[:restaurant_id])
#review = #restaurant.reviews.build(review_params)
#review.user = current_user
if #review.save
redirect_to #restaurant
else
render 'new'
end
end
private
def review_params
params.require(:review).permit(:content)
end
Currently I build the review for the restaurant and then I assign the review's user to the current user.
This all works fine but is there a cleaner way to build the associations?
Is there a way to add additional arguments to the build method alongside the strong params?
I looked at accepts_nested_attributes_for but I couldn't get it to work.
Thanks!
You can use merge in the review_params like below
def review_params
params.require(:review).permit(:content).merge(user_id: current_user.id)
end
so that you can erase this line #review.user = current_user in the create method
In your form, you can put a hidden field with the user_id that you want to assign:
<%= f.hidden_field :user_id, value: #user.id %>
Then, add it to your review_params:
params.require(:review).permit(:content, :user_id)

Setting a default value when creating a new object of a model

Im learning Rails and I'm just wondering if some code I wrote is correct and safe. I have two models, a user and post model. The posts belong to users, so I want to pass the user_id automatically to post when the object is created. I used an assign_attributes method in the post controller to set the user_id using the current_user helper provided by devise. Below is my relevant code. Again I want to know if this is correct or if there is better way of doing it.
def create
#post = Post.new(params[:post])
#post.assign_attributes({:user_id => current_user.id})
end
Post Model
class Post < ActiveRecord::Base
attr_accessible :content, :title, :user_id
validates :content, :title, :presence => true
belongs_to :user
end
User model
class User < ActiveRecord::Base
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
# Setup accessible (or protected) attributes for your model
attr_accessible :email, :password, :password_confirmation, :remember_me
has_many :posts
end
You're pretty close. Since you've 1) been provided the current_user convenience helper by Devise, and 2) configured User and Post as a has_many/belongs_to relationship, it makes sense to create the new post, then append it to current_user. Then, in your Post model, you'll want to break up validations for individual attributes – the way you've listed :content, :title in sequence won't work.
# app/controllers/posts_controller.rb
def create
post = Post.create(params[:post])
current_user.posts << post
end
# app/models/post.rb
class Post < ActiveRecord::Base
attr_accessible :content, :title, :user_id
validates :content, :presence => true
validates :title, :presence => true
belongs_to :user
end
I don't think that is necessary since you have already created the relationship between posts and users. If you nest the posts resources into the user, it will automatically create the relationship between the 2 models.
In routes.rb
resources :users do
resources :posts
end
With that done, you will now reference posts as #user.post. I have already shown an example in this question.
I would say something like this :
def create
params[:post][:user_id] = current_user.id
#post = Post.new(params[:post])
#post.save
end
or
def create
#post = Post.new(params[:post])
#post.user = current_user
if #post.save
...
else
...
end
end
or
def create
#post = #post.new(params[:post])
#post.user_id = current_user.id
#post.save
end
You could put the user_id in the params but that would not be safe. user_id should not be in 'attr_accessable' so it will be protected for mass_assignment.

devise and multiple "user" models

I'm using rails 3.2 and devise 2.0 and I'm quite new to Rails.
Requirements
I'd like to achieve the following:
have 2 or more "user" models, eg. Member, Customer, Admin
all models share some required fields (eg. email and password)
each model may have some unique fields (eg. company for Customer only)
some fields may be shared but not have the same validation (eg. name is required for Customer but optional for Member)
all fields must be filled during the registration process, so the forms are different
the login form should be unique
Possible solutions
I googled and searched StackOverflow for quite a long time, but nothing seems right to me (I'm a Java guy, sorry :) and now I'm quite confused. Two solutions came up:
Single devise user
That's the most frequent answer. Just create the default devise User and create relations between Member-->User and Customer-->User.
My concern here is how can I achieve a customized registration process for each model? I tried different things but all ended as a mess!
Multiple devise users
This solves the custom registration process, and seems right to me, but the unique login form is a blocker. I found an answer on SO (Devise - login from two model) which suggests to override Devise::Models::Authenticatable.find_for_authentication(conditions).
That seems complicated (?) and since I'm new to rails, I'd like to know if that could work?
Thanks for your advice!
Welcome aboard Java guy =), I hope you'll enjoy the Rails world.
Simply, to solve your issue you have 2 solutions:
For each user create a table in the database and corresponding model.
Create a single table in the database and for each user type create a model. This is called single table inheritance (STI).
Which one to choose?
It depends on the common attributes of the roles. If they are almost common (for example all have a name, email, mobile, ...) and a few attributes are different, I highly recommend the STI solution.
How to do the STI?
1. Simply create the the devise user model and table using the command rails generate devise User
2. Add a column named type with string datatype to the user table in the database using a migration.
3. For each user type create a model (for example rails g model admin)
4. Make the Admin class inherits from user model
class Admin < User
end
That's it you are done =) ... Yupeee
To create an admin run the command Admin.create(...) where the dots is the admin attributes for example the email, name, ...
I think this question could help you too
I'm in similar shoes as you, after trying all sorts of approaches I went with a single User model, which would belong to polymorphic roles. This seems like the simplest way to achieve single-login.
The User model would contain the information specific to log-in only.
The Role model would store fields specific to each role, as well as other associations specific to the role.
New registrations would be customized for each user type (roles) via individual controllers, and then building nested attributes for the User.
class User < ActiveRecord::Base
#... devise code ...
belongs_to :role, :polymorphic => true
end
class Member < ActiveRecord::Base
attr_accessible :name, :tel, :city #etc etc....
attr_accessible :user_attributes #this is needed for nested attributes assignment
#model specific associations like
has_many :resumes
has_one :user, :as => :role, dependent: :destroy
accepts_nested_attributes_for :user
end
Routes -- just regular stuff for the Member model.
resources :members
#maybe make a new path for New signups, but for now its new_member_path
Controller -- you have to build_user for nested attributes
#controllers/members_controller.rb
def new
#member = Member.new
#member.build_user
end
def create
#... standard controller stuff
end
views/members/new.html.erb
<h2>Sign up for new members!</h2>
<%= simple_form_for #member do |f| %>
# user fields
<%= f.fields_for :user do |u| %>
<%= u.input :email, :required => true, :autofocus => true %>
<%= u.input :password, :required => true %>
<%= u.input :password_confirmation, :required => true %>
<% end %>
# member fields
<%= f.input :name %>
<%= f.input :tel %>
<%= f.input :city %>
<%= f.button :submit, "Sign up" %>
<% end %>
I would like to point out that there is NO NEED to reach for nested_form gem; since the requirement is that User can only belong_to one type of Role.
I found a way to go and I'm quite happy with it so far. I'll describe it here for others.
I went with the single "user" class. My problem was to achieve a customized registration process for each pseudo model.
model/user.rb:
class User < ActiveRecord::Base
devise :confirmable,
:database_authenticatable,
:lockable,
:recoverable,
:registerable,
:rememberable,
:timeoutable,
:trackable,
:validatable
# Setup accessible (or protected) attributes for your model
attr_accessible :email, :password, :password_confirmation, :remember_me, :role
as_enum :role, [:administrator, :client, :member]
validates_as_enum :role
## Rails 4+ for the above two lines
# enum role: [:administrator, :client, :member]
end
Then I adapted http://railscasts.com/episodes/217-multistep-forms and http://pastie.org/1084054 to have two registration paths with an overridden controller:
config/routes.rb:
get 'users/sign_up' => 'users/registrations#new', :as => 'new_user_registration'
get 'clients/sign_up' => 'users/registrations#new_client', :as => 'new_client_registration'
post 'clients/sign_up' => 'users/registrations#create', :as => 'client_registration'
get 'members/sign_up' => 'users/registrations#new_member', :as => 'new_member_registration'
post 'members/sign_up' => 'users/registrations#create', :as => 'member_registration'
controllers/users/registrations_controller.rb:
I created a wizard class which knows the fields to validate at each step
class Users::RegistrationsController < Devise::RegistrationsController
# GET /resource/sign_up
def new
session[:user] ||= { }
#user = build_resource(session[:user])
#wizard = ClientRegistrationWizard.new(current_step)
respond_with #user
end
# GET /clients/sign_up
def new_client
session[:user] ||= { }
session[:user]['role'] = :client
#user = build_resource(session[:user])
#wizard = ClientRegistrationWizard.new(current_step)
render 'new_client'
end
# GET /members/sign_up
def new_member
# same
end
# POST /clients/sign_up
# POST /members/sign_up
def create
session[:user].deep_merge!(params[:user]) if params[:user]
#user = build_resource(session[:user])
#wizard = ClientRegistrationWizard.new(current_step)
if params[:previous_button]
#wizard.previous
elsif #user.valid?(#wizard)
if #wizard.last_step?
#user.save if #user.valid?
else
#wizard.next
end
end
session[:registration_current_step] = #wizard.current_step
if #user.new_record?
clean_up_passwords #user
render 'new_client'
else
#session[:registration_current_step] = nil
session[:user_params] = nil
if #user.active_for_authentication?
set_flash_message :notice, :signed_up if is_navigational_format?
sign_in(:user, #user)
respond_with #user, :location => after_sign_up_path_for(#user)
else
set_flash_message :notice, :"signed_up_but_#{#user.inactive_message}" if is_navigational_format?
expire_session_data_after_sign_in!
respond_with #user, :location => after_inactive_sign_up_path_for(#user)
end
end
end
private
def current_step
if params[:wizard] && params[:wizard][:current_step]
return params[:wizard][:current_step]
end
return session[:registration_current_step]
end
end
and my views are:
new.rb
new_client.rb including a partial according to the wizard step:
_new_client_1.rb
_new_client_2.rb
new_member.rb including a partial according to the wizard step:
_new_member_1.rb
_new_member_2.rb
So what's wrong? Just run rails g devise:views [model_name], customize each registration forms and in config/initializer/devise.rb just put config.scoped_views = true.

Resources