blacklight-spotlight 0.12.1 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/stylesheets/spotlight/_exhibits_index.scss +16 -0
  3. data/app/assets/stylesheets/spotlight/_nestable.scss +10 -10
  4. data/app/controllers/spotlight/catalog_controller.rb +10 -7
  5. data/app/controllers/spotlight/concerns/catalog_search_context.rb +15 -2
  6. data/app/controllers/spotlight/exhibits_controller.rb +1 -0
  7. data/app/helpers/spotlight/application_helper.rb +1 -1
  8. data/app/models/concerns/spotlight/solr_document.rb +20 -20
  9. data/app/models/concerns/spotlight/user.rb +1 -0
  10. data/app/models/spotlight/blacklight_configuration.rb +10 -4
  11. data/app/models/spotlight/custom_field.rb +5 -1
  12. data/app/models/spotlight/exhibit.rb +1 -1
  13. data/app/models/spotlight/resource.rb +1 -1
  14. data/app/models/spotlight/search.rb +1 -1
  15. data/app/models/spotlight/solr_document_sidecar.rb +1 -1
  16. data/app/views/layouts/spotlight/spotlight.html.erb +1 -1
  17. data/app/views/spotlight/exhibits/_exhibit_card.html.erb +3 -0
  18. data/app/views/spotlight/exhibits/index.html.erb +40 -15
  19. data/config/locales/spotlight.en.yml +4 -1
  20. data/lib/spotlight/catalog.rb +14 -11
  21. data/lib/spotlight/catalog/access_controls_enforcement.rb +2 -1
  22. data/lib/spotlight/engine.rb +5 -0
  23. data/lib/spotlight/version.rb +1 -1
  24. data/spec/controllers/spotlight/about_pages_controller_spec.rb +1 -1
  25. data/spec/controllers/spotlight/appearances_controller_spec.rb +10 -8
  26. data/spec/controllers/spotlight/browse_controller_spec.rb +3 -3
  27. data/spec/controllers/spotlight/catalog_controller_spec.rb +88 -7
  28. data/spec/controllers/spotlight/confirmations_controller_spec.rb +7 -4
  29. data/spec/controllers/spotlight/contact_forms_controller_spec.rb +1 -1
  30. data/spec/controllers/spotlight/exhibits_controller_spec.rb +25 -21
  31. data/spec/controllers/spotlight/home_pages_controller_spec.rb +23 -24
  32. data/spec/controllers/spotlight/metadata_configurations_controller_spec.rb +17 -13
  33. data/spec/controllers/spotlight/resources/upload_controller_spec.rb +1 -1
  34. data/spec/controllers/spotlight/resources_controller_spec.rb +3 -3
  35. data/spec/controllers/spotlight/roles_controller_spec.rb +5 -3
  36. data/spec/controllers/spotlight/search_configurations_controller_spec.rb +10 -8
  37. data/spec/controllers/spotlight/searches_controller_spec.rb +12 -8
  38. data/spec/controllers/spotlight/solr_controller_spec.rb +34 -29
  39. data/spec/controllers/spotlight/versions_controller_spec.rb +2 -2
  40. data/spec/controllers/spotlight/view_configurations_controller_spec.rb +7 -5
  41. data/spec/helpers/spotlight/application_helper_spec.rb +1 -1
  42. data/spec/helpers/spotlight/pages_helper_spec.rb +2 -2
  43. data/spec/models/solr_document_spec.rb +5 -5
  44. data/spec/models/spotlight/custom_field_spec.rb +8 -9
  45. data/spec/models/spotlight/resources/open_graph_spec.rb +1 -1
  46. data/spec/models/spotlight/search_spec.rb +10 -0
  47. data/spec/models/spotlight/sitemap_spec.rb +1 -1
  48. data/spec/models/spotlight/solr_document/atomic_updates_spec.rb +1 -1
  49. data/spec/models/spotlight/solr_document/uploaded_resource_spec.rb +6 -6
  50. data/spec/views/spotlight/exhibits/index.html.erb_spec.rb +35 -6
  51. data/spec/views/spotlight/sir_trevor/blocks/_solr_documents_block.html.erb_spec.rb +1 -1
  52. data/spec/views/spotlight/sir_trevor/blocks/_solr_documents_carousel_block.html.erb_spec.rb +5 -1
  53. data/spec/views/spotlight/sir_trevor/blocks/_solr_documents_embed_block.html.erb_spec.rb +1 -1
  54. data/spec/views/spotlight/sir_trevor/blocks/_solr_documents_features_block.html.erb_spec.rb +3 -3
  55. data/spec/views/spotlight/sir_trevor/blocks/_solr_documents_grid_block.html.erb_spec.rb +5 -1
  56. metadata +2 -5
  57. data/app/views/shared/_flash_messages.html.erb +0 -7
  58. data/app/views/spotlight/exhibits/_exhibit_list.html.erb +0 -3
  59. data/db/migrate/20151210073829_create_spotlight_configuration.rb +0 -7
