spotlight-resources-iiif 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 2fbc962270af9f8879ad88cb7518450028149e9d
4
- data.tar.gz: d1e148ef76b96e482b7813d066880ed914d667b9
3
+ metadata.gz: 99f85b3ef79f2c569645668b1bb55318756c70a5
4
+ data.tar.gz: 54414056e0566e567250875a5dca68427c1dff7e
5
5
  SHA512:
6
- metadata.gz: c0d8eb4a499d156fbce45138f8766e73840859ec380e0dcb6f18e66573f448d39caa9de0b2756104e146aa570317c4899f9d5ed53a13cb56cacf1b3623af5141
7
- data.tar.gz: 4941537eb392d728e53e34c74bf98054ad984a7519c16af5b890f276fde927cdf2d47675a06f4989e0aa35d1817fe68ad98d6b07f74cbcab1a57273a03af750b
6
+ metadata.gz: 5472e66b31c7e6e2020967ab18e63a4960584302b93f0cf42665265522c91407308fb650f6aa243982c4ea7459da1ac67fb80769198a9051b1cdba91448e0269
7
+ data.tar.gz: 0e976a850c9822d501301e66a78b68a6f40b3280b22ef8f7af6bd2b58a3018cb40fac78a2fb76d506d3a46bcaf389ec829a8d55c64f4d66a29c529bd1ef9545b
data/.rubocop.yml CHANGED
@@ -3,6 +3,7 @@ inherit_from: .rubocop_todo.yml
3
3
  require: rubocop-rspec
4
4
 
5
5
  AllCops:
6
+ TargetRubyVersion: 2.2
6
7
  Exclude:
7
8
  - 'Gemfile'
8
9
  - 'bin/**/*'
@@ -0,0 +1,32 @@
1
+ module Spotlight
2
+ module Resources
3
+ class IiifHarvesterController < Spotlight::ApplicationController
4
+ before_action :authenticate_user!
5
+
6
+ load_and_authorize_resource :exhibit, class: Spotlight::Exhibit
7
+ before_action :build_resource
8
+
9
+ def create
10
+ if @resource.save_and_index
11
+ redirect_to spotlight.admin_exhibit_catalog_index_path(current_exhibit, sort: :timestamp)
12
+ else
13
+ flash[:error] = @resource.errors.values.join(', ') if @resource.errors.present?
14
+ redirect_to spotlight.new_exhibit_resource_path(current_exhibit)
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ def resource_params
21
+ params.require(:resources_iiif_harvester).permit(:url)
22
+ end
23
+
24
+ def build_resource
25
+ @resource ||= Spotlight::Resources::IiifHarvester.create(
26
+ url: resource_params[:url],
27
+ exhibit: current_exhibit
28
+ )
29
+ end
30
+ end
31
+ end
32
+ end
@@ -1,10 +1,43 @@
1
- module Spotlight::Resources
2
- # harvest Images from IIIF Manifest and turn them into a Spotlight::Resource
3
- class IiifHarvester < Spotlight::Resource
4
- self.weight = -5000
1
+ require 'iiif/presentation'
5
2
 
6
- after_save :harvest_resources
3
+ module Spotlight
4
+ module Resources
5
+ # harvest Images from IIIF Manifest and turn them into a Spotlight::Resource
6
+ # Note: IIIF API : http://iiif.io/api/presentation/2.0
7
+ class IiifHarvester < Spotlight::Resource
8
+ self.weight = -5000
7
9
 
10
+ validate :valid_url?
8
11
 
