Rails Relationship (has_many/belongs_to) Completed - ruby-on-rails

So it's been quite a bit of time since I played with relationships and I want to make sure I've done it right.
In my model for Client I have:
class Client < ApplicationRecord
has_many :projects, dependent: :destroy
end
In my model for Projects I have:
class Project < ApplicationRecord
belongs_to :client
end
So I know that's set. Then to grab projects I put in my projects controller:
def create
#client = Client.find(params[:client_id])
#project = #client.project.new(project_params)
flash[:notice] = "Project created successfully" if #client.project << #project
respond with #project, location: admin_project_path
end
Would I need to put something in my show that does the same?
Anything else I'm missing for relationships?

I would think this:
def create
#client = Client.find(params[:client_id])
#project = #client.project.new(project_params)
flash[:notice] = "Project created successfully" if #client.project << #project
respond with #project, location: admin_project_path
end
Would look more like:
def create
#client = Client.find(params[:client_id])
#project = #client.projects.new(project_params)
if #project.save
# do success stuff
else
# do failure stuff
end
end
Note that
#project = #client.project.new(project_params)
should be:
#project = #client.projects.new(project_params)
As Yechiel K says, no need to do:
#client.project << #project
Since:
#project = #client.projects.new(project_params)
will automatically set client_id on the new #project. BTW, if you want to add a project to the client manually, then it's:
#client.projects << #project
(Note projects vs. project.)
In the off chance that there is not a client with params[:client_id], then #client = Client.find(params[:client_id]) will throw an error. You should probably include a rescue block. Alternatively, I prefer:
def create
if #client = Client.find_by(id: params[:client_id])
#project = #client.projects.new(project_params)
if #project.save
# do success stuff
else
# do failure stuff
end
else
# do something when client not found
end
end
Also, respond with isn't a thing. respond_with is a thing. (I believe it's been moved to a separate gem, responders.) It's unclear from your code if you're needing different responses, say, for html and js. If not, then I think it would be more like:
def create
if #client = Client.find_by(id: params[:client_id])
#project = #client.projects.new(project_params)
if #project.save
flash[:notice] = "Project created successfully"
redirect_to [#client, #project]
else
# do failure stuff
end
else
# do something when client not found
end
end
This assumes that your routes look something like:
Rails.application.routes.draw do
resources :clients do
resources :projects
end
end
In which case rails will resolve [#client, #project] to the correct route/path.
As DaveMongoose mentions, you could move #client = Client.find_by(id: params[:client_id]) into a before_action. This is quite common. Here's one discussion of why not to do that. Personally, I used to use before_action like this, but don't any more. As an alternative, you could do:
class ProjectsController < ApplicationController
...
def create
if client
#project = client.projects.new(project_params)
if #project.save
flash[:notice] = "Project created successfully"
redirect_to [client, #project]
else
# do failure stuff
end
else
# do something when client not found
end
end
private
def client
#client ||= Client.find_by(id: params[:client_id])
end
end
Taking this a bit further, you could do:
class ProjectsController < ApplicationController
...
def create
if client
if new_project.save
flash[:notice] = "Project created successfully"
redirect_to [client, new_project]
else
# do failure stuff
end
else
# do something when client not found
end
end
private
def client
#client ||= Client.find_by(id: params[:client_id])
end
def new_project
#new_project ||= client.projects.new(project_params)
end
end

I would replace this line:
flash[:notice] = "Project created successfully" if #client.project << #project
with:
flash[:notice] = "Project created successfully" if #project.save
No need to manually add #project to #client.projects, it gets added automatically when you create it using #client.projects.new, the only thing you missed was that creating something using .new doesn't persist it in the DB, that gets accomplished by calling #project.save.
For your show action, I'm not sure if you mean the client's show page or the project's, but in either case, you would retrieve it using params[:id] (unless you were using some nested routing).

Related

Best practice regarding controllers for a resource that can have different parent classes

I'm currently trying to refactor some controller code and I came accross some code that I'm not sure how to implement in a correct way.
My application has users and companies, and both can have projects.
The current situation is that we have 2 urls:
example.com/projects/*action (for user projects)
example.com/company/:company_id/projects/*action (for company projects)
Those will route to the same controller which will handle the request differently based on if a company_id exists or not. This is not very clean in my opinion so I have been thinking about a better way to do this.
So far, I think the best way is to split them up in seperate controllers, like:
Users::ProjectsController
Companies::ProjectsControler
But since the only difference between a user project and a company project is pretty much that one has a 'user_id' and the other has a 'company_id', it feels like that will not be very DRY as I'll be writing a lot of duplicate code.
The current solution probably isn't as much of a problem, but I want to do this the correct way, so was hoping that someone over here would have a suggestion on how to handle this.
Thanks in advance!
EDIT:
This is how my ProjectsController#create currently looks
def create
if params[:company_id]
company = current_user.get_companies.find(params[:company_id])
#project = Project.new(project_params)
#project.company_id = company.id
else
#project = Project.new(project_params)
#project.user_id = current_user.id
end
#project = Project.new(project_params)
if #project.save
flash[:notice] = "Project '#{#project.name}' created."
if #project.company
redirect_to company_project_path(#project.company, #project)
else
redirect_to project_path(#project)
end
else
flash[:error] = #project.errors.full_messages
if params[:company_id]
redirect_to new_company_project_path(params[:company_id], #project)
else
redirect_to new_project_path(#project)
end
end
end
It's mainly the if/else logic I'd like to get rid off
So i should probably just add company_id and user_id to the permitted_params and let use a function to put either one of them in the params...
Because you said the only difference is associating company_id vs user_id, as #TomLord said, you might find something like below work for you:
Assuming that you are using shallow nested routes:
class ProjectsController < ApplicationController
COLLECTION_ACTIONS = [:index, :new, :create].freeze
MEMBER_ACTIONS = [:show, :edit, :update, :destroy].freeze
before_action :set_associated_record, only: COLLECTION_ACTIONS
before_action :set_project, only: MEMBER_ACTIONS
def index
#projects = #associated_record.projects
# ...
end
def new
#project = #associated_record.projects.new
# ...
end
def create
#project = #associated_record.projects.new(project_params)
# ...
end
private
def set_project
#project = Project.find(params[:id])
end
def set_company
if params[:company_id].present?
#company = Company.find(params[:company_id])
end
end
# you might want to remove this set_user method, because perhaps you are already setting #user from sesssion
def set_user
if params[:user_id].present?
#user = User.find(params[:user_id])
end
end
def set_associated_record
set_company
set_user
#associated_record = #company || #user
end
end
Okay I managed to handle it in a way that I'm happy with.
ProjectsController#create now looks like this:
def create
#project = owner.projects.new(project_params)
if #project.save
flash[:notice] = "Project '#{#project.name}' created."
redirect_to action: :show, id: #project.id
else
flash[:error] = #project.errors.full_messages
#project = Project.new(project_params)
render action: :new
end
end
def owner
if params[:company_id]
return policy_scope(Company).find(params[:company_id])
else
return current_user
end
end
I added the owner class to return the entity that the project belongs to.
Any suggestions for improvements are still welcome though!

How does || work in?: #client = client.find(params[:client_id] || params[:id])

New to rails. Following a tutorial on polymorphic associations, I bump into this to set #client in create and destroy.
#client = Client.find(params[:client_id] || params[:id])
I'm normally only used to that you can only find #client = Client.find(params[:id])
so how does this work with there being two params? How does the || work?
FavoriteClientsController.rb:
class FavoriteClientsController < ApplicationController
def create
#client = Client.find(params[:client_id] || params[:id])
if Favorite.create(favorited: #client, user: current_user)
redirect_to #client, notice: 'Leverandøren er tilføjet til favoritter'
else
redirect_to #client, alert: 'Noget gik galt...*sad panda*'
end
end
def destroy
#client = Client.find(params[:client_id] || params[:id])
Favorite.where(favorited_id: #client.id, user_id: current_user.id).first.destroy
redirect_to #client, notice: 'Leverandøren er nu fjernet fra favoritter'
end
end
Full code for controller, models can be seen here
Using rails 5
Expression: params[:client_id] || params[:id] is the same as:
if params[:client_id]
params[:client_id]
else
params[:id]
end
Wow thats an incredibly bad way to do it.
A very extendable and clean pattern for doing controllers for polymorphic children is to use inheritance:
class FavoritesController < ApplicationController
def create
#favorite = #parent.favorites.new(user: current_user)
if #favorite.save
redirect_to #parent, notice: 'Leverandøren er tilføjet til favoritter'
else
redirect_to #parent, alert: 'Noget gik galt...*sad panda*'
end
end
def destroy
#favorite = #parent.favorites.find_by(user: current_user)
redirect_to #parent, notice: 'Leverandøren er nu fjernet fra favoritter'
end
private
def set_parent
parent_class.includes(:favorites).find(param_key)
end
def parent_class
# this will look up Parent if the controller is Parents::FavoritesController
self.class.name.deconstantize.singularize.constantify
end
def param_key
"#{ parent_class.naming.param_key }_id"
end
end
We then define child classes:
# app/controllers/clients/favorites_controller.rb
module Clients
class FavoritesController < ::FavoritesController; end
end
# just an example
# app/controllers/posts/favorites_controller.rb
module Posts
class FavoritesController < ::FavoritesController; end
end
You can then create the routes by using:
Rails.application.routes.draw do
# this is just a routing helper that proxies resources
def favoritable_resources(*names, **kwargs)
[*names].flatten.each do |name|
resources(name, kwargs) do
scope(module: name) do
resource :favorite, only: [:create, :destroy]
end
yield if block_given?
end
end
end
favoritable_resources :clients, :posts
end
The end result is a customizable pattern based on OOP instead of "clever" code.
The tutorial which teaches you to do
Client.find(params[:client_id] || params[:id])
is a super-duper bad tutorial :) I strongly recommend you to switch to another one.
Back to the topic: it is logical OR: if first expression is neither nil or false, return it, otherwise return second expression.
That thing is just trying to find client by client_id if there is one in the request params. If not it's trying to find client by id.
However such practic can make you much more pain than profit.

Updating TopicsController to allow a moderator to update topics, but not create or delete

I'm in the process of creating a website similar to Reddit. I would like to allow a moderator to be able to update a topic, but not be able to create or delete topic. I'm aware that I need to update TopicsController but I'm not sure how. My main problem is that I'm not sure how to make the code specific enough to ensure that a moderator can only update; not delete or create a topic, as an admin can.
My current code looks like this:
class PostsController < ApplicationController
before_action :require_sign_in, except: :show
before_action :authorize_user, except: [:show, :new, :create]
def show
#post = Post.find(params[:id])
end
def new
#topic = Topic.find(params[:topic_id])
#post = Post.new
end
def create
#post.body = params[:post][:body]
#topic = Topic.find(params[:topic_id])
#post = #topic.posts.build(post_params)
#post.user= current_user
if #post.save
flash[:notice] = "Post was saved"
redirect_to [#topic, #post]
else
flash[:error] = "There was an error saving the post. Please try again."
render :new
end
end
def edit
#post = Post.find(params[:id])
end
def update
#post = Post.find(params[:id])
#post.assign_attributes(post_params)
if #post.save
flash[:notice] = "Post was updated."
redirect_to [#post.topic, #post]
else
flash[:error] = "There was an error saving the post. Please try again."
render :edit
end
end
def destroy
#post = Post.find(params[:id])
if #post.destroy
flash[:notice] = "\"#{#post.title}\" was deleted successfully."
redirect_to #post.topic
else
flash[:error] = "There was an error deleting the post."
render :show
end
end
private
def post_params
params.require(:post).permit(:title, :body)
end
def authorize_user
post = Post.find(params[:id])
unless current_user == post.user || current_user.admin?
flash[:error] = "You must be an admin to do that."
redirect_to [post.topic, post]
end
end
end
I've already added a moderator role to the enum role.
I apologise if this seems really basic...but it has got me stumped!
Thanks in advance!
I could answer with some custom solution, but it's better to use a more structured and community-reviewed approach: authorization with cancan.
As tompave noticed you can use cancan gem for this.
Personally I prefer pundit.
In old days I used to define permissions directly in code everywhere: in controllers, in views and even models. But it's really bad practice. When your app grows, you are lost: you update a view, but you should make the same change in controller and sometimes in model too. It soon becomes absolutely unmanageable and you have no idea what your users can and cannot do.
Pundit, on the other hand, offers central place -- policy -- for defining what user can do. Views and controllers can then use those policies.
For example, if you need to define Post's policy you simply create app/policies/post_policy.rb file:
class PostPolicy
attr_reader :user
attr_reader :post
def initialize(user, post)
#user = user
#post = post
end
def author?
post.user == user
end
def update?
author? || user.admin? || user.moderator?
end
def create?
author? || user.admin?
end
def destroy?
author? || user.admin?
end
# etc.
end
Now whenever you need to check user's ability to perform action, you can simply invoke:
# in controller
def update
#post = Post.find(params[:id])
authorize #post
# do whatever required
end
# in view
<% if policy(post).update? %>
<%= link_to 'Edit Post', post_edit_path(post) %>
<% end %>
As you can see Pundit is very easy to comprehend and it uses the same "convention over configuration" approach as Rails. At the same time it's very flexible and allows you to test virtually anything.
You will definitely need Pundit or any similar gem to manage permission in your ambitious app.

converting the methods for a has_many association to a has_one association

I have 2 models, users, and common_apps.
users has_one :common_app.
Before this, I wrote the code as the users has_many common_apps, however I'm not sure how to rewrite that for a has_one association. The main confusion is how to structure 'new' in common_app controller.
When I try, I get an undefined method error.
undefined method `new' for #<CommonApp:>
This is my code -->
def new
if current_user.common_app.any?
redirect_to current_user
else
#common_app = current_user.common_app.new
end
end
def create
#common_app = current_user.common_app.build(common_app_params)
if #common_app.save
flash[:success] = "Common App Created!"
redirect_to root_url
else
redirect_to 'common_apps/new'
end
end
def show
#common_apps = current_user.common_app
end
how would you restructure this, if this were to be a has_one association?
I think I know how the 'create' one should be -->
def create
#common_app = current_user.build_common_app(common_app_params)
if #common_app.save
flash[:success] = "Common App Created!"
redirect_to root_url
else
redirect_to 'common_apps/new'
end
end
Your new action should look like this:
def new
if current_user.common_app.present?
redirect_to current_user
else
#common_app = current_user.build_common_app
end
end
You can also call build_common_app without any parameters passed to it, which will initialize an empty CommonApp for current_user.

How to restrict foreign keys in Rails' update controller action?

In my Rails app I have invoices which in turn can have many projects.
model:
class Invoice < ActiveRecord::Base
attr_accessible :project_id
end
controller:
class InvoicesController < ApplicationController
before_filter :authorized_user, :only => [ :show, :edit, :destroy ]
before_filter :authorized_project, :only => [ :create, :update ]
def create # safe
#invoice = #project.invoices.build(params[:invoice])
if #invoice.save
flash[:success] = "Invoice saved."
redirect_to edit_invoice_path(#invoice)
else
render :new
end
end
def update # not safe yet
if #invoice.update_attributes(params[:invoice])
flash[:success] = "Invoice updated."
redirect_to edit_invoice_path(#invoice)
else
render :edit
end
end
private
def authorized_user
#invoice = Invoice.find(params[:id])
redirect_to root_path unless current_user?(#invoice.user)
end
def authorized_project
#project = Project.find(params[:invoice][:project_id])
redirect_to root_path unless current_user?(#project.user)
end
end
My biggest concern is that a malicious user might, one day, create an invoice that belongs to the project of another user.
Now thanks to the help of some people on this board I managed to come up with a before_filter that makes sure that this won't happen when a project is created.
The problem is I don't understand how to apply this filter to the update action as well.
Since the update action does not make use of Rails' build function, I simply don't know how to get my #project in there.
Can anybody help?
In your case I would start from current_user, not #project (provided User has_many :invoices):
current_user.invoices.build(params[:invoice])
Also instead of explicitly check current_user?(#invoice.user) you can do:
def find_invoice
#invoice = current_user.invoices.find(params[:id])
end
def find_project
#project = current_user.projects.find(params[:invoice][:project_id])
end
Wrong invoice or project will throw 500 which you may or may not want to handle.
If User has_many :invoices, :through => :projects and Project hence has_many :invoices then:
def find_invoice
#invoice = #project.invoices.find(params[:id])
end
The #project.invoices.build method creates a new Invoice that is automatically associated with that particular #project. You don't have to do any work, and there's no risk of it being linked to the wrong project.
You'll want to be sure that project_id is not an accessible attribute, though.

Resources