Rails 4: form_for with nested resource and without - ruby-on-rails

In my app, when a regular user logs in, they are dropped in on a dashboard that displays Service Requests for the company they belong_to.
When an admin logs in, they are dropped onto a dashboard that displays all of the company logos that can login and file Service Requests.
The views between a regular user and an admin user are virtually the exact same, outside of one or two entities on the form (which are controlled via cancan). So I am trying to be able to use the same form for if an admin creates an SR or a regular user creates an SR.
Routes.rb:
resources :service_requests do
resources :notes
end
namespace :admin do
get '', to: 'dashboard#index', as: '/'
resources :companies do
resources :service_requests, only: [:index, :new]
end
end
If an admin logs in and clicks on a company logo and clicks to create a new SR, the route is /admin/companies/1/service_requests/new. If a regular user logs in it is, /service_requests/new. I am just slightly confused on how to reuse the same form for both the admin and non-admin side. Because I am setting the company_id on the SR in my create resource in the ServiceRequestsController

I do something like the following to use the same form on an admin and a not admin using cancan
#models/ability.rb
class Ability
include CanCan::Ability
def initialize(user)
if user.is_admin? #replace this by something that returns true if logged-in user is admin
can :generate_this_form_fields, User
# ^ normally can -> points to a method in the controller, but you can
# create one even if you're not validating any method, just so you could use it anywhere
end
end
end
While on the view file where your form is, something like
#views/form.html.erb
<%= form_tag ... %>
<% if can? :generate_this_form_fields, User %>
This is where the fields are displayed if admin account
<% else %>
This is where the fields are displayed if not admin
<% end %>
<% end %>
Actually in the view, you could just use
<% if current_user.is_admin? %>
And not
<% if can? :generate_this_form_fields, User %>
But I've made the example in such a way that might fit to what you're trying to solve.
Hope this helps. Cheers!

Related

Rails require user's profile setup after sign_up