12
+ def to_solr
13
+ return to_enum(:to_solr) { 0 } unless block_given?
14
+
15
+ base_doc = super
16
+ iiif_manifests.each do |manifest|
17
+ manifest.with_exhibit(exhibit)
18
+ manifest_solr = manifest.to_solr
19
+ yield base_doc.merge(manifest_solr) if manifest_solr.present?
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def iiif_manifests
26
+ @iiif_manifests ||= IiifService.parse(url)
27
+ end
28
+
29
+ def valid_url?
30
+ errors.add(:url, 'Invalid IIIF URL') unless url_is_iiif?(url)
31
+ end
32
+
33
+ def url_is_iiif?(url)
34
+ valid_content_types = ['application/json', 'application/ld+json']
35
+ req = Faraday.head(url)
36
+ return unless req.success?
37
+ valid_content_types.any? do |valid_type|
38
+ req.headers['content-type'].include?(valid_type)
39
+ end
40
+ end
41
+ end
9
42
  end
10
- end
43
+ end
@@ -0,0 +1,186 @@
1
+ module Spotlight
2
+ module Resources
3
+ ##
4
+ # A PORO to construct a solr hash for a given IiifManifest
5
+ class IiifManifest
6
+ def initialize(attrs = {})
7
+ @url = attrs[:url]
8
+ @manifest = attrs[:manifest]
9
+ @solr_hash = {}
10
+ end
11
+
12
+ def to_solr
13
+ add_document_id
14
+ add_label
15
+ add_thumbnail_url
16
+ add_manifest_url
17
+ add_image_urls
18
+ add_metadata
19
+ solr_hash
20
+ end
21
+
22
+ def with_exhibit(e)
23
+ @exhibit = e
24
+ end
25
+
26
+ private
27
+
28
+ attr_reader :url, :manifest, :exhibit, :solr_hash
29
+ delegate :blacklight_config, to: :exhibit
30
+
31
+ def compound_id
32
+ Digest::MD5.hexdigest("#{exhibit.id}-#{url}")
33
+ end
34
+
35
+ def add_document_id
36
+ solr_hash[exhibit.blacklight_config.document_model.unique_key.to_sym] = compound_id
37
+ end
38
+
39
+ def add_manifest_url
40
+ solr_hash[Spotlight::Resources::Iiif::Engine.config.iiif_manifest_field] = url
41
+ end
42
+
43
+ def add_thumbnail_url
44
+ return unless thumbnail_field && manifest['thumbnail'].present?
45
+ solr_hash[thumbnail_field] = manifest['thumbnail']['@id']
46
+ end
47
+
48
+ def add_label
49
+ return unless title_field && manifest.try(:label)
50
+ solr_hash[title_field] = manifest.label
51
+ end
52
+
53
+ def add_image_urls
54
+ solr_hash[tile_source_field] = image_urls
55
+ end
56
+
57
+ def add_metadata
58
+ solr_hash.merge!(manifest_metadata)
59
+ sidecar.update(data: sidecar.data.merge(manifest_metadata))
60
+ end
61
+
62
+ def manifest_metadata
63
+ metadata = metadata_class.new(manifest).to_solr
64
+ return {} unless metadata.present?
65
+ create_sidecars_for(*metadata.keys)
66
+
67
+ metadata.each_with_object({}) do |(key, value), hash|
68
+ next unless (field = exhibit_custom_fields[key])
69
+ hash[field.field] = value
70
+ end
71
+ end
72
+
73
+ def create_sidecars_for(*keys)
74
+ missing_keys(keys).each do |k|
75
+ exhibit.custom_fields.create! label: k
76
+ end
77
+ @exhibit_custom_fields = nil
78
+ end
79
+
80
+ def missing_keys(keys)
81
+ custom_field_keys = exhibit_custom_fields.keys.map(&:downcase)
82
+ keys.reject do |key|
83
+ custom_field_keys.include?(key.downcase)
84
+ end
85
+ end
86
+
87
+ def exhibit_custom_fields
88
+ @exhibit_custom_fields ||= exhibit.custom_fields.each_with_object({}) do |value, hash|
89
+ hash[value.label] = value
90
+ end
91
+ end
92
+
93
+ def image_urls
94
+ @image_urls ||= resources.map do |resource|
95
+ next unless resource && !resource.service.empty?
96
+ image_url = resource.service['@id']
97
+ image_url << '/info.json' unless image_url.downcase.ends_with?('/info.json')
98
+ image_url
99
+ end
100
+ end
101
+
102
+ def resources
103
+ @resources ||= sequences
104
+ .flat_map(&:canvases)
105
+ .flat_map(&:images)
106
+ .flat_map(&:resource)
107
+ end
108
+
109
+ def sequences
110
+ manifest.try(:sequences) || []
111
+ end
112
+
113
+ def thumbnail_field
114
+ blacklight_config.index.try(:thumbnail_field)
115
+ end
116
+
117
+ def tile_source_field
118
+ blacklight_config.show.try(:tile_source_field)
119
+ end
120
+
121
+ def title_field
122
+ blacklight_config.index.try(:title_field)
123
+ end
124
+
125
+ def sidecar
126
+ @sidecar ||= document_model.new(id: compound_id).sidecar(exhibit)
127
+ end
128
+
129
+ def document_model
130
+ exhibit.blacklight_config.document_model
131
+ end
132
+
133
+ def metadata_class
134
+ Spotlight::Resources::Iiif::Engine.config.metadata_class.call
135
+ end
136
+
137
+ ###
138
+ # A simple class to map the metadata field
139
+ # in a IIIF document to label/value pairs
140
+ # This is intended to be overriden by an
141
+ # application if a different metadata
142
+ # strucure is used by the consumer
143
+ class Metadata
144
+ def initialize(manifest)
145
+ @manifest = manifest
146
+ end
147
+
148
+ def to_solr
149
+ metadata_hash.merge(manifest_level_metadata)
150
+ end
151
+
152
+ private
153
+
154
+ attr_reader :manifest
155
+
156
+ def metadata
157
+ manifest.try(:metadata) || []
158
+ end
159
+
160
+ def metadata_hash
161
+ return {} unless metadata.present?
162
+ return {} unless metadata.is_a?(Array)
163
+
164
+ metadata.each_with_object({}) do |md, hash|
165
+ next unless md['label'] && md['value']
166
+ hash[md['label']] ||= []
167
+ hash[md['label']] += Array(md['value'])
168
+ end
169
+ end
170
+
171
+ def manifest_level_metadata
172
+ manifest_fields.each_with_object({}) do |field, hash|
173
+ next unless manifest.respond_to?(field) &&
174
+ manifest.send(field).present?
175
+ hash[field.capitalize] ||= []
176
+ hash[field.capitalize] += Array(manifest.send(field))
177
+ end
178
+ end
179
+
180
+ def manifest_fields
181
+ %w(attribution description license)
182
+ end
183
+ end
184
+ end
185
+ end
186
+ end
@@ -0,0 +1,85 @@
1
+ require 'iiif/presentation'
2
+ module Spotlight
3
+ module Resources
4
+ ###
5
+ # Wrapper around IIIIF-Presentation's IIIF::Service that provides the
6
+ # ability to recursively traverse through all collections and manifests
7
+ class IiifService
8
+ def initialize(url)
9
+ @url = url
10
+ end
11
+
12
+ def collections
13
+ @collections ||= (object.try(:collections) || []).map do |collection|
14
+ self.class.new(collection['@id'])
15
+ end
16
+ end
17
+
18
+ def manifests
19
+ @manifests ||= if manifest?
20
+ [create_iiif_manifest(object)]
21
+ else
22
+ things = []
23
+ things << create_iiif_manifest(object) if collection?
24
+ (object.try(:manifests) || []).each do |manifest|
25
+ things << create_iiif_manifest(
26
+ self.class.new(manifest['@id']).object
27
+ )
28
+ end
29
+ things
30
+ end
31
+ end
32
+
33
+ def self.parse(url)
34
+ recursive_manifests(new(url))
35
+ end
36
+
37
+ protected
38
+
39
+ def object
40
+ @object ||= IIIF::Service.parse(response)
41
+ end
42
+
43
+ private
44
+
45
+ attr_reader :url
46
+
47
+ class << self
48
+ private
49
+
50
+ def recursive_manifests(thing, &block)
51
+ return to_enum(:recursive_manifests, thing) unless block_given?
52
+
53
+ thing.manifests.each(&block)
54
+
55
+ thing.collections.each do |collection|
56
+ recursive_manifests(collection, &block)
57
+ end if thing.collections.present?
58
+ end
59
+
60
+ def iiif_response(url)
61
+ Faraday.get(url).body
62
+ rescue Faraday::Error::ConnectionFailed, Faraday::TimeoutError => e
63
+ Rails.logger.warn("HTTP GET for #{url} failed with #{e}")
64
+ {}.to_json
65
+ end
66
+ end
67
+
68
+ def create_iiif_manifest(manifest)
69
+ IiifManifest.new(url: manifest['@id'], manifest: manifest)
70
+ end
71
+
72
+ def manifest?
73
+ object.is_a?(IIIF::Presentation::Manifest)
74
+ end
75
+
76
+ def collection?
77
+ object.is_a?(IIIF::Presentation::Collection)
78
+ end
79
+
80
+ def response
81
+ @response ||= self.class.iiif_response(url)
82
+ end
83
+ end
84
+ end
85
+ end
@@ -1,4 +1,4 @@
1
- <%= bootstrap_form_for([current_exhibit, @resource], layout: :horizontal, label_col: 'col-md-2', control_col: 'col-sm-6 col-md-6', html: { class: 'item-upload-form', multipart: true } ) do |f| %>
1
+ <%= bootstrap_form_for([spotlight_resources_iiif_engine, current_exhibit, Spotlight::Resources::IiifHarvester.new], layout: :horizontal, label_col: 'col-md-2', control_col: 'col-sm-6 col-md-6' ) do |f| %>
2
2
  <%= f.text_field :url, help: t('.url-field.help'), label: t('.manifest') %>