@@ -311,7 +311,9 @@ en:
311
311
  exhibits:
312
312
  breadcrumb: Home
313
313
  index:
314
- private: Private exhibits
314
+ published: Published exhibits
315
+ user: Your exhibits
316
+ unpublished: Unpublished exhibits
315
317
  delete:
316
318
  heading: Delete exhibit
317
319
  warning_html: >
@@ -337,6 +339,7 @@ en:
337
339
  label: URL slug
338
340
  help_block: A hyphenated name to be displayed in the URL for the exhibit (e.g., "maps-of-africa").
339
341
  exhibit_card:
342
+ unpublished: Unpublished
340
343
  visit_exhibit: "Visit exhibit"
341
344
  new:
342
345
  header: Create a new exhibit
@@ -11,17 +11,20 @@ module Spotlight
11
11
  include Spotlight::Catalog::AccessControlsEnforcement
12
12
 
13
13
  included do
14
- before_filter do
15
- if current_exhibit && can?(:curate, current_exhibit)
16
- blacklight_config.add_facet_field 'exhibit_visibility',
17
- label: I18n.t(:'spotlight.catalog.facets.exhibit_visibility.label'),
18
- query: {
19
- private: {
20
- label: I18n.t(:'spotlight.catalog.facets.exhibit_visibility.private'),
21
- fq: "#{Spotlight::SolrDocument.visibility_field(current_exhibit)}:false" }
22
- }
23
- end
24
- end
14
+ before_action :add_facet_visibility_field
15
+ end
16
+
17
+ # Adds a facet to display document visibility for the current exhibit
18
+ # if the user is a curator
19
+ def add_facet_visibility_field
20
+ return unless current_exhibit && can?(:curate, current_exhibit)
21
+ blacklight_config.add_facet_field 'exhibit_visibility',
22
+ label: I18n.t(:'spotlight.catalog.facets.exhibit_visibility.label'),
23
+ query: {
24
+ private: {
25
+ label: I18n.t(:'spotlight.catalog.facets.exhibit_visibility.private'),
26
+ fq: "#{blacklight_config.document_model.visibility_field(current_exhibit)}:false" }
27
+ }
25
28
  end
26
29
  end
27
30
  end
@@ -18,11 +18,12 @@ module Spotlight
18
18
  self.default_processor_chain += [:apply_permissive_visibility_filter, :apply_exhibit_resources_filter]
19
19
  end
20
20
 
21
+ # Adds a filter that excludes resources that have been marked as not-visible
21
22
  def apply_permissive_visibility_filter(solr_params)
22
23
  return unless current_exhibit
23
24
  return if scope.respond_to?(:can?) && scope.can?(:curate, current_exhibit) && !blacklight_params[:public]
24
25
 
25
- solr_params.append_filter_query "-#{Spotlight::SolrDocument.visibility_field(current_exhibit)}:false"
26
+ solr_params.append_filter_query "-#{blacklight_config.document_model.visibility_field(current_exhibit)}:false"
26
27
  end
27
28
 
28
29
  def apply_exhibit_resources_filter(solr_params)
@@ -83,6 +83,11 @@ module Spotlight
83
83
  Spotlight::Engine.config.solr_fields.string_suffix = '_ssim'.freeze
84
84
  Spotlight::Engine.config.solr_fields.text_suffix = '_tesim'.freeze
85
85
 