How can I require user to create profile after he has signed up with Devise?
I am thinking about using Wicked Wizard gem, but it is possible for user to skip all steps and just access the website.
User must setup their profile first, only then they should have access to website.
I am thinking about this implementation:
SomeController.rb
before_action :authenticate_user!
before_action :check_if_profile_created?
private
def check_if_profile_created?
current_user.profile
end
end
But this solution will do this check on all requests made by user, which I think is not good. Is there any other ways to do this? Or how can it be implemented with Wicked Wizard gem? I haven't found how to make it redirect from all pages to current step in wizard.
I have a solution that I just implemented two days ago. There is no much documentation on the Internet about this matter. There might be many ways to do this. This is just the way I did it. So, without more delay, let's get to it.
The Concept:
The whole idea, at least how I conceive it, is to "force" the recently signed up user, after he/she also confirms its email, to additionally confirm his profile. As simple as that. That's because in my case many of the profile fields are obviously not mandatory, except the first_name, last_name, email and obviously the password. The rest (job_title, company, affiliation, etc, etc, etc), are not mandatory. But the thing is that at the same time, I would like to enforce that every single User has a profile as complete as possible. This is absolutely necessary for purposes that are not necessary to mention here.
Note:
This is based in that you already have a SettingsController, that allows to any logged user to update his own profile and to update his password as well (in a separated action method).
Step # 1: Add the related boolean field
Modify your users table by adding a boolean field.
class AddProfileWasConfirmedToUsersTable < ActiveRecord::Migration[6.1]
def change
add_column :users, :profile_was_confirmed, :boolean, null: false, default: false
end
end
The boolean field profile_was_confirmed will ensure that the User's profile has been confirmed. It's false by default, right from creation, and it will be updated once and only once to true, when the user confirms his profile. The system won't enforce a 100% complete profile after that, because in order to do such thing it would have to be mandatory on the User model, and that would mess up with the initial sign up process (using the usual devise).
Step # 2: Add the necessary routes.
This one is for the view where we will include a form (inn our case reuse the settings profile form):
get :confirm_profile, to: 'settings#confirm_profile'
And this one is for the updating of the incoming fields from the previously mentioned form:
patch :update_confirmed_profile, to: 'settings#update_confirmed_profile'
So at the end you would have something like this (just an example):
get :confirm_profile, to: 'settings#confirm_profile'
resource :settings, only: [:edit, :update] do
patch :update_confirmed_profile, to: 'settings#update_confirmed_profile'
patch :change_password, to: "settings#change_password"
resource :site_terms, only: [:edit, :update]
end
Note:
Remember to place the additional get route before the resources declaration. Otherwise the framework will confuse your route with the show action method.
Step # 3: Modify your ApplicationsController
before_action :check_profile_was_confirmed
.
.
.
def check_profile_was_confirmed
return unless current_user.present?
unless current_user.profile_was_confirmed? || devise_controller?
redirect_to confirm_profile_path
end
end
Step # 3: Modify your SettingsController
skip_before_action :check_profile_was_confirmed
def confirm_profile
if current_user.profile_was_confirmed?
flash["notice"] = "Your profile has been already confirmed"
redirect_to dashboard_path
end
end
def update_confirmed_profile
if #user.update(update_params) && confirm_params_ready?
#user.update({ profile_was_confirmed: true })
flash["notice"] = "Profile confirmed"
redirect_to dashboard_path
else
flash["alert"] = "All the fields must be filled"
redirect_to confirm_profile_path
end
end
private
def confirm_params_ready?
params_base = params.require(:user).permit(:first_name, :last_name, :email, :company, :job_title)
params_base.values.all?(&:present?)
end
Step 4: Create your view with the form or reuse the same one you are using for updating the profile.
File:
app/views/settings/confirm_profile.html.erb
<% content_for(:title) { " | Profile Confirmation" } %>
<% content_for(:view_header) do %>
<%= render 'shared/view_header', view_title: 'Profile Confirmation' %>
<% end %>
<%#= render partial: 'nav' %>
<div class="w-prose mx-auto space-y-8">
<div class="card">
<div class="card-header">
<h4 class="mb-0">Fill all the fields</h4>
</div>
<div class="card-body">
<%= render partial: "settings/profile_form", locals: { form_url: update_confirmed_profile_settings_path } %>
</div>
</div>
</div>
Step 5: Update your User model's factory
Step 6: Update all your integration tests.
there is no way you can do it without a validation on every request, because you always have to be sure they fill the profile, no matter on what page you enter.
maybe you can add that validation to the application controller with a before action so you don't have to do it in every controller. and just validate if user is logged in and user.profile...
What do you mean ugly? If it's a matter of "the code will be duplicated everywhere", then you can create a controller that inherits from ActionController, and add before_action to that controller specifying that the profile needs to be filled up. Or you can create a concern which you can then plop in
class SomethingController < ApplicationController
before_action :ensure_profile_filled
def ensure_profile_filled
redirect_to 'wherever'
end
end
class ChildController < SomethingController
end
There are also route constraints but I think you want to do this on the controller layer.

Ruby No Method Error on a Model for Current User when Logged Out

