Skip to content

Commit

Permalink
add contexts to the flexible metadata model. (#6915)
Browse files Browse the repository at this point in the history
* add contexts to the flexible metadata model. they can be assigned on admin sets and then apply the contexts to the works made with that admin set selected

* Update app/models/hyrax/flexible_schema.rb

Co-authored-by: LaRita Robinson <[email protected]>

* Update app/services/hyrax/m3_schema_loader.rb

Co-authored-by: LaRita Robinson <[email protected]>

* remove no longer needed conditionals

* migration fixes

---------

Co-authored-by: LaRita Robinson <[email protected]>
  • Loading branch information
orangewolf and laritakr authored Dec 12, 2024
1 parent a94f39e commit 98a7d59
Show file tree
Hide file tree
Showing 22 changed files with 148 additions and 63 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class AddContextsToHyraxFlexibleSchemas < ActiveRecord::Migration[6.1]
def change
add_column :hyrax_flexible_schemas, :contexts, :text unless column_exists?(:hyrax_flexible_schemas, :contexts)
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class AddContextsToHyraxFlexibleSchemas < ActiveRecord::Migration[6.1]
def change
add_column :hyrax_flexible_schemas, :contexts, :text unless column_exists?(:hyrax_flexible_schemas, :contexts)
end
end
6 changes: 4 additions & 2 deletions app/controllers/concerns/hyrax/works_controller_behavior.rb
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ def new
# TODO: move these lines to the work form builder in Hyrax
curation_concern.depositor = current_user.user_key
curation_concern.admin_set_id = params[:admin_set_id] || admin_set_id_for_new
curation_concern.contexts = Hyrax.query_service.find_by(id: curation_concern.admin_set_id)&.contexts
build_form
end

Expand Down Expand Up @@ -178,7 +179,8 @@ def admin_set_id_for_new
Hyrax::AdminSetCreateService.find_or_create_default_admin_set.id.to_s
end

def build_form
def build_form(contexts: [])
curation_concern.contexts = contexts unless contexts.blank?
@form = work_form_service.build(curation_concern, current_ability, self)
end

Expand All @@ -190,7 +192,7 @@ def actor
# @return [#errors]
# rubocop:disable Metrics/MethodLength
def create_valkyrie_work
form = build_form
form = build_form(contexts: params[hash_key_for_curation_concern]['contexts'])
action = create_valkyrie_work_action.new(form: form,
transactions: transactions,
user: current_user,
Expand Down
27 changes: 27 additions & 0 deletions app/forms/concerns/hyrax/flexible_form_behavior.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# frozen_string_literal: true
module Hyrax
module FlexibleFormBehavior
extend ActiveSupport::Concern

included do
include Hyrax::BasedNearFieldBehavior
property :contexts
end

# OVERRIDE disposable 0.6.3 to make schema dynamic
def schema
Hyrax::Forms::ResourceForm::Definition::Each.new(singleton_class.schema_definitions)
end

private

# OVERRIDE valkyrie 3.0.1 to make schema dynamic
def field(field_name)
singleton_class.schema_definitions.fetch(field_name.to_s)
end

def _form_field_definitions
singleton_class.schema_definitions
end
end
end
2 changes: 1 addition & 1 deletion app/forms/hyrax/forms/administrative_set_form.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class AdministrativeSetForm < Hyrax::Forms::ResourceForm

property :title, required: true, primary: true
property :description, primary: true

property :contexts, primary: true if Hyrax.config.flexible?
property :creator

validates :title, presence: true
Expand Down
35 changes: 3 additions & 32 deletions app/forms/hyrax/forms/resource_form.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ class ResourceForm < Hyrax::ChangeSet # rubocop:disable Metrics/ClassLength
# These do not get auto loaded when using a flexible schema and should instead
# be added to the individual Form classes for a work type or smart enough
# to be selective as to when they trigger
include BasedNearFieldBehavior if Hyrax.config.flexible?
include FlexibleFormBehavior if Hyrax.config.flexible?

##
# @api private
Expand Down Expand Up @@ -56,8 +56,7 @@ def initialize(deprecated_resource = nil, resource: nil) # rubocop:disable Metri
if Hyrax.config.flexible?
singleton_class.schema_definitions = self.class.definitions
r = resource || deprecated_resource

Hyrax::Schema.default_schema_loader.form_definitions_for(schema: r.class.to_s, version: Hyrax::FlexibleSchema.current_schema_id).map do |field_name, options|
Hyrax::Schema.default_schema_loader.form_definitions_for(schema: r.class.to_s, version: Hyrax::FlexibleSchema.current_schema_id, contexts: r.contexts).map do |field_name, options|
singleton_class.property field_name.to_sym, options.merge(display: options.fetch(:display, true), default: [])
singleton_class.validates field_name.to_sym, presence: true if options.fetch(:required, false)
end
Expand Down Expand Up @@ -169,7 +168,7 @@ def primary_terms
.select { |_, definition| definition[:primary] }
.keys.map(&:to_sym)

terms = [:schema_version] + terms if Hyrax.config.flexible?
terms = [:schema_version, :contexts] + terms if Hyrax.config.flexible?
terms
end

Expand All @@ -186,34 +185,6 @@ def secondary_terms
def display_additional_fields?
secondary_terms.any?
end

# OVERRIDE disposable 0.6.3 to make schema dynamic
def schema
if Hyrax.config.flexible?
Definition::Each.new(singleton_class.schema_definitions)
else
super
end
end

private

# OVERRIDE valkyrie 3.0.1 to make schema dynamic
def field(field_name)
if Hyrax.config.flexible?
singleton_class.schema_definitions.fetch(field_name.to_s)
else
super
end
end

def _form_field_definitions
if Hyrax.config.flexible?
singleton_class.schema_definitions
else
self.class.definitions
end
end
end
end
end
2 changes: 1 addition & 1 deletion app/helpers/hyrax/attributes_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ def view_options_for(presenter)
model_name = presenter.model.model_name.name

if Hyrax.config.flexible?
Hyrax::Schema.default_schema_loader.view_definitions_for(schema: model_name, version: presenter.solr_document.schema_version)
Hyrax::Schema.default_schema_loader.view_definitions_for(schema: model_name, version: presenter.solr_document.schema_version, contexts: presenter.solr_document.contexts)
else
schema = model_name.constantize.schema || (model_name + 'Resource').safe_constantize.schema
Hyrax::Schema.default_schema_loader.view_definitions_for(schema:)
Expand Down
9 changes: 8 additions & 1 deletion app/models/concerns/hyrax/flexibility.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ module Flexibility
extend ActiveSupport::Concern
included do
attribute :schema_version, Valkyrie::Types::String
attribute :contexts, Valkyrie::Types::Set.of(Valkyrie::Types::String)
end

class_methods do
Expand Down Expand Up @@ -70,13 +71,19 @@ def load(attributes, safe = false)
attributes[:schema_version] ||= Hyrax::FlexibleSchema.order('id DESC').pick(:id)
struct = allocate
schema_version = attributes[:schema_version]
struct.singleton_class.attributes(Hyrax::Schema(self, schema_version:).attributes)
contexts = attributes[:contexts] || []
struct.singleton_class.attributes(Hyrax::Schema(self, schema_version:, contexts:).attributes)
clean_attributes = safe ? struct.singleton_class.schema.call_safe(attributes) { |output = attributes| return yield output } : struct.singleton_class.schema.call_unsafe(attributes)
struct.__send__(:initialize, clean_attributes)
struct
end
end

def contexts=(value)
val = Array.wrap(value).map { |v| v.split }.flatten
@attributes[:contexts] = val
end

# Override set_value from valkyrie 3.1.1 to enable dynamic schema loading
def set_value(key, value)
@attributes[key.to_sym] = self.singleton_class.schema.key(key.to_sym).type.call(value)
Expand Down
6 changes: 5 additions & 1 deletion app/models/concerns/hyrax/solr_document_behavior.rb
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ def valkyrie?
def hydra_model(classifier: nil)
model_name = first('has_model_ssim')
valkyrie_model = "#{model_name}Resource".safe_constantize if Hyrax.config.valkyrie_transition

valkyrie_model ||
model_name&.safe_constantize ||
model_classifier(classifier).classifier(self).best_model
Expand Down Expand Up @@ -153,6 +153,10 @@ def schema_version
self['schema_version_ssi']
end

def contexts
self['contexts_ssim']
end

private

def model_classifier(classifier)
Expand Down
1 change: 1 addition & 0 deletions app/models/hyrax/file_set.rb
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ module Hyrax
class FileSet < Hyrax::Resource
include Hyrax::Schema(:core_metadata) unless Hyrax.config.flexible?
include Hyrax::Schema(:file_set_metadata) unless Hyrax.config.flexible?
# TODO why isn't this automatic?
include Hyrax::Schema('Hyrax::FileSet') if Hyrax.config.flexible?

def self.model_name(name_class: Hyrax::Name)
Expand Down
18 changes: 15 additions & 3 deletions app/models/hyrax/flexible_schema.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
# frozen_string_literal: true
class Hyrax::FlexibleSchema < ApplicationRecord
serialize :profile, coder: YAML

serialize :contexts, coder: YAML

validate :validate_profile_classes

before_save :update_contexts

def self.current_version
order("created_at asc").last.profile
end
Expand All @@ -30,7 +33,11 @@ def self.default_properties
rescue StandardError => e
[]
end


def update_contexts
self.contexts = profile['contexts']
end

def title
"#{profile['profile']['responsibility_statement']} - version #{id}"
end
Expand All @@ -43,6 +50,10 @@ def schema_version
profile['m3_version']
end

def context_select
contexts&.map { |k, v| [v&.[]('display_label'), k] }
end

def metadata_profile_type
profile['profile']['type']
end
Expand All @@ -61,7 +72,7 @@ def validate_profile_classes
required_classes = [
Hyrax.config.collection_model,
Hyrax.config.file_set_model,
Hyrax.config.admin_set_model
Hyrax.config.admin_set_model
]

if profile['classes'].blank?
Expand Down Expand Up @@ -97,6 +108,7 @@ def values_map(values)
values['predicate'] = values['property_uri']
values['index_keys'] = values['indexing']
values['multiple'] = values['multi_value']
values['context'] = values['available_on']['context']
values
end

Expand Down
16 changes: 12 additions & 4 deletions app/services/hyrax/m3_schema_loader.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ module Hyrax
#
# @see config/metadata_profiles/m3_profile.yaml for an example configuration
class M3SchemaLoader < Hyrax::SchemaLoader
def view_definitions_for(schema:, version: 1)
definitions(schema, version).each_with_object({}) do |definition, hash|
def view_definitions_for(schema:, version: 1, contexts: nil)
definitions(schema, version, contexts).each_with_object({}) do |definition, hash|
next if definition.view_options.empty?

hash[definition.name] = definition.view_options
Expand All @@ -21,11 +21,19 @@ def view_definitions_for(schema:, version: 1)
##
# @param [#to_s] schema_name
# @return [Enumerable<AttributeDefinition]
def definitions(schema_name, version)
def definitions(schema_name, version, contexts = nil)
schema = Hyrax::FlexibleSchema.find_by(id: version) || Hyrax::FlexibleSchema.create_default_schema
schema.attributes_for(schema_name).map do |name, config|
# We might be able to consolidate these conditions, but they have been kept separate to make it easier to reason about
# If there is a context filter on the metadata field and no context is set, skip it
next if contexts.blank? && config['context'].present?

# If there is a context filter on the metadata field and we have set a context, but the context does not match, skip it
next if contexts.present? && config['context'].present? && !contexts.intersect?(config['context'])

# Wew, we are in the clear to use this field
AttributeDefinition.new(name, config)
end
end.compact
rescue ActiveRecord::StatementInvalid
Rails.logger.error "Skipping definition load for migrations to run"
[]
Expand Down
14 changes: 7 additions & 7 deletions app/services/hyrax/schema_loader.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ class UndefinedSchemaError < ArgumentError; end
#
# @return [Hash<Symbol, Dry::Types::Type>] a map from attribute names to
# types
def attributes_for(schema:, version: 1)
definitions(schema, version).each_with_object({}) do |definition, hash|
def attributes_for(schema:, version: 1, contexts: nil)
definitions(schema, version, contexts).each_with_object({}) do |definition, hash|
hash[definition.name] = definition.type.meta(definition.config)
end
end
Expand All @@ -25,8 +25,8 @@ def attributes_for(schema:, version: 1)
# @param [Symbol] schema
#
# @return [Hash{Symbol => Hash{Symbol => Object}}]
def form_definitions_for(schema:, version: 1)
definitions(schema, version).each_with_object({}) do |definition, hash|
def form_definitions_for(schema:, version: 1, contexts: nil)
definitions(schema, version, contexts).each_with_object({}) do |definition, hash|
next if definition.form_options.empty?

hash[definition.name] = definition.form_options
Expand All @@ -37,8 +37,8 @@ def form_definitions_for(schema:, version: 1)
# @param [Symbol] schema
#
# @return [{Symbol => Symbol}] a map from index keys to attribute names
def index_rules_for(schema:, version: 1)
definitions(schema, version).each_with_object({}) do |definition, hash|
def index_rules_for(schema:, version: 1, contexts: nil)
definitions(schema, version, contexts).each_with_object({}) do |definition, hash|
definition.index_keys.each do |key|
hash[key] = definition.name
end
Expand Down Expand Up @@ -140,7 +140,7 @@ class UndefinedSchemaError < ArgumentError; end

private

def definitions(_schema_name, _version)
def definitions(_schema_name, _version, _contexts)
raise NotImplementedError, 'Implement #definitions in a child class'
end
end
Expand Down
4 changes: 2 additions & 2 deletions app/services/hyrax/simple_schema_loader.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ module Hyrax
#
# @see config/metadata/basic_metadata.yaml for an example configuration
class SimpleSchemaLoader < Hyrax::SchemaLoader
def view_definitions_for(schema:, _version: 1)
def view_definitions_for(schema:, version: 1, contexts: nil)
schema.each_with_object({}) do |property, metadata|
view_options = property.meta['view']
metadata[property.name.to_s] = view_options unless view_options.nil?
Expand All @@ -26,7 +26,7 @@ def permissive_schema_for_valkrie_adapter
##
# @param [#to_s] schema_name
# @return [Enumerable<AttributeDefinition]
def definitions(schema_name, _version)
def definitions(schema_name, _version, _contexts = nil)
schema_config(schema_name)['attributes'].map do |name, config|
AttributeDefinition.new(name, config)
end
Expand Down
3 changes: 3 additions & 0 deletions app/views/hyrax/admin/admin_sets/_form_metadata.html.erb
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
<%= f.input :title %>
<%= f.input :description, as: :text %>
<% if Hyrax.config.flexible? && Hyrax.config.admin_set_class.new.respond_to?(:contexts) %>
<%= f.input :contexts, collection: Hyrax::FlexibleSchema.last.context_select, input_html: { multiple: true } %>
<% end %>
1 change: 1 addition & 0 deletions app/views/records/edit_fields/_contexts.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<%= f.input key, as: :hidden %>
2 changes: 1 addition & 1 deletion app/views/shared/_schema_version.html.erb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<% schema_version = SolrDocument.find(f.object.id).schema_version if action_name == 'edit' %>
<% schema_version = Hyrax::FlexibleSchema.current_schema_id if action_name == 'new' %>
<% updating = (schema_version.to_f < @latest_schema_version) ? 'updating' : '' %>
<% updating = (schema_version.to_f < @latest_schema_version.to_f) ? 'updating' : '' %>

<div class='pull-right'>
<% if schema_version.present? %>
Expand Down
Loading

0 comments on commit 98a7d59

Please sign in to comment.