86
+ # A lambda expression that filters the solr index per exhibit
87
+ config.exhibit_filter = lambda do |exhibit|
88
+ { :"#{config.solr_fields.prefix}spotlight_exhibit_slug_#{exhibit.slug}#{config.solr_fields.boolean_suffix}" => true }
89
+ end
90
+
86
91
  Spotlight::Engine.config.resource_global_id_field = :"#{config.solr_fields.prefix}spotlight_resource_id#{config.solr_fields.string_suffix}"
87
92
 
88
93
  # The solr field that original (largest) images will be stored.
@@ -1,4 +1,4 @@
1
1
  #
2
2
  module Spotlight
3
- VERSION = '0.12.1'
3
+ VERSION = '0.13.0'
4
4
  end
@@ -9,7 +9,7 @@ describe Spotlight::AboutPagesController, type: :controller do
9
9
  describe 'when not logged in' do
10
10
  describe 'POST update_all' do
11
11
  let(:exhibit) { FactoryGirl.create(:exhibit) }
12
- it 'does not be allowed' do
12
+ it 'is not allowed' do
13
13
  post :update_all, exhibit_id: exhibit
14
14
  expect(response).to redirect_to main_app.new_user_session_path
15
15
  end
@@ -8,16 +8,18 @@ describe Spotlight::AppearancesController, type: :controller do
8
8
  sign_in FactoryGirl.create(:exhibit_visitor)
9
9
  end
10
10
 
11
- it 'denies access' do
12
- get :edit, exhibit_id: exhibit
13
- expect(response).to redirect_to main_app.root_path
14
- expect(flash[:alert]).to be_present
11
+ describe 'GET edit' do
12
+ it 'denies access' do
13
+ get :edit, exhibit_id: exhibit
14
+ expect(response).to redirect_to main_app.root_path
15
+ expect(flash[:alert]).to be_present
16
+ end
15
17
  end
16
18
  end
17
19
 
18
20
  describe 'when not logged in' do
19
- describe '#update' do
20
- it 'does not be allowed' do
21
+ describe 'PATCH update' do
22
+ it 'is not allowed' do
21
23
  patch :update, exhibit_id: exhibit
22
24
  expect(response).to redirect_to main_app.new_user_session_path
23
25
  end
@@ -28,7 +30,7 @@ describe Spotlight::AppearancesController, type: :controller do
28
30
  let(:user) { FactoryGirl.create(:exhibit_admin, exhibit: exhibit) }
29
31
  before { sign_in user }
30
32
 
31
- describe '#edit' do
33
+ describe 'GET edit' do
32
34
  it 'is successful' do
33
35
  expect(controller).to receive(:add_breadcrumb).with('Home', exhibit)
34
36
  expect(controller).to receive(:add_breadcrumb).with('Configuration', exhibit_dashboard_path(exhibit))
@@ -39,7 +41,7 @@ describe Spotlight::AppearancesController, type: :controller do
39
41
  end
40
42
  end
41
43
 
42
- describe '#update' do
44
+ describe 'PATCH update' do
43
45
  it 'updates the navigation' do
44
46
  first_nav = exhibit.main_navigations.first
45
47
  last_nav = exhibit.main_navigations.last
@@ -11,7 +11,7 @@ describe Spotlight::BrowseController, type: :controller do
11
11
 
12
12
  describe 'when authenticated as an admin' do
13
13
  before { sign_in admin }
14
- describe '#index' do
14
+ describe 'GET index' do
15
15
  it 'does not show unpublished categories' do
16
16
  expect(controller).to receive(:add_breadcrumb).with('Home', exhibit)
17
17
  expect(controller).to receive(:add_breadcrumb).with('Browse', exhibit_browse_index_path(exhibit))
@@ -26,7 +26,7 @@ describe Spotlight::BrowseController, type: :controller do
26
26
  end
27
27
 
28
28
  describe 'when unauthenticated' do
29
- describe '#index' do
29
+ describe 'GET index' do
30
30
  it 'shows the list of browse categories' do
31
31
  expect(controller).to receive(:add_breadcrumb).with('Home', exhibit)
32
32
  expect(controller).to receive(:add_breadcrumb).with('Browse', exhibit_browse_index_path(exhibit))
@@ -39,7 +39,7 @@ describe Spotlight::BrowseController, type: :controller do
39
39
  end
40
40
  end
41
41
 
