Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ensures that Ephemera Projects have IIIF Manifests #385

Merged
merged 7 commits into from
Oct 18, 2017
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ def initialize(change_set_persister:, change_set:, post_save_resource: nil)

def run
messenger.record_updated(post_save_resource)
# For cases where the resource is a FileSet, propagate for the parent resource
messenger.record_updated(post_save_resource.decorate.parent) if post_save_resource.is_a? FileSet
end

delegate :messenger, to: :change_set_persister
Expand Down
30 changes: 28 additions & 2 deletions app/change_sets/ephemera_project_change_set.rb
Original file line number Diff line number Diff line change
@@ -1,10 +1,36 @@
# frozen_string_literal: true
class EphemeraProjectChangeSet < Valkyrie::ChangeSet
validates :title, presence: true
validates :title, :slug, presence: true
property :title, multiple: false
property :member_ids, multiple: true, required: false, type: Types::Strict::Array.member(Valkyrie::Types::ID)
property :slug, multiple: false, required: true
validate :slug_unique?
validate :slug_valid?

def primary_terms
[:title]
[:title, :slug]
end

def slug_valid?
return if Slug.new(Array.wrap(slug).first).valid?
errors.add(:slug, 'contains invalid characters, please only use alphanumerics, dashes, and underscores')
end

def slug_unique?
return unless slug_exists?
errors.add(:slug, 'is already in use by another project')
end

private

def metadata_adapter
Valkyrie.config.metadata_adapter
end
delegate :query_service, to: :metadata_adapter

def slug_exists?
slug_value = Array.wrap(slug).first
results = query_service.custom_queries.find_by_string_property(property: :slug, value: slug_value).to_a
!results.empty?
end
end
11 changes: 10 additions & 1 deletion app/controllers/ephemera_projects_controller.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# frozen_string_literal: true
class EphemeraProjectsController < ApplicationController
class EphemeraProjectsController < BaseResourceController
include Valhalla::ResourceController
include TokenAuth
self.change_set_class = DynamicChangeSet
Expand All @@ -14,6 +14,15 @@ def index
render 'index'
end

def manifest
@resource = find_resource(params[:id])
respond_to do |f|
f.json do
render json: ManifestBuilder.new(@resource).build
end
end
end

private

def load_ephemera_projects
Expand Down
2 changes: 1 addition & 1 deletion app/decorators/collection_decorator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ def manageable_files?
end

def members
@members ||= query_service.find_inverse_references_by(resource: self, property: :member_of_collection_ids).to_a
@members ||= query_service.find_inverse_references_by(resource: model, property: :member_of_collection_ids).to_a
end

# Nested collections are not currently supported
Expand Down
10 changes: 9 additions & 1 deletion app/decorators/ephemera_box_decorator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,21 @@ def title
end

def members
@members ||= find_members(resource: model)
@members ||= query_service.find_members(resource: model).to_a
end

def folders
@folders ||= members.select { |r| r.is_a?(EphemeraFolder) }.map(&:decorate).to_a
end

def ephemera_projects
@ephemera_projects ||= query_service.find_parents(resource: model).map(&:decorate).to_a
end

def collection_slugs
@collection_slugs ||= ephemera_projects.map(&:slug)
end

def ephemera_project
@ephemera_box ||= query_service.find_parents(resource: model).to_a.first.try(:decorate) || NullProject.new
end
Expand Down
4 changes: 4 additions & 0 deletions app/decorators/ephemera_field_decorator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ class EphemeraFieldDecorator < Valkyrie::ResourceDecorator

# Retrieves the EphemeraProjects to which this EphemeraField is linked
# @return [Array<EphemeraProject>]
def parents
@parents ||= query_service.find_parents(resource: model).to_a
end

def projects
@projects ||= parents.select { |r| r.is_a?(EphemeraProject) }.map(&:decorate).to_a
end
Expand Down
15 changes: 11 additions & 4 deletions app/decorators/ephemera_folder_decorator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,11 @@ class EphemeraFolderDecorator < Valkyrie::ResourceDecorator
]
self.iiif_manifest_attributes = display_attributes + [:title] - \
imported_attributes(Schema::Common.attributes) - \
Schema::IIIF.attributes - \
[:visibility, :internal_resource, :rights_statement, :rendered_rights_statement, :thumbnail_id, :provenance]
Schema::IIIF.attributes - [:visibility, :internal_resource, :rights_statement, :rendered_rights_statement, :thumbnail_id]