3
3
  <div class="form-actions">
4
4
  <div class="primary-actions">
@@ -2,10 +2,9 @@ en:
2
2
  spotlight:
3
3
  resources:
4
4
  iiif:
5
- tabbed_form:
6
- manifest: "IIIF Manifest"
7
- manifest:
8
- manifest: "Manifest"
9
- add_item: "Add IIIF manifest URLs"
5
+ form:
6
+ title: 'IIIF URL'
7
+ manifest: "URL"
8
+ add_item: "Add IIIF items"
10
9
  url-field:
11
- help: "Add the URL of a IIIF manifest."
10
+ help: "Add the URL of a IIIF manifest or collection."
data/config/routes.rb ADDED
@@ -0,0 +1,5 @@
1
+ Spotlight::Resources::Iiif::Engine.routes.draw do
2
+ resources :exhibits, only: [] do
3
+ resource :iiif_harvester, controller: :"spotlight/resources/iiif_harvester", only: :create, as: :resources_iiif_harvesters
4
+ end
5
+ end
@@ -0,0 +1,15 @@
1
+ require 'rails/generators'
2
+
3
+ module Spotlight
4
+ module Resources
5
+ module Iiif
6
+ class InstallGenerator < Rails::Generators::Base
7
+ desc 'This generator mounts the Spotlight::Resources::Iiif engine'
8
+
9
+ def inject_spotlight_iiif_routes
10
+ route "mount Spotlight::Resources::Iiif::Engine, at: 'spotlight_resources_iiif'"
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -1,4 +1,6 @@
1
- require "spotlight/resources/iiif/version"
1
+ require 'spotlight/resources/iiif/version'
2
+ require 'spotlight'
3
+ require 'faraday'
2
4
 