42
- describe '#show' do
42
+ describe 'GET show' do
43
43
  let(:mock_response) { double }
44
44
  let(:document_list) { double }
45
45
  before do
@@ -17,7 +17,7 @@ describe Spotlight::CatalogController, type: :controller do
17
17
  end
18
18
 
19
19
  describe 'GET edit' do
20
- it 'does not be allowed' do
20
+ it 'is not allowed' do
21
21
  get :edit, exhibit_id: exhibit, id: 'dq287tq6352'
22
22
  expect(response).to redirect_to main_app.new_user_session_path
23
23
  end
@@ -135,7 +135,7 @@ describe Spotlight::CatalogController, type: :controller do
135
135
  end
136
136
 
137
137
  describe 'GET edit' do
138
- it 'does not be allowed' do
138
+ it 'is not allowed' do
139
139
  get :edit, exhibit_id: exhibit, id: 'dq287tq6352'
140
140
  expect(response).to redirect_to main_app.root_path
141
141
  expect(flash[:alert]).to eq 'You are not authorized to access this page.'
@@ -143,7 +143,7 @@ describe Spotlight::CatalogController, type: :controller do
143
143
  end
144
144
 
145
145
  describe 'GET show with private item' do
146
- it 'does not be allowed' do
146
+ it 'is not allowed' do
147
147
  allow_any_instance_of(::SolrDocument).to receive(:private?).and_return(true)
148
148
  get :show, exhibit_id: exhibit, id: 'dq287tq6352'
149
149
  expect(response).to redirect_to main_app.root_path
@@ -152,7 +152,7 @@ describe Spotlight::CatalogController, type: :controller do
152
152
  end
153
153
 
154
154
  describe 'PUT make_public' do
155
- it 'does not be allowed' do
155
+ it 'is not allowed' do
156
156
  put :make_public, exhibit_id: exhibit, catalog_id: 'dq287tq6352'
157
157
  expect(response).to redirect_to main_app.root_path
158
158
  expect(flash[:alert]).to eq 'You are not authorized to access this page.'
@@ -160,7 +160,7 @@ describe Spotlight::CatalogController, type: :controller do
160
160
  end
161
161
 
162
162
  describe 'DELETE make_private' do
163
- it 'does not be allowed' do
163
+ it 'is not allowed' do
164
164
  delete :make_private, exhibit_id: exhibit, catalog_id: 'dq287tq6352'
165
165
  expect(response).to redirect_to main_app.root_path
166
166
  expect(flash[:alert]).to eq 'You are not authorized to access this page.'
@@ -249,14 +249,14 @@ describe Spotlight::CatalogController, type: :controller do
249
249
  describe 'when the user is a site admin' do
250
250
  before { sign_in FactoryGirl.create(:site_admin, exhibit: exhibit) }
251
251
 
252
- describe 'show' do
252
+ describe 'GET show' do
253
253
  it 'has a solr_json serialization' do
254
254
  get :show, exhibit_id: exhibit, id: 'dq287tq6352', format: :solr_json
255
255
  expect(response).to be_successful
256
256
  data = JSON.parse(response.body).with_indifferent_access
257
257
  expect(data).to include id: 'dq287tq6352'
258
258
  expect(data).to include exhibit.solr_data
259
- expect(data).to include Spotlight::SolrDocument.solr_field_for_tagger(exhibit)
259
+ expect(data).to include ::SolrDocument.solr_field_for_tagger(exhibit)
260
260
  end
261
261
  end
262
262
  end
@@ -292,4 +292,85 @@ describe Spotlight::CatalogController, type: :controller do
292
292
  end
293
293
  end
294
294
  end
