如何构造具有动态作用域的has_many关联?

时间:2019-05-15 10:56:13

标签: ruby-on-rails arrays ruby associations

我的数据库中有一个用户表。用户可以是'admin'或'manager'类型的用户。

鉴于下面的模型和架​​构,我希望对于“经理”用户的每个实例,“管理员”用户可以选择经理所属的租户的一个,部分或全部位置,以便选择经理可以控制的位置。

我的模特

class User < ActiveRecord::Base
  belongs_to :tenant
class Tenant < ActiveRecord::Base
  has_many :users, dependent: :destroy
  has_many :locations, dependent: :destroy
class Location < ActiveRecord::Base
  belongs_to :tenant, inverse_of: :locations

我尝试了两条路径

首先,尝试在用户模型和位置模型之间建立范围限定的has_many关联。但是,我不能全力以赴地设计这种范围,以使“管理员”用户可以选择“经理”用户可以控制的位置。

第二,在users表中设置了受控位置属性。然后,我设置了一些代码,以便“管理员”用户可以选择“经理”可以控制的位置,并填充其“ managed_locations”属性。但是,保存在数据库中(受控位置数组内部)的是字符串而不是位置实例。

这是我为第二条路径尝试的代码:

迁移

def change
  add_column :users, :controlled_locations, :string, array: true, default: []
end

在视图中

= f.input :controlled_locations, label: 'Select', collection: @tenant_locations, include_blank: "Anything", wrapper_html: { class: 'form-group' }, as: :check_boxes, include_hidden: false, input_html: {multiple: true}

在用户控制器中(在更新方法内部)

if params["user"]["controlled_locations"]
  params["user"]["controlled_locations"].each do |l|
    resource.controlled_locations << Location.find(l.to_i)
  end
  resource.save!
end

我所期望的

首先,我不确定我尝试的第二条路径是否是一种好方法(在db中存储数组)。因此,我最好的选择是建立一个可能的范围关联。

在第二条路径可行的情况下,我想得到的是这样的东西。假设登录管理员,我选择了ID为1的用户(管理员)可以控制一个位置(波士顿体育场):

user = User.find(1)
user.controlled_locations = [#<Location id: 55, name: "Boston Stadium", created_at: "2018-10-03 12:45:58", updated_at: "2018-10-03 12:45:58", tenant_id: 5>]

相反,尝试后得到的是:

user = User.find(1)
user.controlled_locations = ["#<Location:0x007fd2be0717a8>"]

保存在数组中的不是简单的位置实例,而是纯字符串。

2 个答案:

答案 0 :(得分:0)

首先,您的代码缺少locations类中的Tenant关联。

class Tenant < ActiveRecord::Base
  has_many :users, dependent: :destroy
  has_many :locations

让我们说变量manager有一条User记录。那么它可以控制的位置是:

manager.tenant.locations

如果需要,可以使用委托语句来缩短它。

class User < ActiveRecord::Base
  belongs_to :tenant
  delegate :locations, to: :tenant

然后您可以使用

manager.locations

答案 1 :(得分:0)

用于授权的常见模式是角色:

class User < ApplicationRecord
  has_many :user_roles
  has_many :roles, through: :user_roles

  def add_role(name, location)
    self.roles << Role.find_or_create_by(name: name, location: location)
  end

  def has_role?(name, location)
    self.roles.exists?(name: name, location: location)
  end
end

# rails g model role name:string
# make sure you add a unique index on name and location
class Role < ApplicationRecord
  belongs_to :location
  has_many :user_roles
  has_many :users, through: :user_roles
  validates_uniqueness_of :name, scope: :location_id
end

# rails g model user_role user:references role:references
# make sure you add a unique compound index on role_id and user_id
class UserRole < ApplicationRecord
  belongs_to :role
  belongs_to :user
  validates_uniqueness_of :user_id, scope: :role_id
end

class Location < ApplicationRecord
  has_many :roles
  has_many :users, through: :roles
end

通过使系统比“受控位置”关联更通用,可以在不同情况下重新使用它。

  

比方说,登录管理员后,我选择了ID为1的用户   (经理)可以控制一个地点(波士顿体育场)

User.find(1)
    .add_role(:manager, Location.find_by(name: "Boston Stadium"))

在实际的MVC术语中,您可以通过将角色设置为可像其他任何资源一样进行CRUD的嵌套资源来实现。可以使用accepts_nested_attributes或AJAX完成单一表单中的多个角色的编辑。

如果要通过角色的存在来限定查询的范围,则可以将角色和用户角色表结合起来:

Location.joins(roles: :user_roles)
        .where(roles: { name: :manager })
        .where(user_roles: { user_id: 1 })

要对单个资源进行身份验证,请执行以下操作:

class ApplicationController < ActionController::Base
  protected 
  def deny_access
    redirect_to "your/sign_in/path", error: 'You are not authorized.'
  end
end

class LocationsController < ApplicationController
  # ...
  def update
    @location = Location.find(params[:location_id])
    deny_access and return unless current_user.has_role?(:manger, @location)
    # ...
  end
end

尽管我会考虑使用rolifypundit,但不要使用自己的授权系统。