activeadmin 和动态存储访问器在新资源上失败

activeadmin and dynamic store accessors fails on new resource

我想为具有 postgres jsonb 列 :data 的资源生成表单,并且我希望将这些表单的架构存储在数据库的 table 中。经过大量研究后,我已经完成了 90%,但我的方法在创建(而不是更新)时在 ActiveAdmin 表单中失败。谁能解释一下?

对于长代码片段,我们深表歉意。这是一个相当复杂的设置,但我认为它会引起一些兴趣,因为如果它可行,就可以动态构建任意新模式而无需硬编码。

我正在关注 this previous discussion Rails 6 和 ActiveAdmin 2.6.1 以及 ruby 2.6.5。

我想将 Json 模式存储在 table SampleActionSchema 中 belong_to SampleAction(使用 json-schema gem 进行验证)

class SampleActionSchema < ApplicationRecord
  validates :category, uniqueness: { case_sensitive: false }, allow_nil: false, allow_blank: true
  validate :schema_is_json_schema

  private

  def schema_is_json_schema
    metaschema = JSON::Validator.validator_for_name("draft4").metaschema
    unless JSON::Validator.validate(metaschema, schema)
       errors.add :schema, 'not a compliant json schema'
    end
  end
end
class SampleAction < ActiveRecord::Base
  belongs_to :sample

  validate :is_sample_action
  validates :name, uniqueness: { case_sensitive: false }

  after_initialize :add_field_accessors
  before_create :add_field_accessors
  before_update :add_field_accessors

  def add_store_accessor field_name
    singleton_class.class_eval {store_accessor :data, field_name.to_sym}
  end

  def add_field_accessors
    num_fields = schema_properties.try(:keys).try(:count) || 0
    schema_properties.keys.each {|field_name| add_store_accessor field_name} if num_fields > 0
  end


  def schema_properties

    schema_arr=SampleActionSchema.where(category: category)
    if schema_arr.size>0
      sc=schema_arr[0]
      if !sc.schema.empty?
        props=sc.schema["properties"]
      else
        props=[]
      end
    else
      []
    end
  end
  private

  def is_sample_action
    sa=SampleActionSchema.where(category: category)
    errors.add :category, 'not a known sample action' unless (sa.size>0)
    errors.add :base, 'incorrect json format' unless (sa.size>0) && JSON::Validator.validate(sa[0].schema, data)
  end

end

一切正常;例如,对于一个名为 category: "cleave" 的简单模式,其中 :data 看起来像 data: {quality: "good"},我可以在 rails 控制台中创建如下资源:

sa=SampleAction.new(sample_id: 6, name: "test0", data: {}, category: "cleave" )
=> #<SampleAction id: nil, name: "test0", category: "cleave", data: {}, created_at: nil, updated_at: nil, sample_id: 6> 

sa.quality = "good"   => true
sa.save => true

为了使这个系统以 AA 形式工作,我调用了带有参数的正常路径(新建或编辑)_admix_sample_action_form:{category: "cleave"} 然后我生成 permit_params动态:

ActiveAdmin.register SampleAction, namespace: :admix do

  permit_params do
    prms=[:name, :category, :data, :sample_id, :created_at, :updated_at]
    #the first case is creating a new record (gets parameter from admix/sample_actions/new?category="xxx"
    #the second case is updating an existing record
    #falls back to blank (no extra parameters)
    categ = @_params[:category] || (@_params[:sample_action][:category] if @_params[:sample_action]) || nil
    cat=SampleActionSchema.where(category: categ)
    if cat.size>0 && !cat[0].schema.empty?
      cat[0].schema["properties"].each do |key, value|
        prms+=[key.to_sym]
      end
    end
    prms
  end

form do |f|
    f.semantic_errors
    new=f.object.new_record?
    cat=params[:category] || f.object.category
    f.object.category=cat if cat && new
    f.object.add_field_accessors if new
    sas=SampleActionSchema.where(category: cat)
    is_schema=(sas.size>0) && !sas[0].schema.empty?
    if session[:active_sample]
      f.object.sample_id=session[:active_sample]
    end

    f.inputs "Sample Action" do
      f.input :sample_id
      f.input :name
      f.input :category
      if !is_schema
        f.input :data, as: :jsonb
      else
        f.object.schema_properties.each do |key, value|
        f.input key.to_sym, as: :string
        end
      end
    end
    f.actions
  end