295
+
296
+ describe 'next and previous documents' do
297
+ before do
298
+ exhibit.searches.first.update(published: true)
299
+ allow(controller).to receive(:current_search_session).and_return(search)
300
+ allow(controller).to receive(:search_session).and_return(search_session)
301
+ end
302
+
303
+ let(:search_session) { { 'counter' => 2 } }
304
+
305
+ let(:response) { double(total: 5, documents: [first_doc, nil, last_doc]) }
306
+ let(:first_doc) { double }
307
+ let(:last_doc) { double }
308
+
309
+ context 'when arriving from a browse page' do
310
+ let(:search) do
311
+ Search.new(query_params: { action: 'show', controller: 'spotlight/browse', id: exhibit.searches.first.id }.with_indifferent_access)
312
+ end
313
+
314
+ context 'when published' do
315
+ before do
316
+ exhibit.searches.first.update(published: true)
317
+ allow(controller).to receive(:get_previous_and_next_documents_for_search).with(1, exhibit.searches.first.query_params).and_return(response)
318
+ end
319
+
320
+ it 'uses the saved search context' do
321
+ get :show, exhibit_id: exhibit, id: 'dq287tq6352'
322
+
323
+ expect(assigns(:previous_document)).to eq first_doc
324
+ expect(assigns(:next_document)).to eq last_doc
325
+ end
326
+ end
327
+
328
+ context 'when arriving from a private browse page' do
329
+ before do
330
+ exhibit.searches.first.update(published: false)
331
+ end
332
+
333
+ it 'ignores the search context' do
334
+ get :show, exhibit_id: exhibit, id: 'dq287tq6352'
335
+
336
+ expect(assigns(:previous_document)).to be_nil
337
+ expect(assigns(:next_document)).to be_nil
338
+ end
339
+ end
340
+ end
341
+
342
+ context 'when arriving from a feature page' do
343
+ let(:page) { FactoryGirl.create(:feature_page, exhibit: exhibit) }
344
+ let(:search) do
345
+ Search.new(query_params: { action: 'show', controller: 'spotlight/feature_pages', id: page.id }.with_indifferent_access)
346
+ end
347
+
348
+ context 'when published' do
349
+ before do
350
+ page.update(published: true)
351
+ end
352
+
353
+ it 'uses the page context' do
354
+ pending 'Waiting to figure out how to construct previous/next documents'
355
+ get :show, exhibit_id: exhibit, id: 'dq287tq6352'
356
+
357
+ expect(assigns(:previous_document)).to be_a_kind_of SolrDocument
358
+ expect(assigns(:next_document)).to be_a_kind_of SolrDocument
359
+ end
360
+ end
361
+
362
+ context 'when unpublished' do
363
+ before do
364
+ page.update(published: false)
365
+ end
366
+
367
+ it 'ignores the search context' do
368
+ get :show, exhibit_id: exhibit, id: 'dq287tq6352'
369
+
370
+ expect(assigns(:previous_document)).to be_nil
371
+ expect(assigns(:next_document)).to be_nil
372
+ end
373
+ end
374
+ end
375
+ end
295
376
  end
@@ -7,12 +7,15 @@ describe Spotlight::ConfirmationsController, type: :controller do
7
7
  @request.env['devise.mapping'] = Devise.mappings[:contact_email]
8
8
  # rubocop:enable RSpec/InstanceVariable
9
9
  end
10
- it 'has new' do
11
- get :new
12
- expect(response).to be_successful
10
+
11
+ describe 'GET new' do
12
+ it 'exists' do
13
+ get :new
14
+ expect(response).to be_successful
15
+ end
13
16
  end
14
17
 
15
- describe '#show' do
18
+ describe 'GET show' do
16
19
  let(:exhibit) { FactoryGirl.create(:exhibit) }
17
20
  let(:contact_email) { Spotlight::ContactEmail.create!(email: 'justin@example.com', exhibit: exhibit) }
18
21
  let(:raw_token) { contact_email.instance_variable_get(:@raw_confirmation_token) }
@@ -15,7 +15,7 @@ describe Spotlight::ContactFormsController, type: :controller do
15
15
  end
16
16
  end
17
17
  end
18
- describe '#create' do
18
+ describe 'POST create' do
19
19
  it 'redirects back' do
20
20
  post :create, exhibit_id: exhibit.id, contact_form: { name: 'Joe Doe', email: 'jdoe@example.com' }
21
21
  expect(response).to redirect_to :back
@@ -13,51 +13,53 @@ describe Spotlight::ExhibitsController, type: :controller do
13
13
  sign_in FactoryGirl.create(:exhibit_visitor)
14
14
  end
15
15
 
16
- it 'denies access' do
17
- get :edit, id: exhibit
18
- expect(response).to redirect_to main_app.root_path
19
- expect(flash[:alert]).to be_present
16
+ describe 'GET edit' do
17
+ it 'denies access' do
18
+ get :edit, id: exhibit
19
+ expect(response).to redirect_to main_app.root_path
20
+ expect(flash[:alert]).to be_present
21
+ end
20
22
  end
