Brian Graham
Brian Graham

Reputation: 152

When updating a nested form, how do I do something with new records?

class Photo < ActiveRecord::Base 
  has_many :item_photos
  has_many :items, through: :item_photos
end 

class Item < ActiveRecord::Base 
  has_many :item_photos
  has_many :photos, through: :item_photos
  accepts_nested_attributes_for :photos
end 

class ItemPhotos < ActiveRecord::Base
  belongs_to :photo
  belongs_to :item
end 

When I edit or create an Item, I also upload or remove Photos. However, more than one user can view an Item. These users should only be able to view their own Photos.

Item #1 has three Photos. Amy has access to one. Barry has access to two. Barry loads up /items/1 and edits it. He deletes one Photo, ignores the other, and adds two new Photos.

class ItemsController < ApplicationController
  def update
    if @item.update(item_params)
      # Give Barry access to the Photo he just made. 
      @item.only_the_photos_barry_just_made.each do |p|
        current_user.add_role :viewer, p
      end 
    end 
  end 
end 

I don't want to pollute models/photo.rb with methods to access session information (like current_user). Is there an idiomatic way to get these records? If not, is there a clean way to get these records?

Thanks for any help.

Upvotes: 0

Views: 58

Answers (1)

max
max

Reputation: 102423

A simple solution would be to add a :creator relation to photo.

rails g migration AddCreatorToPhotos creator:references

class Photo < ActiveRecord::Base
  belongs_to :creator, class: User
  # ...
end

And then you can simply add a rule in your Abilty class:

class Ability
  include CanCan::Ability
  # Define abilities for the passed in user here.
  # @see https://github.com/bryanrite/cancancan/wiki/Defining-Abilities
  def initialize(user = nil)
    user ||= User.new # guest user (not logged in)
    # ...
    can :read, Photo do |photo|
      photo.creator.id == user.id
    end
  end
end

You can then get the photos that can by read by the current user with:

@photos = @item.photos.accessible_by(current_ability)

Edit:

If you want to authorize though roles instead you just need to alter the conditions in the authorization rule:

class Ability
  include CanCan::Ability
  # Define abilities for the passed in user here.
  # @see https://github.com/bryanrite/cancancan/wiki/Defining-Abilities
  def initialize(user = nil)
    user ||= User.new # guest user (not logged in)
    # ...
    can :read, Photo do |photo|
      user.has_role? :viewer, photo
    end
  end
end

Edit 2:

An approach to creating the role could be to add a callback to Photo. But as you already have surmised, accessing the user via the session from a model is not a good approach.

Instead you can pass the to the user to Photo when it is instantiated. You can either setup the belongs_to :creator, class: User relationship or create a virtual attribute:

class Photo < ActiveRecord::Base
  attr_accessor: :creator_id
end

You can then pass the user by a hidden field (remember to whitelist it!):

# GET /items/new
def new 
  @item = Item.new
  @item.photos.build(creator: current_user) # or creator_id: current_user.id
end

<%= fields_for(:photos) do %>
  <%= f.hidden_field :creator_id %>
<% end %>

So, how do we create our callback?

class Photo < ActiveRecord::Base
  attr_accessor: :creator_id

  after_commit :add_viewer_role_to_creator!, on: :create

  def add_viewer_role_to_creator!
    creator.add_role(:viewer, self)
    true # We must return true or we break the callback chain
  end
end

There is one issue though:

We don't want to allow malicious users to be assign their ID to existing photos on update.

We can do this by setting up some custom params whitelisting:

def item_params
  whitelist = params.require(:item).permit(:foo, photos_attributes: [:a,:b, :creator_id]
  current_user_photos = whitelist[:photos_attributes].select do |attrs|
   attrs[:id].nil? && attrs[:creator_id] == current_user.id
  end
  whitelist.merge(photos_attributes: current_user_photos)
end

Upvotes: 2

Related Questions