3
5
  module Spotlight
4
6
  module Resources
@@ -2,11 +2,14 @@ require 'spotlight/engine'
2
2
 
3
3
  module Spotlight::Resources::Iiif
4
4
  class Engine < ::Rails::Engine
5
- Spotlight::Resources::Iiif::Engine.config.resource_partials = ['spotlight/resources/iiif/manifest']
5
+ Spotlight::Resources::Iiif::Engine.config.resource_partial = 'spotlight/resources/iiif/form'
6
+ Spotlight::Resources::Iiif::Engine.config.metadata_class = -> { Spotlight::Resources::IiifManifest::Metadata }
7
+ Spotlight::Resources::Iiif::Engine.config.iiif_manifest_field = :content_metadata_iiif_manifest_ssm
8
+
9
+
6
10
  initializer 'spotlight.resources.iiif.initialize' do
7
- Spotlight::Engine.config.resource_providers << Spotlight::Resources::IiifHarvester
8
- Spotlight::Engine.config.new_resource_partials ||= []
9
- Spotlight::Engine.config.new_resource_partials << 'spotlight/resources/iiif/tabbed_form'
11
+ Spotlight::Engine.config.external_resources_partials ||= []
12
+ Spotlight::Engine.config.external_resources_partials << Spotlight::Resources::Iiif::Engine.config.resource_partial
10
13
  end