如果我正在编辑现有资源(在上面的控制台中创建),一切正常。显示表单并在提交时更新所有动态字段。但是在创建新资源时,例如:data 的格式为 data: {quality: "good"} 我得到

ActiveModel::UnknownAttributeError in Admix::SampleActionsController#create
unknown attribute 'quality' for SampleAction.

我已经尝试在表单中 add_accessors 并覆盖新命令以在初始化后添加访问器(这些应该不需要因为 ActiveRecord 回调似乎在正确的时间完成工作) .

def new
  build_resource
  resource.add_field_accessors
  new!
end

不知何故,当在 AA 控制器中创建资源时,似乎无法存储访问器,即使它在控制台中工作正常。有没有人有正确初始化资源的策略?

解决方案:

我追踪了 AA 正在做什么,以确定所需的最少命令数。有必要向 build_new_resource 添加代码以确保构建的任何新资源 AA 具有正确的 :category 字段,并且一旦这样做,就可以调用动态添加 store_accessor 键到新建实例.

现在用户可以创建自己的原始模式和使用它们的记录,无需任何进一步的编程!我希望其他人觉得这有用,我当然会。

这里有几个丑陋的解决方案,一个是将参数添加到 active admin new route call 不是 AA 期望的,但它仍然有效。我想这个参数可以通过其他方式传递,但这项工作又快又脏。另一个是我必须让表单生成一个会话变量来存储使用了哪种模式,以便 post-form-submission 构建知道,因为按下 "Create Move" 按钮会清除来自 url.

的参数

操作如下:对于一个名为 Move with field :data 的模型,应该根据 json 模式 tables 动态序列化到字段中,两者 admin/moves/new?category="cleave"admin/moves/#/edit 从架构 table 中找到 "cleave" 架构,并使用序列化参数正确创建和填充表单。并且,直接写入 db

m=Move.new(category: "cleave")          ==> true
m.update(name: "t2", quality: "fine")   ==> true

按预期工作。架构 table 定义为:

require "json-schema"
class SampleActionSchema < ApplicationRecord
  validates :category, uniqueness: { case_sensitive: false }, allow_nil: false, allow_blank: true
  validate :schema_is_json_schema

  def self.schema_keys(categ)
    sas=SampleActionSchema.find_by(category: categ)
    schema_keys= sas.nil? ? [] : sas[:schema]["properties"].keys.map{|k| k.to_sym}
  end

  private

  def schema_is_json_schema
    metaschema = JSON::Validator.validator_for_name("draft4").metaschema
    unless JSON::Validator.validate(metaschema, schema)
       errors.add :schema, 'not a compliant json schema'
    end
  end
end

使用此架构的 Move table 是:

class Move < ApplicationRecord
  after_initialize :add_field_accessors

  def add_field_accessors
    if category!=""
      keys=SampleActionSchema.schema_keys(category)
      keys.each {|k| singleton_class.class_eval{store_accessor :data, k}}
    end
  end
end

最后,工作控制器:

ActiveAdmin.register Move do
  permit_params do
    #choice 1 is for new records, choice 2 is for editing existing
    categ = @_params[:category] || (@_params[:move][:category] if @_params[:move]) || ""
    keys=SampleActionSchema.schema_keys(categ)
    prms = [:name, :data] + keys
  end

  form do |f|
    new=f.object.new_record?
    f.object.category=params[:category] if new
    if new
      session[:current_category]=params[:category]
      f.object.add_field_accessors
    else
      session[:current_category] = ""
    end
    keys=SampleActionSchema.schema_keys(f.object.category)
    f.inputs do
      f.input :name
      f.input :category
      keys.each {|k| f.input k}
    end
    f.actions
  end

  controller do
   def build_new_resource
    r=super
    r.assign_attributes(category: session[:current_category])
    r.add_field_accessors
    r
   end
  end
end