在使用ActiveAdmin和Friendly_id时出现了ActiveRecord::ReadOnlyRecord错误

16

我最近在项目中开始使用ActiveAdmin,几乎所有功能都很棒,但是当与friendly_id gem一起使用时出现了问题。当在表单中使用它时,我会遇到ActiveRecord::ReadOnlyRecord异常(我认为)是因为friendly_id属性的ID是只读的:

{"utf8"=>"✓",
"_method"=>"put",
"authenticity_token"=>"Rc5PmUYZt3BiLvfPQr8iCPPXlbfgjoe/n+NhCwXazNs=",
"space"=>{"name"=>"The Kosmonaut",
"address"=>"8 Sichovykh Striltsiv 24",
"email"=>"info@somedomain.com"},
"commit"=>"Update Space",
"id"=>"the-kosmonaut"}  <--- culprit

我猜测最后一行是罪魁祸首,因为它是只读属性,它不在我的表单中,而是在路径中

http://localhost:5000/manage/spaces/the-kosmonaut/edit

我该如何修复它不去尝试更新ID?

ActiveAdmin中的表单如下:

  form do |f|
    f.inputs "Details" do
      f.input :name
      f.input :address
      f.input :email
      f.input :phone
      f.input :website
    end
    f.inputs "Content" do
      f.input :description
      f.input :blurb
    end
    f.buttons
  end

更新:这也不起作用,所以不是 friendly_id 的问题?

我尝试使用 @watson 的建议,本应该可以起作用,但仍然出现了相同的错误;-(

{"utf8"=>"✓",
 "_method"=>"put",
 "authenticity_token"=>"Rc5PmUYZt3BiLvfPQr8iCPPXlbfgjoe/n+NhCwXazNs=",
 "space"=>{"name"=>"The Kosmonaut 23"},
 "commit"=>"Update Space",
 "id"=>"6933"}

http://localhost:5000/manage/spaces/6933/edit

当我使用record.readonly?在控制台中检查记录时,它返回false。

更新:删除scope_to可以解决问题。

scope_to :current_user, :unless => proc{ current_user.admin? }

唯一的问题是我需要使用scope_to,以防止用户查看他们不拥有的记录。 我猜测(因为我假设scope_to通常与has_many一起使用)我的HABTM关联会导致一些奇怪的情况?即 用户 <-- HABTM --> 空间?

5个回答

29

如果您只希望在前端显示友好的ID,并且不在Active Admin中使用它们,那么您可以撤销friendly_id宝石对Active Admin控制器的影响。

我不确定friendly_id如何覆盖to_param方法,但是如果它是正常的方法,请重新在所有Active Admin控制器中覆盖它,例如:

ActiveAdmin.register Foobar do
  before_filter do
    Foobar.class_eval do
      def to_param
        id.to_s
      end
    end
  end
end

更好的做法是在基础的Active Admin控制器ActiveAdmin::ResourceController中创建一个前置过滤器,以便它自动继承到所有的Active Admin控制器。

首先将该过滤器添加到config/initializers/active_admin.rb设置中:

ActiveAdmin.setup do |config|
  # ...
  config.before_filter :revert_friendly_id
end

打开 ActiveAdmin::ResourceController 并添加一个 revert_friendly_id 方法,例如在 config/initializers/active_admin.rb 的末尾添加以下内容:

ActiveAdmin::ResourceController.class_eval do
  protected

  def revert_friendly_id
    model_name = self.class.name.match(/::(.*)Controller$/)[1].singularize

    # Will throw a NameError if the class does not exist
    Module.const_get model_name

    eval(model_name).class_eval do
      def to_param
        id.to_s
      end
    end
  rescue NameError
  end
end

更新:我刚刚更新了最后一个代码示例,以处理没有相关模型的控制器(例如Active Admin Dashboard控制器)。

更新2:我刚刚尝试使用上述hack与friendly_id gem一起使用,看起来完全正常 :)

更新3:清理了代码,使用了向基本控制器添加Active Admin before filters的标准方式。


1
我可能在我的假设上错了,你的解决方案应该是可行的,但我仍然无法修改记录。我已经更改了问题以反映这一点。 - holden
1
这在开发模式下很好用,因为Rails每个请求都会重新加载自己。但是在生产环境中,只有在我首次使用active admin之前它才有效。通过class_eval重新打开类似乎是问题所在。Denny的解决方案对我很有效。 - sunsations
也许我错过了什么,但我没有看到这如何修复仪表板控制器。我在 app/admin/dashboard.rbcontroller 块中添加了一个空的 revert_friendly_id 来修复错误。 - pix0r
它运行良好,但在索引页面上出现了问题:“Admin::DashboardController”没有定义方法“revert_friendly_id”。 - Miguel Peniche
我建议你避免使用这种技术,在多线程环境下会导致Foobar类的修改结果不一致。 - Eliot Sykes
显示剩余2条评论

16

您可以根据http://activeadmin.info/docs/2-resource-customization.html#customizing_resource_retrieval自定义资源检索。请注意,您需要使用find_resource方法而不是resource,否则将无法创建新记录。

(更多详细信息请查看https://github.com/gregbell/active_admin/blob/master/lib/active_admin/resource_controller/data_access.rb)

在您的ActiveAdmin资源类中编写:

controller do
  def find_resource
    scoped_collection.where(slug: params[:id]).first!
  end
end

您也可以通过修改active_admin.rb初始化器中的ResourceController来覆盖所有资源的行为。

ActiveAdmin::ResourceController.class_eval do
  def find_resource
    if scoped_collection.is_a? FriendlyId
      scoped_collection.where(slug: params[:id]).first! 
    else
      scoped_collection.where(id: params[:id]).first!
    end
  end
end
希望这有所帮助!注意:在通过管理界面创建新记录时,请确保不要在表单中包含slug字段,否则FriendlyId将无法生成slug。(我认为这仅适用于版本5+)

如果在我的模型中使用“scoped” slug,我认为这不会起作用。也就是说,只有在与某些其他属性/关联一起使用时,slug 才能保证唯一。在这种情况下有什么建议吗? - elsurudo

4
这个方法适用于我。在app/admin/model_name.rb中添加这段代码即可。
ActiveAdmin.register model_name do
  controller do
    defaults finder: :find_by_slug
  end
end

2

除了Denny的解决方案,一个更“友好”的解决方案是使用friendly_id的查找器。

controller do
  def find_resource
    scoped_collection.friendly.find_by_friendly_id(params[:id])
  end
end

Source


1
这是基于@Thomas解决方案的我的解决方案。
ActiveAdmin.setup do |config|
  # ...
  config.before_filter :revert_friendly_id, :if => -> { !devise_controller? && resource_controller? }
end

# override #to_param method defined in model in order to make AA generate
# routes like /admin/page/:id/edit
ActiveAdmin::BaseController.class_eval do

  protected
  def resource_controller?
    self.class.superclass.name == "ActiveAdmin::ResourceController"
  end

  def revert_friendly_id
    model_name = self.class.name.match(/::(.*)Controller$/)[1].singularize
    # Will throw a NameError if the class does not exist
    Module.const_get model_name

    eval(model_name).class_eval do
      def to_param
        id.to_s
      end
    end
  rescue NameError
  end
end

当我有一个已注册的页面时,这对我很有效。例如:ActiveAdmin.register_page“Dashboard”。感谢@Timur,你救了我的一天。 - RoundOutTooSoon
我建议你避免使用这种技术,在多线程环境下会导致模型类的修改结果不一致。 - Eliot Sykes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接