def members
@members ||= query_service.find_members(resource: model).to_a
end

def collections
@collections ||= query_service.find_references_by(resource: self, property: :member_of_collection_ids).to_a.map(&:decorate)
Expand Down Expand Up @@ -63,11 +66,15 @@ def rendered_rights_statement
end

def ephemera_box
@ephemera_box ||= query_service.find_parents(resource: model).to_a.first.try(:decorate)
@ephemera_box ||= query_service.find_parents(resource: model).map(&:decorate).to_a.first
end

def ephemera_project
@ephemera_project ||= ephemera_box.ephemera_project
@ephemera_project ||= ephemera_box.try(:ephemera_project)
end

def collection_slugs
@collection_slugs ||= Array.wrap(ephemera_project.try(:slug))
end

def manageable_files?
Expand Down
20 changes: 20 additions & 0 deletions app/decorators/ephemera_project_decorator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
class EphemeraProjectDecorator < Valkyrie::ResourceDecorator
self.display_attributes = [:title]

def members
@members ||= query_service.find_members(resource: model).to_a
end

def boxes
@boxes ||= members.select { |r| r.is_a?(EphemeraBox) }.map(&:decorate).to_a
end
Expand All @@ -25,4 +29,20 @@ def manageable_structure?
def title
super.first
end

def slug
Array.wrap(super).first
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we want it to be single-value, we should enforce that on the changeset. But I'm not sure we want it to be single-value. I'm concerned we actually need 2 properties, like current_slug and all_slugs, so that the front-end app can continue routing to old / bookmarked urls. I think this may need more discussion / a new issue.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, it already is enforced on the changeset, okay. Well the rest of my concern still stands :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've created a new issue for handling changed slugs gracefully — not sure if it's ultimately something that needs to change in DPUL or Figgy, but it's something that should work: #395

end

def iiif_manifest_attributes
local_attributes(self.class.iiif_manifest_attributes).merge iiif_manifest_exhibit
end

private

# Generate the Hash for the IIIF Manifest metadata exposing the slug as an "Exhibit" property
# @return [Hash] the exhibit metadata hash
def iiif_manifest_exhibit
{ exhibit: slug }
end
end
8 changes: 8 additions & 0 deletions app/decorators/file_set_decorator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,19 @@ def manageable_files?
false
end

def parents
query_service.find_parents(resource: model).to_a.map(&:decorate)
end

def parent
parents.first
end

def collections
[]
end

def collection_slugs
@collection_slugs ||= parent.try(:collection_slugs)
end
end
6 changes: 6 additions & 0 deletions app/decorators/index_collection_decorator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# frozen_string_literal: true
class IndexCollectionDecorator < Valkyrie::ResourceDecorator
def iiif_manifest_attributes
[]
end
end
4 changes: 4 additions & 0 deletions app/decorators/scanned_map_decorator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ class ScannedMapDecorator < Valkyrie::ResourceDecorator
self.iiif_manifest_attributes = display_attributes + [:title] - \
Schema::IIIF.attributes - [:visibility, :internal_resource, :rights_statement, :rendered_rights_statement, :thumbnail_id]

def members
@members ||= query_service.find_members(resource: model).to_a
end

def scanned_map_members
@scanned_maps ||= members.select { |r| r.is_a?(ScannedMap) }.map(&:decorate).to_a
end
Expand Down
8 changes: 8 additions & 0 deletions app/decorators/scanned_resource_decorator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ class ScannedResourceDecorator < Valkyrie::ResourceDecorator
Schema::IIIF.attributes - [:visibility, :internal_resource, :rights_statement, :rendered_rights_statement, :thumbnail_id]
delegate(*Schema::Common.attributes, to: :primary_imported_metadata, prefix: :imported)

def members
@members ||= query_service.find_members(resource: model).to_a
end

def volumes
@volumes ||= members.select { |r| r.is_a?(ScannedResource) }.map(&:decorate).to_a
end
Expand Down Expand Up @@ -65,6 +69,10 @@ def parents
end
alias collections parents

def collection_slugs
@collection_slugs ||= collections.map(&:slug)
end