11
14
  end
12
- end
15
+ end
@@ -1,7 +1,7 @@
1
1
  module Spotlight
2
2
  module Resources
3
3
  module Iiif
4
- VERSION = "0.0.1"
4
+ VERSION = "0.1.0"
5
5
  end
6
6
  end
7
7
  end
@@ -6,8 +6,8 @@ require 'spotlight/resources/iiif/version'
6
6
  Gem::Specification.new do |spec|
7
7
  spec.name = "spotlight-resources-iiif"
8
8
  spec.version = Spotlight::Resources::Iiif::VERSION
9
- spec.authors = ["Naomi Dushay"]
10
- spec.email = ["ndushay@stanford.edu"]
9
+ spec.authors = ["Naomi Dushay", "Peter Mangiafico"]
10
+ spec.email = ["ndushay@stanford.edu", "petucket@stanford.edu"]
11
11
 
12
12
  spec.summary = 'Spotlight Resource Indexer for IIIF manifests or collections.'
13
13
  spec.homepage = "https://github.com/sul-dlss/spotlight-resources-iiif"
@@ -19,12 +19,14 @@ Gem::Specification.new do |spec|
19
19
  spec.require_paths = ["lib"]
20
20
 
21
21
  spec.add_dependency "blacklight-spotlight"
22
+ spec.add_dependency "faraday"
23
+ spec.add_dependency "iiif-presentation"
22
24
 
23
25
  spec.add_development_dependency "bundler", "~> 1.10"
24
26
  spec.add_development_dependency "rake", "~> 10.0"
25
27
  spec.add_development_dependency "rspec-rails"
26
28
  spec.add_development_dependency 'capybara'
27
- spec.add_development_dependency 'factory_girl', '~> 4.5'
29
+ spec.add_development_dependency 'factory_girl_rails'
28
30
  spec.add_development_dependency 'database_cleaner', '~> 1.3'
29
31
  # spec.add_development_dependency 'poltergeist', '>= 1.5.0' # for js testing using phantomjs
30
32
  spec.add_development_dependency "coveralls"
metadata CHANGED
@@ -1,14 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: spotlight-resources-iiif
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Naomi Dushay
8
+ - Peter Mangiafico
8
9
  autorequire:
9
10
  bindir: exe
10
11
  cert_chain: []
11
- date: 2015-11-25 00:00:00.000000000 Z
12
+ date: 2016-01-28 00:00:00.000000000 Z
12
13
  dependencies:
13
14
  - !ruby/object:Gem::Dependency
14
15
  name: blacklight-spotlight
@@ -24,6 +25,34 @@ dependencies:
24
25
  - - ">="
25
26
  - !ruby/object:Gem::Version
26
27
  version: '0'