I'm building a rails app that has a lot of things that change based on whether or not the user has completed certain quizzes. Everything works fine when a user is logged in, but I just tried logging out and I got a NoMethodErroron this line of code:
<% if current_user.per_quiz.nil? %>
Basically, if the user has completed per_quiz they go to one page (the edit view), if they haven't they go to a different page (the new view). The only thing that changed was the fact that I logged out, so I can only assume that is what caused the error. Do I need to add some kind of if statement to account for a state in which no user is logged in? How should I fix this error in accordance with Ruby best practices?
You just need to check to see if current_user is set before checking per_quiz. You can handle checking for the login state easily in a view by querying current_user.nil?:
<% if !current_user %>
<p> You must be logged in to do anything useful. Please go login</p>
<% elsif !current_user.per_quiz %>
<p>Cool quiz stuff goes here...</p>
<% else %>
What you probably really want is to have a logged out user go elsewhere, such as the home page or signin page. To do that, you need to do a couple of simple things to your controller. I'm going to assume that the controller is called 'QuizzesController' since you hadn't included your controller code in the question.
Here's how to do it:
class QuizzesController < ApplicationController
# Other devise authentication logic goes here...
before_action :authorize_user!
# Your actions go here...
private
def authorize_user!
if !current_user
redirect_to '/', notice: "You must be logged in to access to this page"
end
end
end
What this does is install a "before_action" handler that will check that the user is logged in before letting them do anything in this controller. If they are not logged in, you can redirect them wherever you wish.
Note that sometimes, only certain actions need this kind of treatment. In that case, you can use an :only or :except option to specify which action(s) are or are not handled. It looks like this:
before_action :authorize_user!, only: [ :new, :edit, :create ]
or like this:
before_action :authorize_user!, except: :list
This will give you greater flexibility in managing the authorization part of the equation, where devise handles the authentication part.
That's because current_user is nil when no user logged in. And if you will cal some method on nil it will throw NoMethodError.
I assume that have code you provided in the view. So, you can check if user is logged in.
<% if user_signed_in? %>
# do smth with user quizzes
<% else %>
# do something else
<% end %>
But the best way is to use before filter in the controller and not allow to anonymous to see that page.
Here is the example application with Rails and devise. https://github.com/RailsApps/rails-devise
Hope that helps.
I'm not sure if this is the "correct" Ruby way to do this, but the way I eventually found was to change it to an if/elsif/else statement:
<% if current_user.nil? %>
...
<% elsif current_user.bal_quiz.nil? %>
...
<% else %>
...
<% end %>
Probably should have figured this out before I posted the question, but I was (and still am) interested to see if there's a better or "more Ruby" way of doing this.

How to allow Admin user to access customer user objects rails

I have a report object that belongs_to a customer(user_type). I have it setup so that the customer is able to grant or remove permission from an admin to see the customers report. I need to know what steps or direction do I need to take to get the reports viewable to the admin user. The admin user should only have the ability to see reports (show) and see an index of reports that they have access to.
I am thinking maybe I should create a new controller but I am not sure if that is correct or how to do another controller tied to an object that already has a controller.
I highly suggest looking into the CanCanCan gem (the continuation of the CanCan gem which is no longer supported). In this way your code would look something like this in the end (you didn't post any code, so I have to make assumptions about your variables and such):
<% if current_user.admin? %>
<% if can? :view_report, #customer %>
<!-- Render the report to this admin -->
<!-- ............................. -->
<% end %>
<% end %>
And then you would have something like this in your controller to give an admin permission to view the user's report:
def handle_report
can [:show], Report, :user_id => user.id
if current_user.admin?
can :manage, :current_report
end
end
Again, apologies for guessing the variables.

Rails authority apparent inconsistency

I am using the gems authority and rolify to manage user permissions on a set of subjects.
Each subject can be seen by a user only if the user has the :admin role for that subject.
Code in the view:
<% if (current_user.has_role? :admin, #subject) %>
ADMIN
<% end %>
<% if #subject.readable_by?(current_user)%>
#some other code
<% end %>
Code in the authorizer:
class SubjectAuthorizer < ApplicationAuthorizer
# can the user view the subject?
def self.readable_by?(user)
user.has_role? :admin, #subject
end
end
My problem is that the ADMIN part is displayed, but not the rest of the page. However, the two if conditions should have the same truth value. Can anyone spot a mistake?
#subject is not available in the Authorizer. You need to use resource instead.

Double controllers when using namespace in rails?

I'm working on an application with a namespacing (admin section). Is there a DRY solution for not creating 2 controllers? Because I will need to create an public user controller and a admin user controller to manage the users.
How about inheriting the user controller? I use it myself (for images) and it suits me nicely:
# file: apps/controllers/images_controller.rb
class ImagesController < ApplicationController
# image code (to show the image for example)
end
# file: apps/controllers/admin/images_controller.rb
class Admin::ImagesCOntroller < ImagesController
# additional admin code (to delete the image for example)
end
You might consider rendering the page with optional "edit" buttons. For example, something like this:
Name: <%= #user.name %>
<% if #user.admin? %>
<% form_for #user do |f| %>
Editing stuff
<% end %>
<% end %>
That way, a user only sees it as a page, but an admin sees additional controls that allows them to edit the field. P.S. Make sure in your controller that you are checking to make sure it's an admin that is calling the update call.

Resources