ruby-on-railsdevisenested-forms

How to update the nested form attributes of only the current_user in rails 6?


I have a 'User', 'Task' and 'Comment' model in my rails 6 app. The User model is defined using devise for authentication. User has_many comments and has_many tasks and Task has_many comments. I have a nested form for 'Task' which accepts_nested_attributes_for 'Comment'. Something like this:

<%= form_with model: @task do |f| %>
  <%= f.text_field(:name) %>
  <br><br>
  
  <%= form.fields_for :comments do |c| %>
    <%= render('comment_fields', f: c) %>
  <% end %>
  <%= link_to_add_association('Add comment', f, :comments) %>
  <%= f.submit %>
<% end %>

The '_comment_fields.html.erb' file looks like this:

<div class="nested-fields">
  <%= f.text_area(:body) %>
  <%= link_to_remove_association('Remove comment', f) %>
</div>

The above two forms are just a minimum version of the original code as I am using them only for reference here. Now, suppose a user named 'user1' added a task named 'task1' using the task form in the database and also added some comments using the nested form for comments. What I want is if some other user named 'user2' tries to edit the task 'task1' then he/she should be able to add and edit only his comments with this form and can only edit the task name and 'user2' should not be able to edit or remove someone else's comments but only his. How would we go about doing this for a nested form.

We can have this functionality in some normal form like:

<% if @task.user.id == current_user.id %>
  <%= f.text_field(:name) %>
<% end %>

The above code will only show the text_field when the current model's user_id matches the current signed in user's id but I don't know how to do this in f.fields_for because we don't have a variable like @task in a nested form.


Solution

  • You can get the object wrapped by a form builder instance through the object attribute:

    <div class="nested-fields">
      <%= f.text_area(:body) %>
      <% if f.object.user == current_user %>
        <%= link_to_remove_association('Remove comment', f) %>
      <% end %>
    </div>
    

    However I wouldn't really encourage you to reinvent the authorization wheel. The Pundit or CanCanCan gems are the most popular choices to avoid spreading and duplicating the authentication logic all over your views and controllers.

    With Pundit you would do:

    class CommentPolicy < ApplicationPolicy
      def destroy?
        record.user == user
      end
    end
    
    <div class="nested-fields">
      <%= f.text_area(:body) %>
      <% if policy(f.object).destroy? %>
        <%= link_to_remove_association('Remove comment', f) %>
      <% end %>
    </div>
    

    The equivilent with CanCanCan is:

    class Ability
      include CanCan::Ability
    
      def initialize(user)
        can :destroy, Comment, user: user
      end
    end
    
    <div class="nested-fields">
      <%= f.text_area(:body) %>
      <% if can?(:destroy, f.object) %>
        <%= link_to_remove_association('Remove comment', f) %>
      <% end %>
    </div>