app/controllers/concerns/hyrax/works_controller_behavior.rb
# frozen_string_literal: true
# Hyrax 5.0.1 override, line 495, method `#permissions_changed?`
require 'iiif_manifest'
module Hyrax
module WorksControllerBehavior
extend ActiveSupport::Concern
include Blacklight::Base
include Blacklight::AccessControls::Catalog
included do
with_themed_layout :decide_layout
copy_blacklight_config_from(::CatalogController)
before_action do
blacklight_config.track_search_session = false
blacklight_config.search_builder_class = search_builder_class
end
class_attribute :_curation_concern_type, :show_presenter, :work_form_service, :search_builder_class
class_attribute :iiif_manifest_builder, instance_accessor: false
class_attribute :create_valkyrie_work_action
self.show_presenter = Hyrax::WorkShowPresenter
self.work_form_service = Hyrax::WorkFormService
self.search_builder_class = WorkSearchBuilder
self.create_valkyrie_work_action = Hyrax::Action::CreateValkyrieWork
self.iiif_manifest_builder = nil
attr_accessor :curation_concern
helper_method :curation_concern, :contextual_path
rescue_from WorkflowAuthorizationException, with: :render_unavailable
end
class_methods do
def curation_concern_type=(curation_concern_type)
load_and_authorize_resource class: curation_concern_type, instance_name: :curation_concern, except: [:show, :file_manager, :inspect_work, :manifest]
# Load the fedora resource to get the etag.
# No need to authorize for the file manager, because it does authorization via the presenter.
load_resource class: curation_concern_type, instance_name: :curation_concern, only: :file_manager
self._curation_concern_type = curation_concern_type
# We don't want the breadcrumb action to occur until after the concern has
# been loaded and authorized
before_action :save_permissions, only: :update
end
def curation_concern_type
_curation_concern_type
end
def cancan_resource_class
Hyrax::ControllerResource
end
end
def new
@admin_set_options = available_admin_sets
# TODO: move these lines to the work form builder in Hyrax
curation_concern.depositor = current_user.user_key
curation_concern.admin_set_id = admin_set_id_for_new
build_form
end
def create
case curation_concern
when ActiveFedora::Base
original_input_params_for_form = params[hash_key_for_curation_concern].deep_dup
actor.create(actor_environment) ? after_create_response : after_create_error(curation_concern.errors, original_input_params_for_form)
else
create_valkyrie_work
end
end
# Finds a solr document matching the id and sets @presenter
# @raise CanCan::AccessDenied if the document is not found or the user doesn't have access to it.
def show
@user_collections = user_collections
respond_to do |wants|
wants.html { presenter && parent_presenter }
wants.json do
# load @curation_concern manually because it's skipped for html
@curation_concern = load_curation_concern
curation_concern # This is here for authorization checks (we could add authorize! but let's use the same method for CanCanCan)
render :show, status: :ok
end
additional_response_formats(wants)
end
end
def edit
@admin_set_options = available_admin_sets
build_form
end
def update
case curation_concern
when ActiveFedora::Base
actor.update(actor_environment) ? after_update_response : after_update_error(curation_concern.errors)
else
update_valkyrie_work
end
end
def destroy
case curation_concern
when ActiveFedora::Base
title = curation_concern.to_s
env = Actors::Environment.new(curation_concern, current_ability, {})
return unless actor.destroy(env)
Hyrax.config.callback.run(:after_destroy, curation_concern.id, current_user, warn: false)
else
transactions['work_resource.destroy']
.with_step_args('work_resource.delete' => { user: current_user },
'work_resource.delete_all_file_sets' => { user: current_user })
.call(curation_concern).value!
title = Array(curation_concern.title).first
end
after_destroy_response(title)
end
def file_manager
@form = presenter
end
def inspect_work
raise Hydra::AccessDenied unless current_ability.admin?
presenter
end
def manifest
headers['Access-Control-Allow-Origin'] = '*'
json = iiif_manifest_builder.manifest_for(presenter: iiif_manifest_presenter)
respond_to do |wants|
wants.any { render json: json }
end
end
private
def load_curation_concern
if Hyrax.config.disable_wings
Hyrax.query_service.find_by(id: params[:id])
else
Hyrax.query_service.find_by_alternate_identifier(alternate_identifier: params[:id])
end
end
def iiif_manifest_builder
self.class.iiif_manifest_builder ||
(Flipflop.cache_work_iiif_manifest? ? Hyrax::CachingIiifManifestBuilder.new : Hyrax::ManifestBuilderService.new)
end
def iiif_manifest_presenter
IiifManifestPresenter.new(search_result_document(id: params[:id])).tap do |p|
p.hostname = request.base_url
p.ability = current_ability
end
end
def user_collections
collections_service.search_results(:deposit)
end
def collections_service
Hyrax::CollectionsService.new(self)
end
def admin_set_id_for_new
Hyrax::AdminSetCreateService.find_or_create_default_admin_set.id.to_s
end
def build_form
@form = work_form_service.build(curation_concern, current_ability, self)
end
def actor
@actor ||= Hyrax::CurationConcern.actor
end
##
# @return [#errors]
# rubocop:disable Metrics/MethodLength
def create_valkyrie_work
form = build_form
action = create_valkyrie_work_action.new(form: form,
transactions: transactions,
user: current_user,
params: params,
work_attributes_key: hash_key_for_curation_concern)
return after_create_error(form_err_msg(action.form), action.work_attributes) unless action.validate
result = action.perform
@curation_concern = result.value_or { return after_create_error(transaction_err_msg(result)) }
after_create_response
end
# rubocop:enable Metrics/MethodLength
def update_valkyrie_work
form = build_form
return after_update_error(form_err_msg(form)) unless form.validate(params[hash_key_for_curation_concern])
result =
transactions['change_set.update_work']
.with_step_args('work_resource.add_file_sets' => { uploaded_files: uploaded_files, file_set_params: params[hash_key_for_curation_concern][:file_set] },
'work_resource.update_work_members' => { work_members_attributes: work_members_attributes },
'work_resource.save_acl' => { permissions_params: form.input_params["permissions"] })
.call(form)
@curation_concern = result.value_or { return after_update_error(transaction_err_msg(result)) }
after_update_response
end
def work_members_attributes
params[hash_key_for_curation_concern][:work_members_attributes]&.permit!&.to_h
end
def form_err_msg(form)
form.errors.messages.values.flatten.to_sentence
end
def transaction_err_msg(result)
msg = if result.failure[1].respond_to?(:full_messages)
"#{result.failure[1].full_messages.to_sentence} [#{result.failure[0]}]"
else
result.failure[0].to_s
end
Rails.logger.info("Transaction failed: #{msg}\n #{result.trace}")
msg
end
def presenter
@presenter ||= show_presenter.new(search_result_document(id: params[:id]), current_ability, request)
end
def parent_presenter
return @parent_presenter unless params[:parent_id]
@parent_presenter ||=
show_presenter.new(search_result_document(id: params[:parent_id]), current_ability, request)
end
# Include 'hyrax/base' in the search path for views, while prefering
# our local paths. Thus we are unable to just override `self.local_prefixes`
def _prefixes
@_prefixes ||= super + ['hyrax/base']
end
def actor_environment
Actors::Environment.new(curation_concern, current_ability, attributes_for_actor)
end
def hash_key_for_curation_concern
_curation_concern_type.model_name.param_key
end
def contextual_path(presenter, parent_presenter)
::Hyrax::ContextualPath.new(presenter, parent_presenter).show
end
##
# Only returns unsuppressed documents the user has read access to
#
# @api public
#
# @param search_params [ActionController::Parameters] this should
# include an :id key, but based on implementation and use of the
# WorkSearchBuilder, it need not.
#
# @return [SolrDocument]
#
# @raise [WorkflowAuthorizationException] when the object is not
# found via the search builder's search logic BUT the object is
# suppressed AND the user can read it (Yeah, it's confusing but
# after a lot of debugging that's the logic)
#
# @raise [CanCan::AccessDenied] when the object is not found via
# the search builder's search logic BUT the object is not
# supressed OR not readable by the user (Yeah.)
#
# @note This is Jeremy, I have suspicions about the first line of
# this comment (eg, "Only return unsuppressed..."). The
# reason is that I've encounter situations in the specs
# where the document_list is empty but if I then query Solr
# for the object by ID, I get a document that is NOT
# suppressed AND can be read. In other words, I believe
# there is more going on in the search_results method
# (e.g. a filter is being applied that is beyond what the
# comment indicates)
#
# @see #document_not_found!
def search_result_document(search_params)
_, document_list = search_results(search_params)
return document_list.first unless document_list.empty?
document_not_found!
end
def document_not_found!
doc = ::SolrDocument.find(params[:id])
raise WorkflowAuthorizationException if doc.suppressed? && current_ability.can?(:read, doc)
raise CanCan::AccessDenied.new(nil, :show)
end
def render_unavailable
message = I18n.t("hyrax.workflow.unauthorized")
respond_to do |wants|
wants.html do
unavailable_presenter
flash[:notice] = message
render 'unavailable', status: :unauthorized
end
wants.json { render plain: message, status: :unauthorized }
additional_response_formats(wants)
wants.ttl { render plain: message, status: :unauthorized }
wants.jsonld { render plain: message, status: :unauthorized }
wants.nt { render plain: message, status: :unauthorized }
end
end
def unavailable_presenter
@presenter ||= show_presenter.new(::SolrDocument.find(params[:id]), current_ability, request)
end
def decide_layout
layout = case action_name
when 'show'
'1_column'
else
'dashboard'
end
File.join(theme, layout)
end
##
# @todo should the controller know so much about browse_everything?
# hopefully this can be refactored to be more reusable.
#
# Add uploaded_files to the parameters received by the actor.
def attributes_for_actor # rubocop:disable Metrics/MethodLength
raw_params = params[hash_key_for_curation_concern]
attributes = if raw_params
work_form_service.form_class(curation_concern).model_attributes(raw_params)
else
{}
end
# If they selected a BrowseEverything file, but then clicked the
# remove button, it will still show up in `selected_files`, but
# it will no longer be in uploaded_files. By checking the
# intersection, we get the files they added via BrowseEverything
# that they have not removed from the upload widget.
uploaded_files = params.fetch(:uploaded_files, [])
selected_files = params.fetch(:selected_files, {}).values
browse_everything_urls = uploaded_files &
selected_files.map { |f| f[:url] }
# we need the hash of files with url and file_name
browse_everything_files = selected_files
.select { |v| uploaded_files.include?(v[:url]) }
attributes[:remote_files] = browse_everything_files
# Strip out any BrowseEverthing files from the regular uploads.
attributes[:uploaded_files] = uploaded_files -
browse_everything_urls
attributes
end
def after_create_response
respond_to do |wants|
wants.html do
# Calling `#t` in a controller context does not mark _html keys as html_safe
flash[:notice] = view_context.t('hyrax.works.create.after_create_html', application_name: view_context.application_name)
redirect_to [main_app, curation_concern]
end
wants.json { render :show, status: :created, location: polymorphic_path([main_app, curation_concern]) }
end
end
def format_error_messages(errors)
# the error may already be a string
errors.respond_to?(:messages) ? errors.messages.values.flatten.join("\n") : errors
end
def after_create_error(errors, original_input_params_for_form = nil)
respond_to do |wants|
wants.html do
flash[:error] = format_error_messages(errors)
rebuild_form(original_input_params_for_form) if original_input_params_for_form.present?
render 'new', status: :unprocessable_entity
end
wants.json { render_json_response(response_type: :unprocessable_entity, options: { errors: errors }) }
end
end
# Creating a form object that can re-render most of the submitted parameters.
# Required for ActiveFedora::Base objects only.
def rebuild_form(original_input_params_for_form)
build_form
@form = Hyrax::Forms::FailedSubmissionFormWrapper
.new(form: @form,
input_params: original_input_params_for_form)
end
def after_update_response
return redirect_to hyrax.confirm_access_permission_path(curation_concern) if permissions_changed? && concern_has_file_sets?
respond_to do |wants|
wants.html { redirect_to [main_app, curation_concern], notice: "Work \"#{curation_concern}\" successfully updated." }
wants.json { render :show, status: :ok, location: polymorphic_path([main_app, curation_concern]) }
end
end
def after_update_error(errors)
respond_to do |wants|
wants.html do
flash[:error] = format_error_messages(errors)
build_form unless @form.is_a? Hyrax::ChangeSet
render 'edit', status: :unprocessable_entity
end
wants.json { render_json_response(response_type: :unprocessable_entity, options: { errors: errors }) }
end
end
def after_destroy_response(title)
respond_to do |wants|
wants.html { redirect_to hyrax.my_works_path, notice: "Deleted #{title}" }
wants.json { render_json_response(response_type: :deleted, message: "Deleted #{curation_concern.id}") }
end
end
def additional_response_formats(format)
respond_to_endnote(format)
respond_to_ttl(format)
respond_to_jsonld(format)
respond_to_nt(format)
end
def respond_to_endnote(format)
format.endnote do
send_data(presenter.solr_document.export_as_endnote,
type: "application/x-endnote-refer",
filename: presenter.solr_document.endnote_filename)
end
end
def respond_to_ttl(format)
format.ttl do
if presenter.valkyrie_presenter?
render plain: "Error: Not Implemented", status: :not_implemented
else
render body: presenter.export_as_ttl, mime_type: Mime[:ttl]
end
end
end
def respond_to_jsonld(format)
format.jsonld do
if presenter.valkyrie_presenter?
render plain: "Error: Not Implemented", status: :not_implemented
else
render body: presenter.export_as_jsonld, mime_type: Mime[:jsonld]
end
end
end
def respond_to_nt(format)
format.nt do
if presenter.valkyrie_presenter?
render plain: "Error: Not Implemented", status: :not_implemented
else
render body: presenter.export_as_nt, mime_type: Mime[:nt]
end
end
end
def save_permissions
@saved_permissions =
case curation_concern
when ActiveFedora::Base
curation_concern.permissions.map(&:to_hash)
else
Hyrax::AccessControl.for(resource: curation_concern).permissions
end
end
def permissions_changed?
# Hyrax 5.0.1 override
# Sometimes the order of permissions is not the same between saved permissions and new permissions for Valkyrie
# I am replacing array comparison (which relies on order) with comparing array sizes and checking for existence of elements in both arrays
case curation_concern
when ActiveFedora::Base
@saved_permissions != curation_concern.permissions.map(&:to_hash)
else
new_permissions = Hyrax::AccessControl.for(resource: curation_concern).permissions
saved_permissions_set = @saved_permissions.to_set
new_permissions.size != @saved_permissions.size || new_permissions.any? { |e| !saved_permissions_set.include? e }
end
end
def concern_has_file_sets?
case curation_concern
when ActiveFedora::Common
curation_concern.file_sets.present?
else
Hyrax.custom_queries.find_child_file_set_ids(resource: curation_concern).any?
end
end
def uploaded_files
UploadedFile.find(params.fetch(:uploaded_files, []))
end
def available_admin_sets
# only returns admin sets in which the user can deposit
admin_set_results = Hyrax::AdminSetService.new(self).search_results(:deposit)
# get all the templates at once, reducing query load
templates = PermissionTemplate.where(source_id: admin_set_results.map(&:id)).to_a
admin_sets = admin_set_results.map do |admin_set_doc|
template = templates.find { |temp| temp.source_id == admin_set_doc.id.to_s }
# determine if sharing tab should be visible
sharing = can?(:manage, template) || !!template&.active_workflow&.allows_access_grant?
AdminSetSelectionPresenter::OptionsEntry
.new(admin_set: admin_set_doc, permission_template: template, permit_sharing: sharing)
end
AdminSetSelectionPresenter.new(admin_sets: admin_sets)
end
end
end