21
23
  end
22
24
 
23
25
  describe 'when not logged in' do
24
- describe '#index' do
26
+ describe 'GET index' do
25
27
  it 'is allowed' do
26
28
  get :index
27
29
  expect(response).to be_success
28
30
  end
29
31
  end
30
32
 
31
- describe '#new' do
32
- it 'does not be allowed' do
33
+ describe 'GET new' do
34
+ it 'is not allowed' do
33
35
  get :new, id: exhibit
34
36
  expect(response).to redirect_to main_app.new_user_session_path
35
37
  end
36
38
  end
37
39
 
38
- describe '#edit' do
39
- it 'does not be allowed' do
40
+ describe 'GET edit' do
41
+ it 'is not allowed' do
40
42
  get :edit, id: exhibit
41
43
  expect(response).to redirect_to main_app.new_user_session_path
42
44
  end
43
45
  end
44
46
 
45
- describe '#update' do
46
- it 'does not be allowed' do
47
+ describe 'PATCH update' do
48
+ it 'is not allowed' do
47
49
  patch :update, id: exhibit
48
50
  expect(response).to redirect_to main_app.new_user_session_path
49
51
  end
50
52
  end
51
53
 
52
- describe '#process_import' do
53
- it 'does not be allowed' do
54
+ describe 'PATCH process_import' do
55
+ it 'is not allowed' do
54
56
  patch :process_import, id: exhibit
55
57
  expect(response).to redirect_to main_app.new_user_session_path
56
58
  end
57
59
  end
58
60
 
59
- describe '#destroy' do
60
- it 'does not be allowed' do
61
+ describe 'DELETE destroy' do
62
+ it 'is not allowed' do
61
63
  delete :destroy, id: exhibit
62
64
  expect(response).to redirect_to main_app.new_user_session_path
63
65
  end
@@ -68,14 +70,14 @@ describe Spotlight::ExhibitsController, type: :controller do
68
70
  let(:user) { FactoryGirl.create(:site_admin) }
69
71
  before { sign_in user }
70
72
 
71
- describe '#new' do
73
+ describe 'GET new' do
72
74
  it 'is successful' do
73
75
  get :new
74
76
  expect(response).to be_successful
75
77
  end
76
78
  end
77
79
 
78
- describe '#create' do
80
+ describe 'POST create' do
79
81
  before do
80
82
  # decouple this test from needing solr running
81
83
  allow_any_instance_of(Spotlight::Search).to receive(:set_default_featured_image)
@@ -91,6 +93,8 @@ describe Spotlight::ExhibitsController, type: :controller do
91
93
 
92
94
  expect(exhibit.title).to eq 'Some Title'
93
95
  expect(exhibit.slug).to eq 'custom-slug'
96
+
97
+ expect(user.exhibits).to include exhibit
94
98
  end
95
99
  end
96
100
  end
@@ -99,14 +103,14 @@ describe Spotlight::ExhibitsController, type: :controller do
99
103
  let(:user) { FactoryGirl.create(:exhibit_admin, exhibit: exhibit) }
100
104
  before { sign_in user }
101
105
 
102
- describe '#new' do
103
- it 'does not be allowed' do
106
+ describe 'GET new' do
107
+ it 'is not allowed' do
104
108
  get :new
105
109
  expect(response).to_not be_successful
106
110
  end
107
111
  end
108
112
 
109
- describe '#process_import' do
113
+ describe 'PATCH process_import' do
110
114
  it 'is successful' do
111
115
  expect_any_instance_of(Spotlight::Exhibit).to receive(:reindex_later).and_return(true)
112
116
  f = Tempfile.new('foo')
@@ -127,7 +131,7 @@ describe Spotlight::ExhibitsController, type: :controller do
127
131
  end
128
132
  end
129
133
 
130
- describe '#edit' do
134
+ describe 'GET edit' do
131
135
  it 'is successful' do
132
136
  expect(controller).to receive(:add_breadcrumb).with('Home', exhibit)
133
137
  expect(controller).to receive(:add_breadcrumb).with('Configuration', exhibit_dashboard_path(exhibit))