Rails: How to make cancan work on each object? - ruby-on-rails

user.rb
def has_delete_role? name
roles.each do |n|
return true if n == name
end
end
ability.rb
if user.has_delete_role? :business_delete
can :destroy, Business
end
index.html.erb
<% if can? :destroy, #business %>
<%= link_to 'delete', business_path(#business.id), method: :delete%>
<% end %>
This piece of code allow user who has the authority to access delete button. Here if a user has authority, he can access delete buttons of all objects. EX: Business has 10 objects id = 1 to id = 10, user can access all of 10 delete buttons if he has the authority
But now I want to set the authority base on object.
EX: Buisness also 1 to 10, user can only see button 2 and 5 because there is a field in user data table called auth_ids [], it stores [2,5]
How to achieve this?

You can use:
can :destroy, Business, id: user.auth_ids

You can set up a condition, something similar to the guide here: https://github.com/CanCanCommunity/cancancan/wiki/Defining-Abilities
can :destroy, Business, Business.where('id = ?', user.auth_ids)

Related

Rails Need help to capture form field in model from product view

I need to capture a field added by a user in a form_for, inside the product show page.
My product.rb model as follows:
belongs_to :user
has_many :complaints
My complaint.rb model as follows:
belongs_to :product
belongs_to :user
My user.rb model as follows:
has_many :products
My product controller is a basic controller with all the new, create, edit, update actions and all the routes are good.
User looks at the product show page like this, and it's all good
http://localhost:3000/products/1
My goal is to create a complaint from the product show page, when user views the specific product. So I have created a complaints_controller.rb to capture all the details of the product, and create a complaint. I have an issue with capturing the complaint_number which is a field inside the complaints table.
Here is my form inside the product show page
<%= form_for([#product, #product.complaints.new]) do |f| %>
<%= f.number_field :complaint_number, placeholder: "Enter complaint number you were given" %>
<%= f.submit 'Complaint' %>
<% end %>
Here is my complaints_controller.rb
Goal is to capture the complaint_number fields and run the make_complaint method to create a complaint and populate rest of the fields in the newly created row of the complains table.
class ComplaintsController < ApplicationController
before_action :authenticate_user!
def create
# Will Get product_id from the action in the form in product show page.
product = Product.find(params[:product_id])
# This complaint_number does not seem to work
complaint_number = product.complaints.find_by(complaint_number: params[:complaint_number])
# Now I want to run a make_complaint method and pass the product and the complaint number. This fails, I can't capture the complaint_number in the form from user input.
make_complaint(product, complaint_number)
redirect_to request.referrer
end
private
def make_complaint(product, complaint_number)
complaint = product.complaints.new
complaint.title = product.title
complaint.owner_name = product.user.name
complaint.owner_id = product.user.id
# Note: complaint_number and current_complaint are a fields in the Orders table
# Note:
complaint.current_complaint = complaint_number
if complaint.save
flash[:notice] = "Your complaint has been sent!"
else
flash[:alert] = complaint.errors.full_messages
end
end
end
For routes I have added resources :complaint, only: [:create] inside the resources of products to get products/:id/complaints
My routes.rb is like this
Rails.application.routes.draw do
get 'products/new'
get 'products/create'
get 'products/edit'
get 'products/update'
get 'products/show'
root 'pages#home'
get '/users/:id', to: 'users#show'
post '/users/edit', to: 'users#update'
resources :products do
member do
delete :remove_image
post :upload_image
end
resources :complaint, only: [:create]
end
devise_for :users, path: '', path_names: { sign_in: 'login', sign_up: 'register', sign_out: 'logout', edit: 'profile' }
Your form has complaint_quantity:
<%= form_for([#product, #product.complaints.new]) do |f| %>
<%= f.number_field :complaint_quantity, placeholder: "Enter complaint number you were given" %>
<%= f.submit 'Complaint' %>
<% end %>
Your controller has complaint_number:
complaint_number = product.complaints.find_by(complaint_number: params[:complaint_number])
If you check your params from the server log, I bet you'll see the value you are looking for is coming across as complaint_quantity and not complaint_number.
UPDATE
With the form misspelling corrected, the error persists, so let's check into more areas:
complaint_number = product.complaints.find_by(complaint_number: params[:complaint_number])
So, break that down:
1. What does params actually include?
Is :complaint_number being submitted from the form?
If not, the form still has an error somewhere.
2. Does product.complaints actually include a complaint that could be matched by complaint_number?
I don't know your data structure well enough to tell, but it looks to me like you might actually want to do:
Complaint.find_by(complaint_number: params[:complaint_number])
instead of:
products.complaints.find_by(complaint_number: params[:complaint_number])
UPDATE #2
You know the problem is with your params.
I'm confident you aren't accessing your params correctly since you are using a nested form:
form_for([#product, #product.complaints.new])
Should mean your params are structured like { product: { complaint: { complaint_number: 1234 }}}
So params[: complaint_number] is nil because it should really be something like params[:product][:complaint][:complaint_number]
Please look at your server log in your terminal right after you submit the form to see the structure of your params. Or insert a debugger in the controller action and see what params returns.
ALSO, Instead of accessing params directly, you should whitelist params as a private method in your controller.
Something along these lines:
private
def product_complaint_params
params.require(:product).permit(:id, complaint_params: [ :complaint_number ])
end
See this: https://api.rubyonrails.org/classes/ActionController/StrongParameters.html

Updating rails 6 params with a link_to

Hello I have a lists of invoices that belong to a business and also the business belongs to a user, I am trying to have a button (link to) on a table in which all the invoices are listed for the user to be able to update the status of the invoice.
Pretty much if the user hits the link it will change from paid: true to paid: false and viseversa.
here are the routes:
resources :businesses do
resources :invoices
end
Here is the section of the table in which the link is:
<% if invoice.paid %>
<td><%= link_to "Mark as Not Paid", business_invoice_path(current_user, invoice), method: 'put', data: {paid: false} %></td>
<% else %>
<td><%= link_to "Mark as Paid", business_invoice_path(current_user, invoice), method: 'put', data: {paid: true}%></td>
<% end %>
Note: The paid column is a boolean on the db
Since, the paid column is present on Invoice, it is much better if you handle it at the controller or model level instead of getting the value from the form.
Remove if else conditions and combine it as below:
<%
invoice_text = invoice.paid ? 'Mark as Not Paid' : 'Mark as Paid'
%>
<td><%= link_to invoice_text, business_invoice_path(invoice), method: :put %></td>
In the Business::InvoicesController you can write the logic in update like this:
Business::InvoicesController < ApplicationController
before_action :authenticate_user!
before_action :set_invoice
def update
# TODO: find the business using the invoice
# have a check in place to authorize the
# transaction (if invoice belongs a business which
# belongs the current_user or not, if not then raise Unauthorized error)
# if business does belongs to the current_user then proceed to next step
# invert the value of paid column based on existing value
#invoice.update(paid: !#invoice.paid)
end
private
def set_invoice
#invoice = Invoice.find(params[:id])
end
end
With logic above, you can forget about maintain/finding the value of paid column since you have an option to revert the value of paid to true and back to false. Also, that I assumed you are using Devise for authentication.

How to destory all users but current user(admin) in ruby

I have this setup where it destroys all users but i want it not to destroy current user which is admin.
controller.
def remove_all
User.destroy_all
redirect_to(admin_users_path, { flash: { success: 'You have wiped all the data on the website!' } })
end
navigation.html
<%= link_to "Nuke Button", remove_all_profiles_path, :method => :get %>
route:
resources :profiles do
member do
get :delete
end
collection do
get 'remove_all'
end
end
I know I have to add something to the controller just don't know what to add
I'm assuming you have access to the current_user in your controller (or if not that, you know the id of the current user somehow
User.where.not(id: current_user.id).destroy_all
Note: In Rails 7 you will also be able to do
User.excluding(current_user).destroy_all
Which is a bit nicer maybe, but this doesn't work yet.
https://blog.saeloun.com/2021/03/08/rails-6-1-adds-excluding-to-active-record-relation.html

CanCan::Error - The can? and cannot? call cannot be used with a raw sql 'can' definition

ability.rb
can :destroy, Business, Business.where(id: user.auth_ids)
index.html.erb
<% if can? :destroy, #business %>
<%= link_to 'delete', business_path(#business.id), method: :delete%>
<% end %>
I want to access delete button if user field auth_ids: [] contains business id.
But I got error like this
CanCan::Error - The can? and cannot? call cannot be used with a raw sql 'can' definition.
Anybody could tell me what causes the problem and how to fix it?
This seems to happen when the syntax isn't quite right. Try a hash with your condition:
# ability.rb
can :destroy, Business, id: user.auth_ids
or a block condition with your scope:
# ability.rb
can :destroy, Business, Business.where(id: user.auth_ids) do
#... additional logic
end

Rails Nil Method Issue

I've been borrowing some code from an old ruby on rails 3 app of mine for a new rails 4 app. The code works on the old site, but on the new one it doesn't.
Here's my routes.rb:
scope ':username' do
resources :recipes
end
get "/:username" => "recipes#index"
Here's my controller index:
def index
#user = User.find_by_username params[:username]
#recipes = Recipe.all
end
and my view:
<% #recipes.each do |recipe| %>
<tr>
<td><%= recipe.name %></td>
<td><%= link_to recipe.name, recipe_path(username: #user.username, id: recipe.id) %></td>
<td><%= link_to 'Edit', edit_recipe_path(recipe) %></td>
<td><%= link_to 'Destroy', recipe, method: :delete, data: { confirm: 'Are you sure?' } %></td>
</tr>
<% end %>
but the error pops up:
undefined method `username' for nil:NilClass
but the current user username is set to test so its not nil, and the error shouldn't be popping up.
Thanks for all help!
When you have variables that could be potentially be nil, you should handle the case where they are nil, unless you explicitly wish for it to fail in these circumstances.
The dynamic find_by methods return nil when no records are found. Hence, as alfonso pointed out, you are getting a null value for #user.
In this particular instance, I would question how you are using the username param; if recipes are associated with recipes, then I would set up a has_many :recipes in my user model, and belongs_to :user in my recipe model.
Since the user is the 'parent' here, I would opt to create a recipes action in the UsersController. It seems more logical to me to put the recipes that belong to a user in the user's controller, and access the recipes as a collection from the user.
Alternatively, if you are trying to show recipes associated with a user, I would put the action in the RecipesController, and get the user that belongs to a recipe by using the #user method set up from the belongs_to relationship in the database.
In either case, you'll want to guarantee that the users and/or any recipes are defined before trying to render a page. You might want to display a 501 error or something similar if a user doesn't exist that's trying to be accessed, etc.
If you insist that there should always be a user for a recipe, then you should add that type of validation to the recipes model, so that adding a recipe without a user is disallowed:
validates :user, :presence => true
Sorry if I went a little off tangent.
get "/:username"
Here - :username is in position of id
#user = User.find_by_username params[:username]
Here you are trying to find it by params.
Link should look like this http://localhost:3000/user_name/user_name?username=user_name
to find some user, and it is obviously not what you want to achieve.
get "/:id" => "recipes#index"
#user = User.find(params[:id])

Resources