def display_imported_language
(imported_language || []).map do |language|
ControlledVocabulary.for(:language).find(language).label
Expand Down
4 changes: 4 additions & 0 deletions app/decorators/vector_work_decorator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
class VectorWorkDecorator < Valkyrie::ResourceDecorator
self.display_attributes += Schema::Geo.attributes + [:rendered_coverage, :member_of_collections] - [:thumbnail_id, :coverage]

def members
@members ||= query_service.find_members(resource: model).to_a
end

# Use case for nesting vector works
# - time series: e.g., nyc transit system, released every 6 months
def vector_work_members
Expand Down
5 changes: 5 additions & 0 deletions app/models/ephemera_project.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,9 @@ class EphemeraProject < Valhalla::Resource
attribute :id, Valkyrie::Types::ID.optional
attribute :member_ids, Valkyrie::Types::Array
attribute :title, Valkyrie::Types::Set
attribute :slug, Valkyrie::Types::Set

def logical_structure
[]
end
end
10 changes: 10 additions & 0 deletions app/models/index_collection.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
# frozen_string_literal: true
# Model for exposing IIIF Manifests for all Collection resources
class IndexCollection
# Decorates the object (as this is not a Valkyrie::Resource)
# @return [IndexCollectionDecorator] an instance of the decorator
def decorate
IndexCollectionDecorator.new(self)
end

def logical_structure
[]
end
end
45 changes: 23 additions & 22 deletions app/services/manifest_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,12 @@ def build
# Presenter modeling the Resource subjects as root nodes
class RootNode
def self.for(resource)
if resource.is_a?(Collection)
case resource
when Collection
CollectionNode.new(resource)
elsif resource.is_a?(IndexCollection)
when IndexCollection
IndexCollectionNode.new(resource)
elsif resource.is_a?(ScannedMap)
when ScannedMap
ScannedMapNode.new(resource)
else
new(resource)
Expand All @@ -48,7 +49,12 @@ def to_s
end

def description
value = resource.respond_to?(:primary_imported_metadata) ? resource.primary_imported_metadata.description : resource.description
value = if resource.respond_to?(:primary_imported_metadata)
resource.primary_imported_metadata.try(:description)
else
resource.try(:description)
end

Array.wrap(value).first
end

Expand Down Expand Up @@ -106,7 +112,7 @@ def helper
# Retrieve the child members for the subject resource of the Manifest
# @return [Resource]
def members
@members ||= query_service.find_members(resource: resource).to_a
@members ||= decorate.members
end

##
Expand Down Expand Up @@ -136,32 +142,20 @@ def file_set_presenters
[]
end

def members
@members ||= query_service.find_inverse_references_by(resource: resource, property: :member_of_collection_ids).to_a
end

def viewing_hint
nil
end

def description
resource.description
end
end

class IndexCollectionNode < RootNode
def file_set_presenters
[]
def viewing_hint
'multi-part'
end
end

class IndexCollectionNode < CollectionNode
def members
@members ||= query_service.find_all_of_model(model: Collection).to_a
end

def viewing_hint
nil
end

def manifest_url
helper.index_manifest_url
end
Expand All @@ -173,6 +167,10 @@ def to_s
def description
"All collections which are a part of Plum."
end

def id
nil
end
end

class ScannedMapNode < RootNode
Expand Down Expand Up @@ -328,8 +326,11 @@ def default_url_options
end

def manifest_url(resource)
if resource.is_a?(Collection)
case resource
when Collection
"#{protocol}://#{host}/collections/#{resource.id}/manifest"
when FileSet
"#{protocol}://#{host}/concern/#{resource.decorate.parent.model_name.plural}/#{resource.decorate.parent.id}/manifest"
else
"#{protocol}://#{host}/concern/#{resource.model_name.plural}/#{resource.id}/manifest"
end
Expand Down
13 changes: 13 additions & 0 deletions app/services/manifest_builder/manifest_service_locator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,19 @@ def child_collection_builder
)
end

# Provides the builders injected into the factory for manifests of collections
# @see IIIFManifest::ManifestServiceLocator#collection_manifest_builder
# @return [IIIFManifest::ManifestBuilder::CompositeBuilderFactory] the factory of multiple builders
def collection_manifest_builders
composite_builder_factory.new(
record_property_builder,
child_manifest_builder_factory,
metadata_manifest_builder,
see_also_builder,
composite_builder: composite_builder
)
end

class CollectionManifestBuilder < IIIFManifest::ManifestBuilder
def apply(collection)
collection['collections'] ||= []
Expand Down
Loading