28
+ - !ruby/object:Gem::Dependency
29
+ name: faraday
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - ">="
33
+ - !ruby/object:Gem::Version
34
+ version: '0'
35
+ type: :runtime
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: '0'
42
+ - !ruby/object:Gem::Dependency
43
+ name: iiif-presentation
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '0'
49
+ type: :runtime
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: '0'
27
56
  - !ruby/object:Gem::Dependency
28
57
  name: bundler
29
58
  requirement: !ruby/object:Gem::Requirement
@@ -81,19 +110,19 @@ dependencies:
81
110
  - !ruby/object:Gem::Version
82
111
  version: '0'
83
112
  - !ruby/object:Gem::Dependency
84
- name: factory_girl
113
+ name: factory_girl_rails
85
114
  requirement: !ruby/object:Gem::Requirement
86
115
  requirements:
87
- - - "~>"
116
+ - - ">="
88
117
  - !ruby/object:Gem::Version
89
- version: '4.5'
118
+ version: '0'
90
119
  type: :development
91
120
  prerelease: false
92
121
  version_requirements: !ruby/object:Gem::Requirement
93
122
  requirements:
94
- - - "~>"
123
+ - - ">="
95
124
  - !ruby/object:Gem::Version
96
- version: '4.5'
125
+ version: '0'
97
126
  - !ruby/object:Gem::Dependency
98
127
  name: database_cleaner
99
128
  requirement: !ruby/object:Gem::Requirement
@@ -223,6 +252,7 @@ dependencies:
223
252
  description:
224
253
  email:
225
254
  - ndushay@stanford.edu
255
+ - petucket@stanford.edu
226
256
  executables: []
227
257
  extensions: []
228
258
  extra_rdoc_files: []
@@ -237,13 +267,17 @@ files:
237
267
  - LICENSE.txt
238
268
  - README.md
239
269
  - Rakefile
270
+ - app/controllers/spotlight/resources/iiif_harvester_controller.rb
240
271
  - app/models/spotlight/resources/iiif_harvester.rb
241
- - app/views/spotlight/resources/iiif/_manifest.html.erb
242
- - app/views/spotlight/resources/iiif/_tabbed_form.html.erb
272
+ - app/models/spotlight/resources/iiif_manifest.rb
273
+ - app/models/spotlight/resources/iiif_service.rb
274
+ - app/views/spotlight/resources/iiif/_form.html.erb
243
275
  - bin/console
244
276
  - bin/setup
245
277
  - config/jetty.yml
246
278
  - config/locales/spotlight-resources-iiif.en.yml
279
+ - config/routes.rb
280
+ - lib/generators/spotlight/resources/iiif/install_generator.rb
247
281
  - lib/spotlight/resources/iiif.rb
248
282
  - lib/spotlight/resources/iiif/engine.rb
249
283
  - lib/spotlight/resources/iiif/version.rb
@@ -1,18 +0,0 @@
1
- <div role="tabpanel" class="item-upload-tabs">
2
- <ul class="nav nav-tabs" role="tablist">
3
- <% Spotlight::Resources::Iiif::Engine.config.resource_partials.each_with_index do |p, i| %>
4
- <% name = p.split('/').last %>
5
- <li role="presentation" class="<%= "active" if i ==0 %>">
6
- <%= link_to t(".#{name}"), "##{name}", role: 'tab', 'data-toggle' => 'tab', 'aria-controls' => 'single' %>
7
- </li>
8
- <% end %>
9
- </ul>
10
- <div class="tab-content">
11
- <% Spotlight::Resources::Iiif::Engine.config.resource_partials.each_with_index do |p, i| %>
12
- <% name = p.split('/').last %>
13
- <%= content_tag :div, id: name, role: 'tabpanel', class: "tab-pane #{"active" if i == 0}" do %>
14
- <%= render p %>
15
- <% end %>
16
- <% end %>
17
- </div>
18
- </div>