Search code examples
ruby-on-railsformsvalidationdeviserolify

Rolify: Validate roles with complex functionality inside the User model


I've spent four days trying to achieve this role validation with no success, and I don't know if there is currently any way to do this behaviour with rolify. Here's a simple example of the situation I am having:

Suppose I have 3 types of roles:

  • admin
  • developer
  • guest

A user can be both an admin and a developer, but cannot be also a guest.

A guest cannot be either an admin nor a developer.

I want the user to be able to set their roles on user registration, so I need to validate this behaviour before the user is created, and if it is not valid, the registration should fail and no user should be created.

I've read a lot of guides trying to achieve similar results, at least for the form submission, but there is no explanation about this type of validation.

Thanks in advance!

UPDATE: I've been searching for some ways to implement this, and this is the line of thought I've had during this time of struggles.

First: receive the roles selected as input and create a virtual attribute at the user model corresponding to this roles so that the params would look like this (simplified)

:params => { :user => {:email, :password, :roles => ["admin", "developer", "guest"]}}

if all roles are selected, and

:params => { :user => {:email, :password}}

if no role is selected.

Second: Do any type of validations with this virtual attribute(simplified)

Class User < ApplicationRecord
  attr_accessor :roles
  validate :roles_valid

  def roles_valid
    # Define custom validation
  end
end

Third: if the record passes all the validations, create user and then assign the roles inside the registrations#new action

I think this method would work, but I am now having some problems with the virtual attribute, and it is that it doesn't seem to be connected to the roles params being received from the form. I guess I still don't understand how virtual attributes work, so I wish someone would clear me out on this one.


Solution

  • I got it now! The process I added in the update is correct, so I will make a brief guide on how to achieve this correctly.

    Objective: validate complex functionality for rolify inside the User model. This must be done inside the User model because one must check all the roles being added, and not each individually

    Step One: If the roles your app is going to use are static, then you must first populate your roles table with the specific roles. For this, I will leave a post from another question here in Stack Overflow that explains this process in depth:

    Defining Roles with Rolify

    Step two: modify your registration form. In order to get the roles the user has chosen, we must first add some checkboxes corresponding to the roles defined on step one. Here is a simple code for this.

    <%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %>
      <!-- ADD HERE ALL YOUR REGISTRATION INPUTS -->
      <% Role.all.each do |role| %>
        <%= f.checkbox :input_roles, {multiple: true}, role.name, nil %>
        <%= role.name %>
      <% end %>
    <% end %>
    

    Doing this will generate all the checkboxes necessary, and store the result inside the variable

    params => {:user => { :input_roles => [] }}
    

    Important notes! if the user does not select any role, then the varialbe input_roles won't be defined! you must check for this in your validations. ALSO, DO NOT CHANGE THE NAME OF THE VARIABLE TO roles, I made this mistake and went through hell trying to fix all the errors I was getting. If interested on seeing everything I went through, here is a link to the issue I created:

    https://github.com/RolifyCommunity/rolify/issues/446

    Step three: Since you are adding a new parameter to the form, you must also include it inside your strong parameters. Since this variable is an array, you must explicitly define this inside your strong parameters:

    In case you are using Devise:

    devise_parameter_sanitizer.permit(:sign_up, keys: [:email, :password, :password_confirmation, input_roles: [])
    

    In case you aren't using devise:

    params.require(:user).permit( :email, :password, :password_confirmation, input_roles: [])
    

    Step four: Ok, so now the input_roles param is being passed into our User model. But, now we need to find a way to validate the values inside. Since this parameter is not part of our User model, we must create a virtual attribute to be able to work with this parameters. So we just name it the same as the param being passed to the model, like this:

    Class User < ApplicationRecord
    rolify
    attr_accessor :input_roles
    

    Now when you want to access the values the user has selected as roles, you just have to do this in you User model:

    self.input_roles.to_a # => ["admin", "developer", "guest"] If the user selected all roles
    

    Now you can do all sorts of validation with this information! Hope this helps! If anyone know of a cleaner way to do this, please let me know. I looked through everything I could stumble upon and no answer was given. At leaast with this the validations are done inside the model, instead of the controller.