From 76bc00bea0a37f58f64fcd1bf23ec2afd0c58b09 Mon Sep 17 00:00:00 2001 From: Chris Colvard Date: Fri, 15 Jul 2022 14:51:39 -0400 Subject: [PATCH 001/230] Mount volume where solr 8's docker container stores data --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index af47c2ffe3..94b383f6ed 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -45,7 +45,7 @@ services: image: solr:8-slim volumes: - ./solr/conf:/opt/solr/avalon_conf - - solr:/opt/solr/server/solr/mycores + - solr:/var/solr command: - solr-precreate - avalon From c28800a3782e39c1d8424a3ef0c52371d9a61b7a Mon Sep 17 00:00:00 2001 From: Chris Colvard Date: Thu, 26 May 2022 14:13:04 -0500 Subject: [PATCH 002/230] Checkout model, controller, and views Lending status added to media object json api --- app/controllers/checkouts_controller.rb | 61 +++++++++ app/models/ability.rb | 19 ++- app/models/checkout.rb | 11 ++ app/models/media_object.rb | 7 +- app/views/checkouts/_checkout.json.jbuilder | 2 + app/views/checkouts/index.html.erb | 29 +++++ app/views/checkouts/index.json.jbuilder | 1 + app/views/checkouts/show.json.jbuilder | 1 + config/routes.rb | 2 + db/migrate/20220526185425_create_checkouts.rb | 12 ++ spec/factories/checkouts.rb | 23 ++++ spec/models/checkout_spec.rb | 64 +++++++++ spec/models/media_object_spec.rb | 14 ++ spec/rails_helper.rb | 1 + spec/requests/checkouts_spec.rb | 121 ++++++++++++++++++ spec/routing/checkouts_routing_spec.rb | 29 +++++ spec/views/checkouts/index.html.erb_spec.rb | 16 +++ 17 files changed, 410 insertions(+), 3 deletions(-) create mode 100644 app/controllers/checkouts_controller.rb create mode 100644 app/models/checkout.rb create mode 100644 app/views/checkouts/_checkout.json.jbuilder create mode 100644 app/views/checkouts/index.html.erb create mode 100644 app/views/checkouts/index.json.jbuilder create mode 100644 app/views/checkouts/show.json.jbuilder create mode 100644 db/migrate/20220526185425_create_checkouts.rb create mode 100644 spec/factories/checkouts.rb create mode 100644 spec/models/checkout_spec.rb create mode 100644 spec/requests/checkouts_spec.rb create mode 100644 spec/routing/checkouts_routing_spec.rb create mode 100644 spec/views/checkouts/index.html.erb_spec.rb diff --git a/app/controllers/checkouts_controller.rb b/app/controllers/checkouts_controller.rb new file mode 100644 index 0000000000..c476d8d9cd --- /dev/null +++ b/app/controllers/checkouts_controller.rb @@ -0,0 +1,61 @@ +class CheckoutsController < ApplicationController + before_action :set_checkout, only: %i[show update destroy] + load_and_authorize_resource except: [:create] + + # GET /checkouts or /checkouts.json + def index + @checkouts = Checkout.all + end + + # GET /checkouts/1.json + def show + end + + # POST /checkouts.json + def create + @checkout = Checkout.new(user: current_user, media_object_id: checkout_params[:media_object_id], checkout_time: DateTime.current, return_time: DateTime.current + 2.weeks) + + respond_to do |format| + # TODO: move this can? check into a checkout ability (can?(:create, @checkout)) + if can?(:create, @checkout) && @checkout.save + format.json { render :show, status: :created, location: @checkout } + else + format.json { render json: @checkout.errors, status: :unprocessable_entity } + end + end + end + + # PATCH/PUT /checkouts/1.json + def update + respond_to do |format| + if @checkout.update(checkout_params.slice(:return_time)) + # TODO: Change this since it will be called from the media object show page + format.json { render :show, status: :ok, location: @checkout } + else + format.json { render json: @checkout.errors, status: :unprocessable_entity } + end + end + end + + # DELETE /checkouts/1 or /checkouts/1.json + def destroy + @checkout.destroy + + respond_to do |format| + format.html { redirect_to checkouts_url, notice: "Checkout was successfully destroyed." } + format.json { head :no_content } + end + end + + private + + # Use callbacks to share common setup or constraints between actions. + def set_checkout + @checkout = Checkout.find(params[:id]) + end + + # Only allow a list of trusted parameters through. + def checkout_params + params.require(:checkout).permit(:media_object_id, :return_time) + end +end diff --git a/app/models/ability.rb b/app/models/ability.rb index 9262659028..01680f803f 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -17,8 +17,12 @@ class Ability include Hydra::Ability include Hydra::MultiplePolicyAwareAbility - self.ability_logic += [ :playlist_permissions, :playlist_item_permissions, :marker_permissions, :encode_dashboard_permissions ] - self.ability_logic += [ :timeline_permissions ] + self.ability_logic += [:playlist_permissions, + :playlist_item_permissions, + :marker_permissions, + :encode_dashboard_permissions, + :timeline_permissions, + :checkout_permissions] def encode_dashboard_permissions can :read, :encode_dashboard if is_administrator? @@ -217,6 +221,17 @@ def timeline_permissions end end + def checkout_permissions + if @user.id.present? + can :create, Checkout do |checkout| + checkout.user == @user && can?(:read, checkout.media_object) + end + can :read, Checkout, user: @user + can :update, Checkout, user: @user + can :destroy, Checkout, user: @user + end + end + def is_administrator? @user_groups.include?("administrator") end diff --git a/app/models/checkout.rb b/app/models/checkout.rb new file mode 100644 index 0000000000..4d81fc7942 --- /dev/null +++ b/app/models/checkout.rb @@ -0,0 +1,11 @@ +class Checkout < ApplicationRecord + belongs_to :user + + validates :user, :media_object_id, :checkout_time, :return_time, presence: true + + scope :active_for, ->(media_object_id) { where(media_object_id: media_object_id).where("return_time > now()") } + + def media_object + MediaObject.find(media_object_id) + end +end diff --git a/app/models/media_object.rb b/app/models/media_object.rb index 437438826e..368a076cf2 100644 --- a/app/models/media_object.rb +++ b/app/models/media_object.rb @@ -279,7 +279,8 @@ def as_json(options={}) published: published?, summary: abstract, visibility: visibility, - read_groups: read_groups + read_groups: read_groups, + lending_status: lending_status, }.merge(to_ingest_api_hash(options.fetch(:include_structure, false))) end @@ -366,6 +367,10 @@ def access_text "This item is accessible by: #{actors.join(', ')}." end + def lending_status + Checkout.active_for(id).any? ? "checked_out" : "available" + end + private def calculate_duration diff --git a/app/views/checkouts/_checkout.json.jbuilder b/app/views/checkouts/_checkout.json.jbuilder new file mode 100644 index 0000000000..3378265676 --- /dev/null +++ b/app/views/checkouts/_checkout.json.jbuilder @@ -0,0 +1,2 @@ +json.extract! checkout, :id, :user_id, :media_object_id, :checkout_time, :return_time, :created_at, :updated_at +json.url checkout_url(checkout, format: :json) diff --git a/app/views/checkouts/index.html.erb b/app/views/checkouts/index.html.erb new file mode 100644 index 0000000000..0fabb6d2c9 --- /dev/null +++ b/app/views/checkouts/index.html.erb @@ -0,0 +1,29 @@ +

<%= notice %>

+ +

Checkouts

+ + + + + + + + + + + + + + <% @checkouts.each do |checkout| %> + + + + + + + + <% end %> + +
UserMedia objectCheckout timeReturn time
<%= checkout.user.user_key %><%= checkout.media_object.title %><%= checkout.checkout_time %><%= checkout.return_time %><%= link_to 'Destroy', checkout, method: :delete, data: { confirm: 'Are you sure?' } %>
+ +
diff --git a/app/views/checkouts/index.json.jbuilder b/app/views/checkouts/index.json.jbuilder new file mode 100644 index 0000000000..bad22b98fa --- /dev/null +++ b/app/views/checkouts/index.json.jbuilder @@ -0,0 +1 @@ +json.array! @checkouts, partial: "checkouts/checkout", as: :checkout diff --git a/app/views/checkouts/show.json.jbuilder b/app/views/checkouts/show.json.jbuilder new file mode 100644 index 0000000000..bad25d638f --- /dev/null +++ b/app/views/checkouts/show.json.jbuilder @@ -0,0 +1 @@ +json.partial! "checkouts/checkout", checkout: @checkout diff --git a/config/routes.rb b/config/routes.rb index e5ed6d1102..f956f8870b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -26,6 +26,8 @@ end end + resources :checkouts, only: [:index, :create, :show, :update, :destroy] + resources :bookmarks do concerns :exportable diff --git a/db/migrate/20220526185425_create_checkouts.rb b/db/migrate/20220526185425_create_checkouts.rb new file mode 100644 index 0000000000..01ac8c2e4c --- /dev/null +++ b/db/migrate/20220526185425_create_checkouts.rb @@ -0,0 +1,12 @@ +class CreateCheckouts < ActiveRecord::Migration[6.0] + def change + create_table :checkouts do |t| + t.references :user, foreign_key: true + t.string :media_object_id + t.datetime :checkout_time + t.datetime :return_time + + t.timestamps + end + end +end diff --git a/spec/factories/checkouts.rb b/spec/factories/checkouts.rb new file mode 100644 index 0000000000..c18deb9713 --- /dev/null +++ b/spec/factories/checkouts.rb @@ -0,0 +1,23 @@ +# Copyright 2011-2022, The Trustees of Indiana University and Northwestern +# University. Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed +# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +# CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. +# --- END LICENSE_HEADER BLOCK --- + +FactoryBot.define do + factory :checkout do + user { FactoryBot.create(:user) } + media_object_id { FactoryBot.create(:published_media_object, visibility: 'public').id } + checkout_time { DateTime.now } + return_time { DateTime.now + 2.weeks } + end +end + diff --git a/spec/models/checkout_spec.rb b/spec/models/checkout_spec.rb new file mode 100644 index 0000000000..4e8c1e5047 --- /dev/null +++ b/spec/models/checkout_spec.rb @@ -0,0 +1,64 @@ +require 'rails_helper' +require 'cancan/matchers' + +RSpec.describe Checkout, type: :model do + let(:checkout) { FactoryBot.create(:checkout) } + + describe 'validations' do + it { is_expected.to validate_presence_of(:user) } + it { is_expected.to validate_presence_of(:media_object_id) } + it { is_expected.to validate_presence_of(:checkout_time) } + it { is_expected.to validate_presence_of(:return_time) } + end + + describe 'abilities' do + let(:user) { checkout.user } + subject(:ability) { Ability.new(user, {}) } + + it { is_expected.to be_able_to(:create, checkout) } + it { is_expected.to be_able_to(:read, checkout) } + it { is_expected.to be_able_to(:update, checkout) } + it { is_expected.to be_able_to(:destroy, checkout) } + + context 'when unable to read media object' do + let(:private_media_object) { FactoryBot.create(:media_object) } + let(:checkout) { FactoryBot.create(:checkout, media_object_id: private_media_object.id) } + + it { is_expected.not_to be_able_to(:create, checkout) } + end + + context 'when checkout is owned by a different user' do + let(:user) { FactoryBot.create(:user) } + + it { is_expected.not_to be_able_to(:create, checkout) } + it { is_expected.not_to be_able_to(:read, checkout) } + it { is_expected.not_to be_able_to(:update, checkout) } + it { is_expected.not_to be_able_to(:destroy, checkout) } + end + end + + describe 'scopes' do + let(:media_object) { FactoryBot.create(:media_object) } + let!(:checkout) { FactoryBot.create(:checkout, media_object_id: media_object.id) } + let!(:expired_checkout) { FactoryBot.create(:checkout, media_object_id: media_object.id, checkout_time: DateTime.now - 2.weeks, return_time: DateTime.now - 1.day) } + + describe 'active_for' do + it 'returns active checkouts for the given media object' do + expect(Checkout.active_for(media_object.id)).to include(checkout) + end + + it 'does not return inactive checkouts' do + expect(Checkout.active_for(media_object.id)).not_to include(expired_checkout) + end + end + end + + describe 'media_object' do + let(:media_object) { FactoryBot.create(:media_object) } + let(:checkout) { FactoryBot.create(:checkout, media_object_id: media_object.id) } + + it 'returns the checked out MediaObject' do + expect(checkout.media_object).to eq media_object + end + end +end diff --git a/spec/models/media_object_spec.rb b/spec/models/media_object_spec.rb index 3d9d6acfc7..e1c5e5229f 100644 --- a/spec/models/media_object_spec.rb +++ b/spec/models/media_object_spec.rb @@ -1006,4 +1006,18 @@ end it_behaves_like "an object that has supplemental files" + + describe 'lending_status' do + it 'is available when no active checkouts' do + expect(media_object.lending_status).to eq "available" + end + + context 'with an active checkout' do + before { FactoryBot.create(:checkout, media_object_id: media_object.id) } + + it 'is checked_out' do + expect(media_object.lending_status).to eq "checked_out" + end + end + end end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index a643870474..1fe610ff16 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -187,6 +187,7 @@ # config.include FactoryBot::Syntax::Methods config.include Devise::Test::ControllerHelpers, type: :controller config.include ControllerMacros, type: :controller + config.include Devise::Test::IntegrationHelpers, type: :request config.include Warden::Test::Helpers,type: :feature config.include FixtureMacros, type: :controller config.include OptionalExample diff --git a/spec/requests/checkouts_spec.rb b/spec/requests/checkouts_spec.rb new file mode 100644 index 0000000000..18c733b3f2 --- /dev/null +++ b/spec/requests/checkouts_spec.rb @@ -0,0 +1,121 @@ +require 'rails_helper' + +# This spec was generated by rspec-rails when you ran the scaffold generator. +# It demonstrates how one might use RSpec to test the controller code that +# was generated by Rails when you ran the scaffold generator. +# +# It assumes that the implementation code is generated by the rails scaffold +# generator. If you are using any extension libraries to generate different +# controller code, this generated spec may or may not pass. +# +# It only uses APIs available in rails and/or rspec-rails. There are a number +# of tools you can use to make these specs even more expressive, but we're +# sticking to rails and rspec-rails APIs to keep things simple and stable. + +RSpec.describe "/checkouts", type: :request do + + let(:user) { FactoryBot.create(:user) } + let(:media_object) { FactoryBot.create(:published_media_object, visibility: 'public') } + let(:checkout) { FactoryBot.create(:checkout, user: user, media_object_id: media_object.id) } + + # This should return the minimal set of attributes required to create a valid + # Checkout. As you add validations to Checkout, be sure to + # adjust the attributes here as well. + let(:valid_attributes) { + { media_object_id: media_object.id } + } + + let(:invalid_attributes) { + { media_object_id: 'fake-id' } + } + + before { sign_in(user) } + + describe "GET /index" do + before { checkout } + + it "renders a successful response" do + get checkouts_url + expect(response).to be_successful + end + end + + describe "GET /show" do + it "renders a successful response" do + get checkout_url(checkout, format: :json) + expect(response).to be_successful + end + end + + describe "POST /create" do + context "with valid parameters" do + it "creates a new Checkout" do + expect { + post checkouts_url, params: { checkout: valid_attributes, format: :json } + }.to change(Checkout, :count).by(1) + end + + it "redirects to the created checkout" do + post checkouts_url, params: { checkout: valid_attributes, format: :json } + expect(response).to be_created + end + end + + context "with invalid parameters" do + it "does not create a new Checkout" do + expect { + post checkouts_url, params: { checkout: invalid_attributes, format: :json } + }.to change(Checkout, :count).by(0) + end + + it "returns 404 because the media object cannot be found" do + post checkouts_url, params: { checkout: invalid_attributes, format: :json } + expect(response).to be_not_found + end + end + end + + describe "PATCH /update" do + context "with valid parameters" do + let(:new_return_time) { DateTime.now + 3.weeks } + let(:new_attributes) { + { return_time: new_return_time } + } + let(:invalid_new_attributes) {} + + it "updates the requested checkout" do + patch checkout_url(checkout), params: { checkout: new_attributes, format: :json } + checkout.reload + checkout.return_time = new_return_time + end + + it "redirects to the checkout" do + patch checkout_url(checkout), params: { checkout: new_attributes, format: :json } + checkout.reload + expect(response).to be_ok + end + end + + context "with invalid parameters" do + xit "returns a 422 Unprocessable entity" do + patch checkout_url(checkout), params: { checkout: invalid_new_attributes, format: :json } + expect(response).to be_unprocessable_entity + end + end + end + + describe "DELETE /destroy" do + it "destroys the requested checkout" do + # Make sure the checkout is created before the expect line below + checkout + expect { + delete checkout_url(checkout) + }.to change(Checkout, :count).by(-1) + end + + it "redirects to the checkouts list" do + delete checkout_url(checkout) + expect(response).to redirect_to(checkouts_url) + end + end +end diff --git a/spec/routing/checkouts_routing_spec.rb b/spec/routing/checkouts_routing_spec.rb new file mode 100644 index 0000000000..f6aada1417 --- /dev/null +++ b/spec/routing/checkouts_routing_spec.rb @@ -0,0 +1,29 @@ +require "rails_helper" + +RSpec.describe CheckoutsController, type: :routing do + describe "routing" do + it "routes to #index" do + expect(get: "/checkouts").to route_to("checkouts#index") + end + + it "routes to #create" do + expect(post: "/checkouts").to route_to("checkouts#create") + end + + it "routes to #show via GET" do + expect(get: "/checkouts/1").to route_to("checkouts#show", id: "1") + end + + it "routes to #update via PUT" do + expect(put: "/checkouts/1").to route_to("checkouts#update", id: "1") + end + + it "routes to #update via PATCH" do + expect(patch: "/checkouts/1").to route_to("checkouts#update", id: "1") + end + + it "routes to #destroy" do + expect(delete: "/checkouts/1").to route_to("checkouts#destroy", id: "1") + end + end +end diff --git a/spec/views/checkouts/index.html.erb_spec.rb b/spec/views/checkouts/index.html.erb_spec.rb new file mode 100644 index 0000000000..a5c5bfbba7 --- /dev/null +++ b/spec/views/checkouts/index.html.erb_spec.rb @@ -0,0 +1,16 @@ +require 'rails_helper' + +RSpec.describe "checkouts/index", type: :view do + let(:checkouts) { [FactoryBot.create(:checkout), FactoryBot.create(:checkout)] } + before(:each) do + assign(:checkouts, checkouts) + end + + it "renders a list of checkouts" do + render + assert_select "tr>td", text: checkouts.first.user.user_key + assert_select "tr>td", text: checkouts.first.media_object.title + assert_select "tr>td", text: checkouts.second.user.user_key + assert_select "tr>td", text: checkouts.second.media_object.title + end +end From 537f321d17c073103e08cbf2aa7f391c112bbb58 Mon Sep 17 00:00:00 2001 From: dananji Date: Tue, 7 Jun 2022 12:21:11 -0400 Subject: [PATCH 003/230] Add Checkouts to navbar --- app/controllers/checkouts_controller.rb | 2 +- app/models/checkout.rb | 1 + app/views/_user_util_links.html.erb | 8 ++++++++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/app/controllers/checkouts_controller.rb b/app/controllers/checkouts_controller.rb index c476d8d9cd..7708aceb3b 100644 --- a/app/controllers/checkouts_controller.rb +++ b/app/controllers/checkouts_controller.rb @@ -4,7 +4,7 @@ class CheckoutsController < ApplicationController # GET /checkouts or /checkouts.json def index - @checkouts = Checkout.all + @checkouts = Checkout.checkouts(current_user.id) end # GET /checkouts/1.json diff --git a/app/models/checkout.rb b/app/models/checkout.rb index 4d81fc7942..0f29ad6846 100644 --- a/app/models/checkout.rb +++ b/app/models/checkout.rb @@ -4,6 +4,7 @@ class Checkout < ApplicationRecord validates :user, :media_object_id, :checkout_time, :return_time, presence: true scope :active_for, ->(media_object_id) { where(media_object_id: media_object_id).where("return_time > now()") } + scope :checkouts, ->(user_id) { where(user_id: user_id).where("return_time > now()") } def media_object MediaObject.find(media_object_id) diff --git a/app/views/_user_util_links.html.erb b/app/views/_user_util_links.html.erb index 3d3e7786ad..d9ef72dadb 100644 --- a/app/views/_user_util_links.html.erb +++ b/app/views/_user_util_links.html.erb @@ -32,6 +32,14 @@ Unless required by applicable law or agreed to in writing, software distributed <%= link_to 'Timelines', main_app.timelines_path, id:'timelines_nav', class: 'nav-link' %> <% end %> + <% if current_ability.can? :create, Checkout %> + + <% end %> <% if render_bookmarks_control? %> <% end %> diff --git a/spec/models/checkout_spec.rb b/spec/models/checkout_spec.rb index 4e8c1e5047..fcb257dde4 100644 --- a/spec/models/checkout_spec.rb +++ b/spec/models/checkout_spec.rb @@ -42,13 +42,13 @@ let!(:checkout) { FactoryBot.create(:checkout, media_object_id: media_object.id) } let!(:expired_checkout) { FactoryBot.create(:checkout, media_object_id: media_object.id, checkout_time: DateTime.now - 2.weeks, return_time: DateTime.now - 1.day) } - describe 'active_for' do + describe 'active_for_media_object' do it 'returns active checkouts for the given media object' do - expect(Checkout.active_for(media_object.id)).to include(checkout) + expect(Checkout.active_for_media_object(media_object.id)).to include(checkout) end it 'does not return inactive checkouts' do - expect(Checkout.active_for(media_object.id)).not_to include(expired_checkout) + expect(Checkout.active_for_media_object(media_object.id)).not_to include(expired_checkout) end end end From c37a67b396876027aee5f48b8d743a664f38da8d Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Wed, 8 Jun 2022 11:55:54 -0400 Subject: [PATCH 006/230] Add CDL values to settings file --- config/settings.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/config/settings.yml b/config/settings.yml index b742367789..23b67d36fa 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -95,4 +95,7 @@ waveform: active_storage: service: local #bucket: supplementalfiles - +controlled_digital_lending: + enable: true + default_lending_period: 'P14D' # ISO8601 duration format: P14D == 14.days, PT8H == 8.hours, etc. + max_checkouts_per_user: 25 From 0b731990b01bb3b58f91d029541f2d2fcd5c2751 Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Wed, 8 Jun 2022 15:39:15 -0400 Subject: [PATCH 007/230] Create "Checkouts" page Make regular users see only their active checkouts Add table to checkouts page and get links working WIP Add return all feature WIP Add checkbox to display historical checkouts Add return_all functionality Add display all checkouts checkbox and view tests --- app/controllers/checkouts_controller.rb | 21 ++++- app/models/ability.rb | 1 + app/models/checkout.rb | 5 +- .../checkouts/_inactive_checkout.html.erb | 21 +++++ app/views/checkouts/index.html.erb | 73 +++++++++++++----- config/routes.rb | 6 +- spec/controllers/checkouts_controller_spec.rb | 76 +++++++++++++++++++ spec/rails_helper.rb | 2 +- spec/routing/checkouts_routing_spec.rb | 4 + spec/views/checkouts/index.html.erb_spec.rb | 51 +++++++++++-- 10 files changed, 227 insertions(+), 33 deletions(-) create mode 100644 app/views/checkouts/_inactive_checkout.html.erb create mode 100644 spec/controllers/checkouts_controller_spec.rb diff --git a/app/controllers/checkouts_controller.rb b/app/controllers/checkouts_controller.rb index 9833c42407..86dc98273b 100644 --- a/app/controllers/checkouts_controller.rb +++ b/app/controllers/checkouts_controller.rb @@ -1,10 +1,11 @@ class CheckoutsController < ApplicationController before_action :set_checkout, only: %i[show update destroy] + before_action :user_checkouts, only: %i[index return_all] load_and_authorize_resource except: [:create] # GET /checkouts or /checkouts.json def index - @checkouts = Checkout.active_for_user(current_user.id) + @checkouts end # GET /checkouts/1.json @@ -47,6 +48,16 @@ def destroy end end + # DELETE /checkouts + def return_all + @checkouts.destroy_all + + respond_to do |format| + format.html { redirect_to checkouts_url, notice: "Checkouts were sucessfully destroyed." } + format.json { head :no_content } + end + end + private # Use callbacks to share common setup or constraints between actions. @@ -54,6 +65,14 @@ def set_checkout @checkout = Checkout.find(params[:id]) end + def user_checkouts + @checkouts = if current_user.groups.include? 'administrator' + Checkout.all.where("return_time > now()") + else + Checkout.checkouts(current_user.id) + end + end + # Only allow a list of trusted parameters through. def checkout_params params.require(:checkout).permit(:media_object_id, :return_time) diff --git a/app/models/ability.rb b/app/models/ability.rb index 01680f803f..9ce294a1e8 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -226,6 +226,7 @@ def checkout_permissions can :create, Checkout do |checkout| checkout.user == @user && can?(:read, checkout.media_object) end + can :return_all, Checkout, user: @user can :read, Checkout, user: @user can :update, Checkout, user: @user can :destroy, Checkout, user: @user diff --git a/app/models/checkout.rb b/app/models/checkout.rb index b19df2d30a..f8cb9c1aab 100644 --- a/app/models/checkout.rb +++ b/app/models/checkout.rb @@ -3,8 +3,9 @@ class Checkout < ApplicationRecord validates :user, :media_object_id, :checkout_time, :return_time, presence: true - scope :active_for_media_object, ->(media_object_id) { where(media_object_id: media_object_id).where("return_time > now()") } - scope :active_for_user, ->(user_id) { where(user_id: user_id).where("return_time > now()") } + scope :active_for, ->(media_object_id) { where(media_object_id: media_object_id).where("return_time > now()") } + scope :checkouts, ->(user_id) { where(user_id: user_id).where("return_time > now()") } + scope :returned_checkouts, ->(user_id) { where(user_id: user_id).where("return_time < now()") } def media_object MediaObject.find(media_object_id) diff --git a/app/views/checkouts/_inactive_checkout.html.erb b/app/views/checkouts/_inactive_checkout.html.erb new file mode 100644 index 0000000000..e18f7bb956 --- /dev/null +++ b/app/views/checkouts/_inactive_checkout.html.erb @@ -0,0 +1,21 @@ +<%# +Copyright 2011-2022, The Trustees of Indiana University and Northwestern + University. Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed + under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + CONDITIONS OF ANY KIND, either express or implied. See the License for the + specific language governing permissions and limitations under the License. +--- END LICENSE_HEADER BLOCK --- +%> +<% # container for display inactive checkouts checkbox -%> +
+ <% inactive_checkout = Checkout.returned_checkouts(current_user.id) %> + <%= check_box_tag('inactive_checkout', '', inactive_checkout) %> + <%= label_tag 'inactivecheckout', 'Display Returned Items', class: 'font-weight-bold' %> +
diff --git a/app/views/checkouts/index.html.erb b/app/views/checkouts/index.html.erb index 0fabb6d2c9..949aca825d 100644 --- a/app/views/checkouts/index.html.erb +++ b/app/views/checkouts/index.html.erb @@ -2,28 +2,59 @@

Checkouts

- - - - - - - - - - +
+ <% if Checkout.returned_checkouts(current_user.id).count > 0 or current_user.groups.include? 'administrator' %> + <%= render "inactive_checkout" %> + <% end %> -
- <% @checkouts.each do |checkout| %> +
UserMedia objectCheckout timeReturn time
+ - - - - - + <% if current_user.groups.include? 'administrator' %> + + <% end %> + + + + + - <% end %> - -
<%= checkout.user.user_key %><%= checkout.media_object.title %><%= checkout.checkout_time %><%= checkout.return_time %><%= link_to 'Destroy', checkout, method: :delete, data: { confirm: 'Are you sure?' } %>UserMedia objectCheckout timeReturn timeTime remaining + <% if Checkout.checkouts(current_user.id).count > 1 or current_user.groups.include? 'administrator' %> + <%= link_to 'Return All', main_app.checkouts_path , class: 'btn btn-primary btn-xs', method: :delete, data: { confirm: 'Are you sure?' } %> + <% end %> +
+ -
+ + <% @checkouts.each do |checkout| %> + + <% if current_user.groups.include? 'administrator' %> + <%= checkout.user.user_key %> + <% end %> + <%= link_to checkout.media_object.title, main_app.media_object_url(checkout.media_object) %> + <%= checkout.checkout_time.to_s(:long_ordinal) %> + <%= checkout.return_time.to_s(:long_ordinal) %> + <%= distance_of_time_in_words(checkout.return_time - DateTime.current) %> + + <% if checkout.return_time > DateTime.current %> + <%= link_to 'Return', checkout, class: 'btn btn-danger btn-xs', method: :delete, data: { confirm: 'Are you sure?' } %> + <% else %> + <%= link_to 'Checkout', checkout, class: 'btn btn-primary btn-xs', method: :post %> + <% end %> + + + <% end %> + + + + +<% content_for :page_scripts do %> + +<% end %> diff --git a/config/routes.rb b/config/routes.rb index f956f8870b..51a63375d2 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -26,7 +26,11 @@ end end - resources :checkouts, only: [:index, :create, :show, :update, :destroy] + resources :checkouts, only: [:index, :create, :show, :update, :destroy] do + collection do + delete '', as: '', to: 'checkouts#return_all' + end + end resources :bookmarks do concerns :exportable diff --git a/spec/controllers/checkouts_controller_spec.rb b/spec/controllers/checkouts_controller_spec.rb new file mode 100644 index 0000000000..b323afd092 --- /dev/null +++ b/spec/controllers/checkouts_controller_spec.rb @@ -0,0 +1,76 @@ +require 'rails_helper' + +RSpec.describe CheckoutsController, type: :controller do + + describe 'GET #index' do + before :each do + FactoryBot.reload + FactoryBot.create_list(:checkout, 2) + FactoryBot.create(:checkout, return_time: DateTime.current - 2.weeks) + end + context 'as an admin user' do + let(:admin) { FactoryBot.create(:admin) } + before { sign_in admin } + before { FactoryBot.create(:checkout, user: admin) } + + it 'returns all active checkouts' do + get :index, params: {} + expect(assigns(:checkouts).count).to eq(3) + end + end + context 'as a regular user' do + let(:user) { FactoryBot.create(:user) } + before { sign_in user } + before { FactoryBot.create(:checkout, user: user) } + before { FactoryBot.create(:checkout, user: user, return_time: DateTime.current - 2.weeks) } + + it "returns the current user's active checkouts" do + get :index, params: {} + expect(assigns(:checkouts).count).to eq(1) + end + end + end + + describe 'DELETE #destroy' do + let(:user) { FactoryBot.create(:user) } + before { sign_in user } + before { FactoryBot.create(:checkout, user: user) } + before { FactoryBot.create(:checkout) } + + before { delete :destroy, params: { id: 1 } } + + it 'deletes the selected checkout' do + expect{ Checkout.find(user.id) }.to raise_error(ActiveRecord::RecordNotFound) + expect(flash[:notice]).to match "was successfully destroyed." + end + it 'does not delete non-selected checkouts' do + expect{ Checkout.find(2) }.not_to raise_error + end + end + + describe 'DELETE #return_all' do + before :each do + FactoryBot.reload + FactoryBot.create_list(:checkout, 2) + end + context 'as a regular user' do + let(:user1) { FactoryBot.create(:user) } + before { sign_in user1 } + before { FactoryBot.create(:checkout, user: user1) } + + it "deletes the current user's checkouts" do + delete :return_all + expect(Checkout.all.count).to eq(2) + end + end + context 'as an admin user' do + let(:admin) { FactoryBot.create(:admin) } + before { sign_in admin } + + it 'deletes all checkouts' do + delete :return_all + expect(Checkout.all.count).to eq(0) + end + end + end +end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 1fe610ff16..aed0ab9eb2 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -115,7 +115,7 @@ config.before :suite do WebMock.disable_net_connect!(allow: ['localhost', '127.0.0.1', 'fedora', 'fedora-test', 'solr', 'solr-test', 'matterhorn', 'https://chromedriver.storage.googleapis.com']) DatabaseCleaner.allow_remote_database_url = true - DatabaseCleaner.url_allowlist = ['postgres://postgres:password@db/avalon', 'postgresql://postgres@localhost:5432/postgres'] + DatabaseCleaner.url_allowlist = ['postgres://postgres:password@db/avalon', 'postgresql://postgres@localhost:5432/postgres', 'postgresql://postgres:password@db-test/avalon'] DatabaseCleaner.clean_with(:truncation) ActiveFedora::Cleaner.clean! disable_production_minter! diff --git a/spec/routing/checkouts_routing_spec.rb b/spec/routing/checkouts_routing_spec.rb index f6aada1417..3df1570fc9 100644 --- a/spec/routing/checkouts_routing_spec.rb +++ b/spec/routing/checkouts_routing_spec.rb @@ -25,5 +25,9 @@ it "routes to #destroy" do expect(delete: "/checkouts/1").to route_to("checkouts#destroy", id: "1") end + + it "routes to #return_all" do + expect(delete: "/checkouts").to route_to("checkouts#return_all") + end end end diff --git a/spec/views/checkouts/index.html.erb_spec.rb b/spec/views/checkouts/index.html.erb_spec.rb index a5c5bfbba7..fb73650736 100644 --- a/spec/views/checkouts/index.html.erb_spec.rb +++ b/spec/views/checkouts/index.html.erb_spec.rb @@ -1,16 +1,53 @@ require 'rails_helper' RSpec.describe "checkouts/index", type: :view do + let(:user) { FactoryBot.create(:user) } let(:checkouts) { [FactoryBot.create(:checkout), FactoryBot.create(:checkout)] } before(:each) do assign(:checkouts, checkouts) + allow(view).to receive(:current_user).and_return(user) end - - it "renders a list of checkouts" do - render - assert_select "tr>td", text: checkouts.first.user.user_key - assert_select "tr>td", text: checkouts.first.media_object.title - assert_select "tr>td", text: checkouts.second.user.user_key - assert_select "tr>td", text: checkouts.second.media_object.title + context 'as a regular user' do + it 'renders a list of checkouts without username' do + render + assert_select "tr>td", text: checkouts.first.user.user_key, count: 0 + assert_select "tr>td", text: checkouts.first.media_object.title + assert_select "tr>td", text: checkouts.second.user.user_key, count: 0 + assert_select "tr>td", text: checkouts.second.media_object.title + end + context 'no previously returned checkouts' do + it 'does not render the show inactive checkouts checkbox' do + render + expect(response).not_to render_template('checkouts/_inactive_checkout') + end + end + context 'has previously returned checkouts' do + before { FactoryBot.create(:checkout, user: user, return_time: DateTime.current - 1.week) } + it 'renders the show inactive checkouts checkbox' do + render + expect(response).to render_template('checkouts/_inactive_checkout') + end + end + end + context 'as an admin user' do + let(:admin) { FactoryBot.create(:admin) } + before { allow(view).to receive(:current_user).and_return(admin) } + it 'renders a list of checkouts with usernames' do + render + assert_select "tr>td", text: checkouts.first.user.user_key + assert_select "tr>td", text: checkouts.first.media_object.title + assert_select "tr>td", text: checkouts.second.user.user_key + assert_select "tr>td", text: checkouts.second.media_object.title + end + it 'renders the show inactive checkouts checkbox' do + render + expect(response).to render_template('checkouts/_inactive_checkout') + end + end + describe 'media object entry' do + it 'is a link' do + render + assert_select "tr>td", html: "#{checkouts.first.media_object.title}" + end end end From f4c360ac145f7cd57b167cdffc87706df0b09d52 Mon Sep 17 00:00:00 2001 From: Mason Ballengee <68433277+masaball@users.noreply.github.com> Date: Mon, 13 Jun 2022 10:55:46 -0400 Subject: [PATCH 008/230] Update app/views/checkouts/index.html.erb Co-authored-by: Chris Colvard --- app/views/checkouts/index.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/checkouts/index.html.erb b/app/views/checkouts/index.html.erb index 949aca825d..1a5fc588cd 100644 --- a/app/views/checkouts/index.html.erb +++ b/app/views/checkouts/index.html.erb @@ -3,7 +3,7 @@

Checkouts

- <% if Checkout.returned_checkouts(current_user.id).count > 0 or current_user.groups.include? 'administrator' %> + <% if Checkout.returned_checkouts(current_user.id).any? || current_ability.is_administrator? %> <%= render "inactive_checkout" %> <% end %> From 01c7dc2f43d3ca355bbf224580ca52bcd5ff67be Mon Sep 17 00:00:00 2001 From: Mason Ballengee <68433277+masaball@users.noreply.github.com> Date: Mon, 13 Jun 2022 11:00:42 -0400 Subject: [PATCH 009/230] Update spec/views/checkouts/index.html.erb_spec.rb Co-authored-by: Chris Colvard --- spec/views/checkouts/index.html.erb_spec.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/spec/views/checkouts/index.html.erb_spec.rb b/spec/views/checkouts/index.html.erb_spec.rb index fb73650736..32bea75afe 100644 --- a/spec/views/checkouts/index.html.erb_spec.rb +++ b/spec/views/checkouts/index.html.erb_spec.rb @@ -30,8 +30,7 @@ end end context 'as an admin user' do - let(:admin) { FactoryBot.create(:admin) } - before { allow(view).to receive(:current_user).and_return(admin) } + let(:user) { FactoryBot.create(:admin) } it 'renders a list of checkouts with usernames' do render assert_select "tr>td", text: checkouts.first.user.user_key From b91fd306c6771e102906173cfa4c71df67f02feb Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Mon, 13 Jun 2022 12:21:32 -0400 Subject: [PATCH 010/230] Remove checkout controller spec Best practice is trending towards not using controller specs. Checkout tests are handled by request specs. --- spec/controllers/checkouts_controller_spec.rb | 76 ------------------- 1 file changed, 76 deletions(-) delete mode 100644 spec/controllers/checkouts_controller_spec.rb diff --git a/spec/controllers/checkouts_controller_spec.rb b/spec/controllers/checkouts_controller_spec.rb deleted file mode 100644 index b323afd092..0000000000 --- a/spec/controllers/checkouts_controller_spec.rb +++ /dev/null @@ -1,76 +0,0 @@ -require 'rails_helper' - -RSpec.describe CheckoutsController, type: :controller do - - describe 'GET #index' do - before :each do - FactoryBot.reload - FactoryBot.create_list(:checkout, 2) - FactoryBot.create(:checkout, return_time: DateTime.current - 2.weeks) - end - context 'as an admin user' do - let(:admin) { FactoryBot.create(:admin) } - before { sign_in admin } - before { FactoryBot.create(:checkout, user: admin) } - - it 'returns all active checkouts' do - get :index, params: {} - expect(assigns(:checkouts).count).to eq(3) - end - end - context 'as a regular user' do - let(:user) { FactoryBot.create(:user) } - before { sign_in user } - before { FactoryBot.create(:checkout, user: user) } - before { FactoryBot.create(:checkout, user: user, return_time: DateTime.current - 2.weeks) } - - it "returns the current user's active checkouts" do - get :index, params: {} - expect(assigns(:checkouts).count).to eq(1) - end - end - end - - describe 'DELETE #destroy' do - let(:user) { FactoryBot.create(:user) } - before { sign_in user } - before { FactoryBot.create(:checkout, user: user) } - before { FactoryBot.create(:checkout) } - - before { delete :destroy, params: { id: 1 } } - - it 'deletes the selected checkout' do - expect{ Checkout.find(user.id) }.to raise_error(ActiveRecord::RecordNotFound) - expect(flash[:notice]).to match "was successfully destroyed." - end - it 'does not delete non-selected checkouts' do - expect{ Checkout.find(2) }.not_to raise_error - end - end - - describe 'DELETE #return_all' do - before :each do - FactoryBot.reload - FactoryBot.create_list(:checkout, 2) - end - context 'as a regular user' do - let(:user1) { FactoryBot.create(:user) } - before { sign_in user1 } - before { FactoryBot.create(:checkout, user: user1) } - - it "deletes the current user's checkouts" do - delete :return_all - expect(Checkout.all.count).to eq(2) - end - end - context 'as an admin user' do - let(:admin) { FactoryBot.create(:admin) } - before { sign_in admin } - - it 'deletes all checkouts' do - delete :return_all - expect(Checkout.all.count).to eq(0) - end - end - end -end From b9cd80b09d5e2068d725013b136c8fe3ee5a8870 Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Mon, 13 Jun 2022 12:30:46 -0400 Subject: [PATCH 011/230] Refactor checkouts page --- app/controllers/checkouts_controller.rb | 10 +++--- app/models/ability.rb | 2 +- app/models/checkout.rb | 6 ++-- .../checkouts/_inactive_checkout.html.erb | 2 +- app/views/checkouts/index.html.erb | 8 ++--- config/routes.rb | 2 +- spec/requests/checkouts_spec.rb | 33 +++++++++++++++++++ spec/views/checkouts/index.html.erb_spec.rb | 4 ++- 8 files changed, 51 insertions(+), 16 deletions(-) diff --git a/app/controllers/checkouts_controller.rb b/app/controllers/checkouts_controller.rb index 86dc98273b..1b4626c386 100644 --- a/app/controllers/checkouts_controller.rb +++ b/app/controllers/checkouts_controller.rb @@ -1,6 +1,6 @@ class CheckoutsController < ApplicationController before_action :set_checkout, only: %i[show update destroy] - before_action :user_checkouts, only: %i[index return_all] + before_action :set_checkouts, only: %i[index return_all] load_and_authorize_resource except: [:create] # GET /checkouts or /checkouts.json @@ -49,7 +49,7 @@ def destroy end # DELETE /checkouts - def return_all + def destroy_all @checkouts.destroy_all respond_to do |format| @@ -65,11 +65,11 @@ def set_checkout @checkout = Checkout.find(params[:id]) end - def user_checkouts - @checkouts = if current_user.groups.include? 'administrator' + def set_checkouts + @checkouts = if current_ability.is_administrator? Checkout.all.where("return_time > now()") else - Checkout.checkouts(current_user.id) + Checkout.active_for_user(current_user.id) end end diff --git a/app/models/ability.rb b/app/models/ability.rb index 9ce294a1e8..427b704773 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -226,7 +226,7 @@ def checkout_permissions can :create, Checkout do |checkout| checkout.user == @user && can?(:read, checkout.media_object) end - can :return_all, Checkout, user: @user + can :destroy_all, Checkout, user: @user can :read, Checkout, user: @user can :update, Checkout, user: @user can :destroy, Checkout, user: @user diff --git a/app/models/checkout.rb b/app/models/checkout.rb index f8cb9c1aab..f394554fdb 100644 --- a/app/models/checkout.rb +++ b/app/models/checkout.rb @@ -3,9 +3,9 @@ class Checkout < ApplicationRecord validates :user, :media_object_id, :checkout_time, :return_time, presence: true - scope :active_for, ->(media_object_id) { where(media_object_id: media_object_id).where("return_time > now()") } - scope :checkouts, ->(user_id) { where(user_id: user_id).where("return_time > now()") } - scope :returned_checkouts, ->(user_id) { where(user_id: user_id).where("return_time < now()") } + scope :active_for_media_object, ->(media_object_id) { where(media_object_id: media_object_id).where("return_time > now()") } + scope :active_for_user, ->(user_id) { where(user_id: user_id).where("return_time > now()") } + scope :returned_for_user, ->(user_id) { where(user_id: user_id).where("return_time < now()") } def media_object MediaObject.find(media_object_id) diff --git a/app/views/checkouts/_inactive_checkout.html.erb b/app/views/checkouts/_inactive_checkout.html.erb index e18f7bb956..d7849dab2f 100644 --- a/app/views/checkouts/_inactive_checkout.html.erb +++ b/app/views/checkouts/_inactive_checkout.html.erb @@ -15,7 +15,7 @@ Unless required by applicable law or agreed to in writing, software distributed %> <% # container for display inactive checkouts checkbox -%>
- <% inactive_checkout = Checkout.returned_checkouts(current_user.id) %> + <% inactive_checkout = Checkout.returned_for_user(current_user.id) %> <%= check_box_tag('inactive_checkout', '', inactive_checkout) %> <%= label_tag 'inactivecheckout', 'Display Returned Items', class: 'font-weight-bold' %>
diff --git a/app/views/checkouts/index.html.erb b/app/views/checkouts/index.html.erb index 1a5fc588cd..7f53141080 100644 --- a/app/views/checkouts/index.html.erb +++ b/app/views/checkouts/index.html.erb @@ -3,14 +3,14 @@

Checkouts

- <% if Checkout.returned_checkouts(current_user.id).any? || current_ability.is_administrator? %> + <% if Checkout.returned_for_user(current_user.id).any? || current_ability.is_administrator? %> <%= render "inactive_checkout" %> <% end %> - <% if current_user.groups.include? 'administrator' %> + <% if current_ability.is_administrator? %> <% end %> @@ -18,7 +18,7 @@ @@ -28,7 +28,7 @@ <% @checkouts.each do |checkout| %> - <% if current_user.groups.include? 'administrator' %> + <% if current_ability.is_administrator? %> <% end %> diff --git a/config/routes.rb b/config/routes.rb index 51a63375d2..90413b7957 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -28,7 +28,7 @@ resources :checkouts, only: [:index, :create, :show, :update, :destroy] do collection do - delete '', as: '', to: 'checkouts#return_all' + delete '', as: '', to: 'checkouts#destroy_all' end end diff --git a/spec/requests/checkouts_spec.rb b/spec/requests/checkouts_spec.rb index 18c733b3f2..f236368c74 100644 --- a/spec/requests/checkouts_spec.rb +++ b/spec/requests/checkouts_spec.rb @@ -118,4 +118,37 @@ expect(response).to redirect_to(checkouts_url) end end + + describe 'DELETE #destroy_all' do + before :each do + FactoryBot.reload + FactoryBot.create_list(:checkout, 2) + end + context 'as a regular user' do + it "deletes the current user's checkouts" do + checkout + delete checkouts_url + expect(Checkout.all.count).to eq(2) + end + + it "redirects to the checkouts list" do + delete checkout_url(checkout) + expect(response).to redirect_to(checkouts_url) + end + end + context 'as an admin user' do + let(:admin) { FactoryBot.create(:admin) } + before { sign_in admin } + + it 'deletes all checkouts' do + delete checkouts_url + expect(Checkout.all.count).to eq(0) + end + + it "redirects to the checkouts list" do + delete checkout_url(checkout) + expect(response).to redirect_to(checkouts_url) + end + end + end end diff --git a/spec/views/checkouts/index.html.erb_spec.rb b/spec/views/checkouts/index.html.erb_spec.rb index 32bea75afe..a2d6198b9f 100644 --- a/spec/views/checkouts/index.html.erb_spec.rb +++ b/spec/views/checkouts/index.html.erb_spec.rb @@ -2,10 +2,12 @@ RSpec.describe "checkouts/index", type: :view do let(:user) { FactoryBot.create(:user) } + let(:ability) { Ability.new(user) } let(:checkouts) { [FactoryBot.create(:checkout), FactoryBot.create(:checkout)] } before(:each) do assign(:checkouts, checkouts) allow(view).to receive(:current_user).and_return(user) + allow(view).to receive(:current_ability).and_return(ability) end context 'as a regular user' do it 'renders a list of checkouts without username' do @@ -15,7 +17,7 @@ assert_select "tr>td", text: checkouts.second.user.user_key, count: 0 assert_select "tr>td", text: checkouts.second.media_object.title end - context 'no previously returned checkouts' do + context 'has no previously returned checkouts' do it 'does not render the show inactive checkouts checkbox' do render expect(response).not_to render_template('checkouts/_inactive_checkout') From 227bb985bcb3338ba78bcd6dc941cbe5366df386 Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Tue, 14 Jun 2022 10:10:18 -0400 Subject: [PATCH 012/230] Remove destroy_all functionality --- app/controllers/checkouts_controller.rb | 10 -------- app/views/checkouts/index.html.erb | 4 +-- config/routes.rb | 6 +---- spec/models/checkout_spec.rb | 25 +++++++++++++++++-- spec/requests/checkouts_spec.rb | 33 ------------------------- spec/routing/checkouts_routing_spec.rb | 4 --- 6 files changed, 25 insertions(+), 57 deletions(-) diff --git a/app/controllers/checkouts_controller.rb b/app/controllers/checkouts_controller.rb index 1b4626c386..03ff313b1c 100644 --- a/app/controllers/checkouts_controller.rb +++ b/app/controllers/checkouts_controller.rb @@ -48,16 +48,6 @@ def destroy end end - # DELETE /checkouts - def destroy_all - @checkouts.destroy_all - - respond_to do |format| - format.html { redirect_to checkouts_url, notice: "Checkouts were sucessfully destroyed." } - format.json { head :no_content } - end - end - private # Use callbacks to share common setup or constraints between actions. diff --git a/app/views/checkouts/index.html.erb b/app/views/checkouts/index.html.erb index 7f53141080..8854cd3ed4 100644 --- a/app/views/checkouts/index.html.erb +++ b/app/views/checkouts/index.html.erb @@ -18,9 +18,7 @@ diff --git a/config/routes.rb b/config/routes.rb index 90413b7957..f956f8870b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -26,11 +26,7 @@ end end - resources :checkouts, only: [:index, :create, :show, :update, :destroy] do - collection do - delete '', as: '', to: 'checkouts#destroy_all' - end - end + resources :checkouts, only: [:index, :create, :show, :update, :destroy] resources :bookmarks do concerns :exportable diff --git a/spec/models/checkout_spec.rb b/spec/models/checkout_spec.rb index fcb257dde4..36f7f871e3 100644 --- a/spec/models/checkout_spec.rb +++ b/spec/models/checkout_spec.rb @@ -38,9 +38,10 @@ end describe 'scopes' do + let(:user) { FactoryBot.create(:user) } let(:media_object) { FactoryBot.create(:media_object) } - let!(:checkout) { FactoryBot.create(:checkout, media_object_id: media_object.id) } - let!(:expired_checkout) { FactoryBot.create(:checkout, media_object_id: media_object.id, checkout_time: DateTime.now - 2.weeks, return_time: DateTime.now - 1.day) } + let!(:checkout) { FactoryBot.create(:checkout, user: user, media_object_id: media_object.id) } + let!(:expired_checkout) { FactoryBot.create(:checkout, user: user, media_object_id: media_object.id, checkout_time: DateTime.now - 2.weeks, return_time: DateTime.now - 1.day) } describe 'active_for_media_object' do it 'returns active checkouts for the given media object' do @@ -51,6 +52,26 @@ expect(Checkout.active_for_media_object(media_object.id)).not_to include(expired_checkout) end end + + describe 'active_for_user' do + it 'returns active checkouts for the given media object' do + expect(Checkout.active_for_user(user.id)).to include(checkout) + end + + it 'does not return inactive checkouts' do + expect(Checkout.active_for_user(user.id)).not_to include(expired_checkout) + end + end + + describe 'returned_for_user' do + it 'does not return active checkouts for the given media object' do + expect(Checkout.returned_for_user(user.id)).not_to include(checkout) + end + + it 'does return inactive checkouts' do + expect(Checkout.returned_for_user(user.id)).to include(expired_checkout) + end + end end describe 'media_object' do diff --git a/spec/requests/checkouts_spec.rb b/spec/requests/checkouts_spec.rb index f236368c74..18c733b3f2 100644 --- a/spec/requests/checkouts_spec.rb +++ b/spec/requests/checkouts_spec.rb @@ -118,37 +118,4 @@ expect(response).to redirect_to(checkouts_url) end end - - describe 'DELETE #destroy_all' do - before :each do - FactoryBot.reload - FactoryBot.create_list(:checkout, 2) - end - context 'as a regular user' do - it "deletes the current user's checkouts" do - checkout - delete checkouts_url - expect(Checkout.all.count).to eq(2) - end - - it "redirects to the checkouts list" do - delete checkout_url(checkout) - expect(response).to redirect_to(checkouts_url) - end - end - context 'as an admin user' do - let(:admin) { FactoryBot.create(:admin) } - before { sign_in admin } - - it 'deletes all checkouts' do - delete checkouts_url - expect(Checkout.all.count).to eq(0) - end - - it "redirects to the checkouts list" do - delete checkout_url(checkout) - expect(response).to redirect_to(checkouts_url) - end - end - end end diff --git a/spec/routing/checkouts_routing_spec.rb b/spec/routing/checkouts_routing_spec.rb index 3df1570fc9..f6aada1417 100644 --- a/spec/routing/checkouts_routing_spec.rb +++ b/spec/routing/checkouts_routing_spec.rb @@ -25,9 +25,5 @@ it "routes to #destroy" do expect(delete: "/checkouts/1").to route_to("checkouts#destroy", id: "1") end - - it "routes to #return_all" do - expect(delete: "/checkouts").to route_to("checkouts#return_all") - end end end From 9b3278318c4f0cb5e20ec51642c7b6d1de4b6005 Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Tue, 14 Jun 2022 10:59:46 -0400 Subject: [PATCH 013/230] Fix incorrect test descriptions and ability.rb --- app/models/ability.rb | 1 - spec/models/checkout_spec.rb | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/app/models/ability.rb b/app/models/ability.rb index 427b704773..01680f803f 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -226,7 +226,6 @@ def checkout_permissions can :create, Checkout do |checkout| checkout.user == @user && can?(:read, checkout.media_object) end - can :destroy_all, Checkout, user: @user can :read, Checkout, user: @user can :update, Checkout, user: @user can :destroy, Checkout, user: @user diff --git a/spec/models/checkout_spec.rb b/spec/models/checkout_spec.rb index 36f7f871e3..b6133ab425 100644 --- a/spec/models/checkout_spec.rb +++ b/spec/models/checkout_spec.rb @@ -54,7 +54,7 @@ end describe 'active_for_user' do - it 'returns active checkouts for the given media object' do + it 'returns active checkouts for the given user' do expect(Checkout.active_for_user(user.id)).to include(checkout) end @@ -64,7 +64,7 @@ end describe 'returned_for_user' do - it 'does not return active checkouts for the given media object' do + it 'does not return active checkouts for the given user' do expect(Checkout.returned_for_user(user.id)).not_to include(checkout) end From 9baa1c29b584daab67ed48664c6e7817ece3d149 Mon Sep 17 00:00:00 2001 From: Chris Colvard Date: Wed, 15 Jun 2022 17:15:22 -0400 Subject: [PATCH 014/230] Set checkout time and return time based upon default setting at create time --- app/controllers/checkouts_controller.rb | 2 +- app/models/checkout.rb | 15 +++++++++++++++ spec/models/checkout_spec.rb | 8 +++++++- 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/app/controllers/checkouts_controller.rb b/app/controllers/checkouts_controller.rb index 03ff313b1c..3efdf93f42 100644 --- a/app/controllers/checkouts_controller.rb +++ b/app/controllers/checkouts_controller.rb @@ -14,7 +14,7 @@ def show # POST /checkouts.json def create - @checkout = Checkout.new(user: current_user, media_object_id: checkout_params[:media_object_id], checkout_time: DateTime.current, return_time: DateTime.current + 2.weeks) + @checkout = Checkout.new(user: current_user, media_object_id: checkout_params[:media_object_id]) respond_to do |format| # TODO: move this can? check into a checkout ability (can?(:create, @checkout)) diff --git a/app/models/checkout.rb b/app/models/checkout.rb index f394554fdb..18a21ba770 100644 --- a/app/models/checkout.rb +++ b/app/models/checkout.rb @@ -3,6 +3,8 @@ class Checkout < ApplicationRecord validates :user, :media_object_id, :checkout_time, :return_time, presence: true + after_initialize :set_checkout_return_times! + scope :active_for_media_object, ->(media_object_id) { where(media_object_id: media_object_id).where("return_time > now()") } scope :active_for_user, ->(user_id) { where(user_id: user_id).where("return_time > now()") } scope :returned_for_user, ->(user_id) { where(user_id: user_id).where("return_time < now()") } @@ -10,4 +12,17 @@ class Checkout < ApplicationRecord def media_object MediaObject.find(media_object_id) end + + private + + def set_checkout_return_times! + self.checkout_time ||= DateTime.current + self.return_time ||= checkout_time + duration + end + + def duration + # duration = media_object.lending_period + duration ||= ActiveSupport::Duration.parse(Settings.controlled_digital_lending.default_lending_period) + duration + end end diff --git a/spec/models/checkout_spec.rb b/spec/models/checkout_spec.rb index b6133ab425..28c04c500a 100644 --- a/spec/models/checkout_spec.rb +++ b/spec/models/checkout_spec.rb @@ -5,6 +5,8 @@ let(:checkout) { FactoryBot.create(:checkout) } describe 'validations' do + let(:checkout) { FactoryBot.build(:checkout) } + it { is_expected.to validate_presence_of(:user) } it { is_expected.to validate_presence_of(:media_object_id) } it { is_expected.to validate_presence_of(:checkout_time) } @@ -41,7 +43,11 @@ let(:user) { FactoryBot.create(:user) } let(:media_object) { FactoryBot.create(:media_object) } let!(:checkout) { FactoryBot.create(:checkout, user: user, media_object_id: media_object.id) } - let!(:expired_checkout) { FactoryBot.create(:checkout, user: user, media_object_id: media_object.id, checkout_time: DateTime.now - 2.weeks, return_time: DateTime.now - 1.day) } + let!(:expired_checkout) { FactoryBot.create(:checkout, user: user, media_object_id: media_object.id) } + + before do + expired_checkout.update(return_time: DateTime.current - 1.day) + end describe 'active_for_media_object' do it 'returns active checkouts for the given media object' do From 3482f9821730e5c6575e5eefd6af1b08c40f5663 Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Thu, 16 Jun 2022 13:10:22 -0400 Subject: [PATCH 015/230] Add return and return_all functionality --- app/controllers/checkouts_controller.rb | 21 ++++++++ app/models/ability.rb | 2 + .../checkouts/_inactive_checkout.html.erb | 3 +- app/views/checkouts/index.html.erb | 10 ++-- config/routes.rb | 10 +++- spec/requests/checkouts_spec.rb | 48 +++++++++++++++++++ spec/routing/checkouts_routing_spec.rb | 8 ++++ 7 files changed, 96 insertions(+), 6 deletions(-) diff --git a/app/controllers/checkouts_controller.rb b/app/controllers/checkouts_controller.rb index 3efdf93f42..80c3e3b8a0 100644 --- a/app/controllers/checkouts_controller.rb +++ b/app/controllers/checkouts_controller.rb @@ -38,6 +38,27 @@ def update end end + #PATCH /checkouts/1/return + def return + @checkout.update(return_time: DateTime.current) + + respond_to do |format| + format.html { redirect_to checkouts_url, notice: "Checkout was successfully returned." } + format.json { head :no_content } + end + end + + + # PATCH /checkouts/return_all + def return_all + @checkouts.each { |c| c.update(return_time: DateTime.current) } + + respond_to do |format| + format.html { redirect_to checkouts_url, notice: "All checkouts were successfully returned." } + format.json { head :no_content } + end + end + # DELETE /checkouts/1 or /checkouts/1.json def destroy @checkout.destroy diff --git a/app/models/ability.rb b/app/models/ability.rb index 01680f803f..871c0c1a17 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -226,6 +226,8 @@ def checkout_permissions can :create, Checkout do |checkout| checkout.user == @user && can?(:read, checkout.media_object) end + can :return, Checkout, user: @user + can :return_all, Checkout, user: @user can :read, Checkout, user: @user can :update, Checkout, user: @user can :destroy, Checkout, user: @user diff --git a/app/views/checkouts/_inactive_checkout.html.erb b/app/views/checkouts/_inactive_checkout.html.erb index d7849dab2f..34e04ef6db 100644 --- a/app/views/checkouts/_inactive_checkout.html.erb +++ b/app/views/checkouts/_inactive_checkout.html.erb @@ -15,7 +15,6 @@ Unless required by applicable law or agreed to in writing, software distributed %> <% # container for display inactive checkouts checkbox -%>
- <% inactive_checkout = Checkout.returned_for_user(current_user.id) %> - <%= check_box_tag('inactive_checkout', '', inactive_checkout) %> + <%= check_box_tag('inactive_checkout', '') %> <%= label_tag 'inactivecheckout', 'Display Returned Items', class: 'font-weight-bold' %>
diff --git a/app/views/checkouts/index.html.erb b/app/views/checkouts/index.html.erb index 8854cd3ed4..fdfc194409 100644 --- a/app/views/checkouts/index.html.erb +++ b/app/views/checkouts/index.html.erb @@ -18,7 +18,9 @@
@@ -35,9 +37,11 @@ diff --git a/config/routes.rb b/config/routes.rb index f956f8870b..3a4a6596c1 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -26,7 +26,15 @@ end end - resources :checkouts, only: [:index, :create, :show, :update, :destroy] + resources :checkouts, only: [:index, :create, :show, :update, :destroy] do + collection do + patch :return_all + end + + member do + patch :return + end + end resources :bookmarks do concerns :exportable diff --git a/spec/requests/checkouts_spec.rb b/spec/requests/checkouts_spec.rb index 18c733b3f2..ae8436d928 100644 --- a/spec/requests/checkouts_spec.rb +++ b/spec/requests/checkouts_spec.rb @@ -118,4 +118,52 @@ expect(response).to redirect_to(checkouts_url) end end + + describe "PATCH /return" do + it "updates the return time of requested checkout" do + patch return_checkout_url(checkout) + checkout.reload + expect(checkout.return_time).to be <= DateTime.current + end + it "redirects to the checkouts list" do + patch return_checkout_url(checkout) + expect(response).to redirect_to(checkouts_url) + end + end + + describe "PATCH /return_all" do + before :each do + FactoryBot.reload + FactoryBot.create_list(:checkout, 2) + FactoryBot.create(:checkout, user: user) + end + + context "as a regular user" do + it "updates the user's active checkouts" do + patch return_all_checkouts_url + expect(Checkout.where(user_id: user.id).first.return_time).to be <= DateTime.current + end + it "does not update other checkouts" do + patch return_all_checkouts_url + expect(Checkout.where.not(user_id: user.id).first.return_time).to be > DateTime.current + expect(Checkout.where.not(user_id: user.id).second.return_time).to be > DateTime.current + end + it "redirects to the checkouts list" do + patch return_all_checkouts_url + expect(response).to redirect_to(checkouts_url) + end + end + context "as an admin user" do + let(:user) { FactoryBot.create(:admin) } + it "updates all active checkouts" do + patch return_all_checkouts_url + sleep 1 + expect(Checkout.where("return_time <= '#{DateTime.current}'").count).to eq(3) + end + it "redirects to the checkouts list" do + patch return_all_checkouts_url + expect(response).to redirect_to(checkouts_url) + end + end + end end diff --git a/spec/routing/checkouts_routing_spec.rb b/spec/routing/checkouts_routing_spec.rb index f6aada1417..31860952cb 100644 --- a/spec/routing/checkouts_routing_spec.rb +++ b/spec/routing/checkouts_routing_spec.rb @@ -25,5 +25,13 @@ it "routes to #destroy" do expect(delete: "/checkouts/1").to route_to("checkouts#destroy", id: "1") end + + it "routes to #return_all" do + expect(patch: "/checkouts/return_all").to route_to("checkouts#return_all") + end + + it "routes to #return" do + expect(patch: "/checkouts/1/return").to route_to("checkouts#return", id: "1") + end end end From c67f73f1e12a3a22fb49ea4f04bf46d5db420ab0 Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Fri, 17 Jun 2022 17:26:53 -0400 Subject: [PATCH 016/230] Change the redirect behavior when returning items --- app/controllers/checkouts_controller.rb | 2 +- spec/requests/checkouts_spec.rb | 21 +++++++++++++++++---- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/app/controllers/checkouts_controller.rb b/app/controllers/checkouts_controller.rb index 80c3e3b8a0..2740e93ab8 100644 --- a/app/controllers/checkouts_controller.rb +++ b/app/controllers/checkouts_controller.rb @@ -43,7 +43,7 @@ def return @checkout.update(return_time: DateTime.current) respond_to do |format| - format.html { redirect_to checkouts_url, notice: "Checkout was successfully returned." } + format.html { redirect_back fallback_location: checkouts_url, notice: "Checkout was successfully returned." } format.json { head :no_content } end end diff --git a/spec/requests/checkouts_spec.rb b/spec/requests/checkouts_spec.rb index ae8436d928..0c8d48572a 100644 --- a/spec/requests/checkouts_spec.rb +++ b/spec/requests/checkouts_spec.rb @@ -125,15 +125,28 @@ checkout.reload expect(checkout.return_time).to be <= DateTime.current end - it "redirects to the checkouts list" do - patch return_checkout_url(checkout) - expect(response).to redirect_to(checkouts_url) + context "user is on the checkouts page" do + it "redirects to the checkouts list" do + patch return_checkout_url(checkout), headers: { "HTTP_REFERER" => checkouts_url } + expect(response).to redirect_to(checkouts_url) + end + end + context "user is on the item view page" do + it "redirects to the item view page" do + patch return_checkout_url(checkout), headers: { "HTTP_REFERER" => media_object_url(checkout.media_object)} + expect(response).to redirect_to(media_object_url(checkout.media_object)) + end + end + context "the http referrer fails" do + it "redirects to the checkouts page" do + patch return_checkout_url(checkout) + expect(response).to redirect_to(checkouts_url) + end end end describe "PATCH /return_all" do before :each do - FactoryBot.reload FactoryBot.create_list(:checkout, 2) FactoryBot.create(:checkout, user: user) end From 3203b74c020c03f9c2a33b09d484e5ffff64f8a0 Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Fri, 17 Jun 2022 17:40:18 -0400 Subject: [PATCH 017/230] Remove sleep from checkouts spec --- spec/requests/checkouts_spec.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/spec/requests/checkouts_spec.rb b/spec/requests/checkouts_spec.rb index 0c8d48572a..e63cdca1b3 100644 --- a/spec/requests/checkouts_spec.rb +++ b/spec/requests/checkouts_spec.rb @@ -170,8 +170,7 @@ let(:user) { FactoryBot.create(:admin) } it "updates all active checkouts" do patch return_all_checkouts_url - sleep 1 - expect(Checkout.where("return_time <= '#{DateTime.current}'").count).to eq(3) + expect(Checkout.where("return_time <= ?", DateTime.current).count).to eq(3) end it "redirects to the checkouts list" do patch return_all_checkouts_url From 3d5cb96e5d44462d905c9e34d6c4b7628d8509e9 Mon Sep 17 00:00:00 2001 From: dananji Date: Wed, 8 Jun 2022 16:34:47 -0400 Subject: [PATCH 018/230] [WIP] Add Return/Chek Out buttons to view page --- app/assets/stylesheets/avalon/_buttons.scss | 2 +- app/controllers/checkouts_controller.rb | 6 +- app/models/media_object.rb | 5 ++ app/views/media_objects/_checkout.html.erb | 69 +++++++++++++++++++++ app/views/media_objects/_item_view.html.erb | 1 + 5 files changed, 79 insertions(+), 4 deletions(-) create mode 100644 app/views/media_objects/_checkout.html.erb diff --git a/app/assets/stylesheets/avalon/_buttons.scss b/app/assets/stylesheets/avalon/_buttons.scss index 9ff8d7c324..bd4f65849d 100644 --- a/app/assets/stylesheets/avalon/_buttons.scss +++ b/app/assets/stylesheets/avalon/_buttons.scss @@ -26,7 +26,7 @@ button.close { color: $white; } -#share-button { +#share-button, #return-btn, #checkout-btn { text-align: right; margin-top: 10px; margin-bottom: 0; diff --git a/app/controllers/checkouts_controller.rb b/app/controllers/checkouts_controller.rb index 2740e93ab8..e9d53b88c4 100644 --- a/app/controllers/checkouts_controller.rb +++ b/app/controllers/checkouts_controller.rb @@ -19,7 +19,7 @@ def create respond_to do |format| # TODO: move this can? check into a checkout ability (can?(:create, @checkout)) if can?(:create, @checkout) && @checkout.save - format.json { render :show, status: :created, location: @checkout } + format.json { redirect_to media_object_path, status: :created, location: @checkout } else format.json { render json: @checkout.errors, status: :unprocessable_entity } end @@ -62,10 +62,10 @@ def return_all # DELETE /checkouts/1 or /checkouts/1.json def destroy @checkout.destroy - + flash[:notice] = "Checkout was successfully destroyed." respond_to do |format| format.html { redirect_to checkouts_url, notice: "Checkout was successfully destroyed." } - format.json { head :no_content } + format.json { render json:flash[:notice] } end end diff --git a/app/models/media_object.rb b/app/models/media_object.rb index 90eb5c22b7..c56c1fd77c 100644 --- a/app/models/media_object.rb +++ b/app/models/media_object.rb @@ -371,6 +371,11 @@ def lending_status Checkout.active_for_media_object(id).any? ? "checked_out" : "available" end + def current_checkout(user_id) + checkouts = Checkout.active_for(id) + checkouts.select{ |ch| ch.user_id == user_id }.first + end + private def calculate_duration diff --git a/app/views/media_objects/_checkout.html.erb b/app/views/media_objects/_checkout.html.erb new file mode 100644 index 0000000000..4b5fd8e2c4 --- /dev/null +++ b/app/views/media_objects/_checkout.html.erb @@ -0,0 +1,69 @@ +<%# +Copyright 2011-2022, The Trustees of Indiana University and Northwestern + University. Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed + under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + CONDITIONS OF ANY KIND, either express or implied. See the License for the + specific language governing permissions and limitations under the License. +--- END LICENSE_HEADER BLOCK --- +%> +<% if can? :read, @media_object %> + <% if @media_object.lending_status == "checked_out" %> + <%= button_tag "Return Item", id: "return-btn", rel: "popover", class: "btn btn-danger pull-right", data: { checkout_id: @media_object.current_checkout(current_user.id).id } %> + <% elsif @media_object.lending_status == "available" %> +
+ /> + + + <% end %> +<% end %> + +<% content_for :page_scripts do %> + +<% end %> \ No newline at end of file diff --git a/app/views/media_objects/_item_view.html.erb b/app/views/media_objects/_item_view.html.erb index 229a08be3e..f7a8be120f 100644 --- a/app/views/media_objects/_item_view.html.erb +++ b/app/views/media_objects/_item_view.html.erb @@ -34,6 +34,7 @@ Unless required by applicable law or agreed to in writing, software distributed <%= render 'workflow_progress' %> <%= render partial: 'timeline' if current_ability.can? :create, Timeline %> <%= render 'share' if will_partial_list_render? :share %> + <%= render 'checkout' %> <%= render partial: 'sections', From ae9f31e0de70d3341a7e684b68a9dad79ec59f1e Mon Sep 17 00:00:00 2001 From: dananji Date: Fri, 10 Jun 2022 16:59:09 -0400 Subject: [PATCH 019/230] Show remaining time when checked out --- app/assets/stylesheets/avalon.scss | 27 ++++- app/assets/stylesheets/avalon/_buttons.scss | 2 +- app/views/media_objects/_checkout.html.erb | 105 ++++++++++++++++++-- 3 files changed, 123 insertions(+), 11 deletions(-) diff --git a/app/assets/stylesheets/avalon.scss b/app/assets/stylesheets/avalon.scss index e667597bce..a4aab06b24 100644 --- a/app/assets/stylesheets/avalon.scss +++ b/app/assets/stylesheets/avalon.scss @@ -1166,4 +1166,29 @@ td { */ .irmp--transcript_nav { padding: 10px 0 0 0; -} \ No newline at end of file +} + +/* CDL controls on view page styles */ +.cdl-controls { + display: inline-flex; + margin-top: 10px; + + .remaining-time { + display: flex; + text-align: center; + } + + .remaining-time p { + margin-top: 5px; + } + + .remaining-time span{ + color: #fff; + margin-right: 3px; + padding: 2px 5px; + border-radius: 3px; + background: $primary; + font-size: x-small; + } +} +/* End of CDL controls on view page styles */ diff --git a/app/assets/stylesheets/avalon/_buttons.scss b/app/assets/stylesheets/avalon/_buttons.scss index bd4f65849d..9ff8d7c324 100644 --- a/app/assets/stylesheets/avalon/_buttons.scss +++ b/app/assets/stylesheets/avalon/_buttons.scss @@ -26,7 +26,7 @@ button.close { color: $white; } -#share-button, #return-btn, #checkout-btn { +#share-button { text-align: right; margin-top: 10px; margin-bottom: 0; diff --git a/app/views/media_objects/_checkout.html.erb b/app/views/media_objects/_checkout.html.erb index 4b5fd8e2c4..3cc4498f6b 100644 --- a/app/views/media_objects/_checkout.html.erb +++ b/app/views/media_objects/_checkout.html.erb @@ -14,19 +14,106 @@ Unless required by applicable law or agreed to in writing, software distributed --- END LICENSE_HEADER BLOCK --- %> <% if can? :read, @media_object %> - <% if @media_object.lending_status == "checked_out" %> - <%= button_tag "Return Item", id: "return-btn", rel: "popover", class: "btn btn-danger pull-right", data: { checkout_id: @media_object.current_checkout(current_user.id).id } %> - <% elsif @media_object.lending_status == "available" %> -
- /> - - - <% end %> +
+ <% current_checkout=@media_object.current_checkout(current_user.id) %> + <% media_object_id=@media_object.id %> + <% if @media_object.lending_status == "checked_out" && !current_checkout.nil? %> + + <%= button_tag "Return Item", id: "return-btn", rel: "popover", class: "btn btn-danger pull-right", + data: { checkout_id: current_checkout.id, checkout_returntime: current_checkout.return_time } %> + <% elsif @media_object.lending_status == "available" %> + <%= form_for(Checkout.new, remote: true, format: 'json', html: { style: "display: inline;" }) do |f| %> + <%= hidden_field_tag "checkout[media_object_id]", media_object_id %> + <%= f.submit "Check Out", class: "btn btn-primary pull-right", id: "checkout-btn" %> + <% end %> + <% end %> +
<% end %> + + <% content_for :page_scripts do %> +<% media_object_id=@media_object.id %> +<%= form_for(Checkout.new, remote: true, format: 'json', html: { style: "display: inline;" }) do |f| %> + <%= hidden_field_tag "checkout[media_object_id]", media_object_id %> + <%= f.submit "Check Out", class: "btn btn-secondary" %> <% end %> \ No newline at end of file diff --git a/app/views/media_objects/_destroy_checkout.html.erb b/app/views/media_objects/_destroy_checkout.html.erb new file mode 100644 index 0000000000..ad8024321c --- /dev/null +++ b/app/views/media_objects/_destroy_checkout.html.erb @@ -0,0 +1,138 @@ +<%# +Copyright 2011-2022, The Trustees of Indiana University and Northwestern + University. Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed + under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + CONDITIONS OF ANY KIND, either express or implied. See the License for the + specific language governing permissions and limitations under the License. +--- END LICENSE_HEADER BLOCK --- +%> +<% if can? :read, @media_object %> +
+ <% current_checkout=@media_object.current_checkout(current_user.id) %> + <% if @media_object.lending_status == "checked_out" && !current_checkout.nil? %> + <%= button_tag "Return Item", id: "return-btn", rel: "popover", class: "btn btn-danger", + data: { checkout_id: current_checkout.id, checkout_returntime: current_checkout.return_time } %> + <% end %> + +
+<% end %> + + + +<% content_for :page_scripts do %> + +<% end %> \ No newline at end of file diff --git a/app/views/media_objects/_embed_checkout.html.erb b/app/views/media_objects/_embed_checkout.html.erb new file mode 100644 index 0000000000..5d2520e84b --- /dev/null +++ b/app/views/media_objects/_embed_checkout.html.erb @@ -0,0 +1,25 @@ +<%# +Copyright 2011-2022, The Trustees of Indiana University and Northwestern + University. Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed + under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + CONDITIONS OF ANY KIND, either express or implied. See the License for the + specific language governing permissions and limitations under the License. +--- END LICENSE_HEADER BLOCK --- +%> + <% master_file=@media_object.master_files.first%> +
+ <% if master_file.is_video? %> + <%=t('media_object.cdl.video_checkout').html_safe%> + <%= render "checkout" %> + <% else %> + <%=t('media_object.cdl.audio_checkout').html_safe%> + <%= render "checkout" %> + <% end %> +
\ No newline at end of file diff --git a/app/views/media_objects/_item_view.html.erb b/app/views/media_objects/_item_view.html.erb index f7a8be120f..c21c324a38 100644 --- a/app/views/media_objects/_item_view.html.erb +++ b/app/views/media_objects/_item_view.html.erb @@ -26,7 +26,6 @@ Unless required by applicable law or agreed to in writing, software distributed
<%= render partial: "modules/player/section", locals: {section: @currentStream, section_info: @currentStreamInfo, f_start: @f_start, f_end: @f_end} %> - <%= render file: '_track_scrubber.html.erb' if is_mejs_2? %> <%= render file: '_add_to_playlist.html.erb' if current_user.present? && is_mejs_2? %> <%# Partial view for MEJS4 Add To Playlist plugin %> @@ -34,13 +33,12 @@ Unless required by applicable law or agreed to in writing, software distributed <%= render 'workflow_progress' %> <%= render partial: 'timeline' if current_ability.can? :create, Timeline %> <%= render 'share' if will_partial_list_render? :share %> - <%= render 'checkout' %> <%= render partial: 'sections', locals: { mediaobject: @media_object, - sections: @masterFiles, - activeStream: @currentStream } %> + sections: @masterFiles, + activeStream: @currentStream } %>
diff --git a/app/views/media_objects/show.html.erb b/app/views/media_objects/show.html.erb index 09efc2de56..7747d6e2c8 100644 --- a/app/views/media_objects/show.html.erb +++ b/app/views/media_objects/show.html.erb @@ -26,5 +26,9 @@ Unless required by applicable law or agreed to in writing, software distributed
-<%= render 'administrative_links' %> -<%= render 'item_view' %> +<% if @media_object.lending_status == "available" %> + <%= render 'embed_checkout' %> +<% else %> + <%= render 'administrative_links' %> + <%= render 'item_view' %> +<% end %> diff --git a/config/locales/en.yml b/config/locales/en.yml index 348e0f11b2..37e685b0f1 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -22,6 +22,9 @@ en: empty_share_link: "" empty_share_link_notice: "After processing has started the embedded link will be available." empty_share_section_permalink_notice: "After processing has started the section link will be available." + cdl: + video_checkout: "

To enable streaming please check out resource. This video content is available to be checked out.

" + audio_checkout: "

To enable streaming please check out resource. This audio content is available to be checked out.

" metadata_tip: abstract: | Summary provides a space for describing the contents of the item. Examples diff --git a/spec/controllers/media_objects_controller_spec.rb b/spec/controllers/media_objects_controller_spec.rb index 928bd16db5..f52c113b36 100644 --- a/spec/controllers/media_objects_controller_spec.rb +++ b/spec/controllers/media_objects_controller_spec.rb @@ -674,7 +674,7 @@ end describe "#show" do - let!(:media_object) { FactoryBot.create(:published_media_object, visibility: 'public') } + let!(:media_object) { FactoryBot.create(:published_media_object, :with_checkout, visibility: 'public') } context "Known items should be retrievable" do context 'with fedora 3 pid' do diff --git a/spec/factories/media_objects.rb b/spec/factories/media_objects.rb index 860326f2c3..b6c261bc9e 100644 --- a/spec/factories/media_objects.rb +++ b/spec/factories/media_objects.rb @@ -74,5 +74,10 @@ mo.save end end + trait :with_checkout do + after(:create) do |mo| + mo.current_checkout = FactoryBot.create(:checkout) + mo.save + end end end From 75b1da3b870ce78cffe1856d84a73098a204fdbe Mon Sep 17 00:00:00 2001 From: dananji Date: Tue, 14 Jun 2022 14:33:22 -0400 Subject: [PATCH 021/230] Fix checkout form and flash message --- app/assets/stylesheets/avalon.scss | 1 - app/controllers/checkouts_controller.rb | 3 ++- app/views/media_objects/_checkout.html.erb | 3 ++- .../media_objects/_destroy_checkout.html.erb | 25 +++++++++---------- .../media_objects/_embed_checkout.html.erb | 2 +- config/locales/en.yml | 3 +++ .../media_objects_controller_spec.rb | 2 +- spec/factories/media_objects.rb | 5 ---- 8 files changed, 21 insertions(+), 23 deletions(-) diff --git a/app/assets/stylesheets/avalon.scss b/app/assets/stylesheets/avalon.scss index c37adb6e20..6fd8f528c9 100644 --- a/app/assets/stylesheets/avalon.scss +++ b/app/assets/stylesheets/avalon.scss @@ -1216,5 +1216,4 @@ td { padding: 3rem; height: 50%; } - /* End of CDL controls on view page styles */ diff --git a/app/controllers/checkouts_controller.rb b/app/controllers/checkouts_controller.rb index a09900dbf8..a505c731ef 100644 --- a/app/controllers/checkouts_controller.rb +++ b/app/controllers/checkouts_controller.rb @@ -19,6 +19,7 @@ def create respond_to do |format| # TODO: move this can? check into a checkout ability (can?(:create, @checkout)) if can?(:create, @checkout) && @checkout.save + format.html { redirect_to media_object_path(checkout_params[:media_object_id]), flash: { success: "Checkout was successfully created."} } format.json { render :show, status: :created, location: @checkout } else format.json { render json: @checkout.errors, status: :unprocessable_entity } @@ -64,7 +65,7 @@ def destroy @checkout.destroy flash[:notice] = "Checkout was successfully destroyed." respond_to do |format| - format.html { redirect_to checkouts_url, notice: "Checkout was successfully destroyed." } + format.html { redirect_to checkouts_url, flash: flash } format.json { render json:flash[:notice] } end end diff --git a/app/views/media_objects/_checkout.html.erb b/app/views/media_objects/_checkout.html.erb index f9cee9a734..e259457855 100644 --- a/app/views/media_objects/_checkout.html.erb +++ b/app/views/media_objects/_checkout.html.erb @@ -14,7 +14,8 @@ Unless required by applicable law or agreed to in writing, software distributed --- END LICENSE_HEADER BLOCK --- %> <% media_object_id=@media_object.id %> -<%= form_for(Checkout.new, remote: true, format: 'json', html: { style: "display: inline;" }) do |f| %> +<%= form_for(Checkout.new, html: { style: "display: inline;" }) do |f| %> + <%= hidden_field_tag "authenticity_token", form_authenticity_token %> <%= hidden_field_tag "checkout[media_object_id]", media_object_id %> <%= f.submit "Check Out", class: "btn btn-secondary" %> <% end %> \ No newline at end of file diff --git a/app/views/media_objects/_destroy_checkout.html.erb b/app/views/media_objects/_destroy_checkout.html.erb index ad8024321c..b45a3d4639 100644 --- a/app/views/media_objects/_destroy_checkout.html.erb +++ b/app/views/media_objects/_destroy_checkout.html.erb @@ -19,28 +19,26 @@ Unless required by applicable law or agreed to in writing, software distributed <% if @media_object.lending_status == "checked_out" && !current_checkout.nil? %> <%= button_tag "Return Item", id: "return-btn", rel: "popover", class: "btn btn-danger", data: { checkout_id: current_checkout.id, checkout_returntime: current_checkout.return_time } %> - <% end %> - <% end %> -
<% end %> - - + + <% end %> - - + + - + <% if User.column_names.include? 'provider' %> diff --git a/spec/controllers/samvera/persona/user_controller_spec.rb b/spec/controllers/samvera/persona/user_controller_spec.rb index bcce0d4e80..d8fb1f907b 100644 --- a/spec/controllers/samvera/persona/user_controller_spec.rb +++ b/spec/controllers/samvera/persona/user_controller_spec.rb @@ -57,49 +57,26 @@ end context 'filtering' do - let(:common_params) { { start: 0, length: 20, order: { '0': { column: 0, dir: 'asc' } } } } - it "returns results filtered by username" do - post :paged_index, format: 'json', params: common_params.merge(search: { value: user.username }) - parsed_response = JSON.parse(response.body) - expect(parsed_response['recordsFiltered']).to eq(1) - expect(parsed_response['data'].count).to eq(1) - expect(parsed_response['data'][0][0]).to eq("#{user.username}") - end - - let(:common_params) { { start: 0, length: 20, order: { '0': { column: 'entry', dir: 'asc' } } } } - it "returns results filtered by email" do - post :paged_index, format: 'json', params: common_params.merge(search: { value: 'zzzebra@example.edu' }) - parsed_response = JSON.parse(response.body) - expect(parsed_response['recordsFiltered']).to eq(1) - expect(parsed_response['data'].count).to eq(1) - expect(parsed_response['data'][0][1]).to eq("zzzebra@example.edu") - end - - let(:common_params) { { start: 0, length: 20, order: { '0': { column: 0, dir: 'asc' } } } } - it "returns results filtered by role" do - post :paged_index, format: 'json', params: common_params.merge( { search: { value: 'administrator' } } ) - parsed_response = JSON.parse(response.body) - expect(parsed_response['recordsFiltered']).to eq(1) - expect(parsed_response['data'].count).to eq(1) - expect(parsed_response['data'][0][0]).to eq("aardvark") - end - - let(:common_params) { { start: 0, length: 20, order: { '0': { column: 0, dir: 'asc' } } } } - it "returns results filtered by date" do - post :paged_index, format: 'json', params: common_params.merge( { search: { value: 'May' } } ) - parsed_response = JSON.parse(response.body) - expect(parsed_response['recordsFiltered']).to eq(2) - expect(parsed_response['data'].count).to eq(2) - expect(parsed_response['data'][0][3]).to eq("May 15th, 2022 00:00") - end - - let(:common_params) { { start: 0, length: 20, order: { '0': { column: 0, dir: 'asc' } } } } - it "returns results filtered by status" do - post :paged_index, format: 'json', params: common_params.merge( { search: { value: 'Pending' } } ) - parsed_response = JSON.parse(response.body) - expect(parsed_response['recordsFiltered']).to eq(1) - expect(parsed_response['data'].count).to eq(1) - expect(parsed_response['data'][0][0]).to eq("zzzebra") + context 'username' do + let(:common_params) { { start: 0, length: 20, order: { '0': { column: 0, dir: 'asc' } } } } + it "returns results filtered by username" do + post :paged_index, format: 'json', params: common_params.merge(search: { value: user.username }) + parsed_response = JSON.parse(response.body) + expect(parsed_response['recordsFiltered']).to eq(1) + expect(parsed_response['data'].count).to eq(1) + expect(parsed_response['data'][0][0]).to eq("#{user.username}") + end + end + + context 'email' do + let(:common_params) { { start: 0, length: 20, order: { '0': { column: 'entry', dir: 'asc' } } } } + it "returns results filtered by email" do + post :paged_index, format: 'json', params: common_params.merge(search: { value: 'zzzebra@example.edu' }) + parsed_response = JSON.parse(response.body) + expect(parsed_response['recordsFiltered']).to eq(1) + expect(parsed_response['data'].count).to eq(1) + expect(parsed_response['data'][0][1]).to eq("zzzebra@example.edu") + end end end @@ -118,20 +95,6 @@ expect(parsed_response['data'][0][0]).to eq("zzzebra") expect(parsed_response['data'][11][0]).to eq("aardvark") end - - it "returns results sorted by role ascending" do - post :paged_index, format: 'json', params: common_params.merge(order: { '0': { column: 2, dir: 'asc' } }) - parsed_response = JSON.parse(response.body) - expect(parsed_response['data'][0][2]).to eq("
  • administrator
") - expect(parsed_response['data'][11][2]).to eq("
    ") - end - - it "returns results sorted by role descending" do - post :paged_index, format: 'json', params: common_params.merge(order: { '0': { column: 2, dir: 'desc' } }) - parsed_response = JSON.parse(response.body) - expect(parsed_response['data'][0][2]).to eq("
      ") - expect(parsed_response['data'][11][2]).to eq("
      • administrator
      ") - end end end From afe7eab0b29ff9a63c3dfc0258350bf2cdd02262 Mon Sep 17 00:00:00 2001 From: dananji Date: Mon, 29 Aug 2022 08:30:45 -0700 Subject: [PATCH 118/230] Rename variables and function --- app/javascript/packs/iiif-timeliner.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/app/javascript/packs/iiif-timeliner.js b/app/javascript/packs/iiif-timeliner.js index e24bb23354..40f56edae5 100644 --- a/app/javascript/packs/iiif-timeliner.js +++ b/app/javascript/packs/iiif-timeliner.js @@ -10175,11 +10175,11 @@ import "../iiif-timeliner-styles.css" /*!********************************!*\ !*** ./src/actions/project.js ***! \********************************/ - /*! exports provided: updateSettings, setLanguage, setTitle, setDescription, resetDocument, importDocument, importError, exportDocument, loadProject, saveProject, setcolourPalette, clearCustomColors, setProjectStatus */ + /*! exports provided: updateSettings, setLanguage, setTitle, setDescription, resetDocument, importDocument, importError, exportDocument, loadProject, saveProject, setcolourPalette, clearCustomColors, setProjectChanged */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; - eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"updateSettings\", function() { return updateSettings; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"setLanguage\", function() { return setLanguage; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"setTitle\", function() { return setTitle; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"setDescription\", function() { return setDescription; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"resetDocument\", function() { return resetDocument; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"importDocument\", function() { return importDocument; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"importError\", function() { return importError; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"exportDocument\", function() { return exportDocument; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"loadProject\", function() { return loadProject; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"saveProject\", function() { return saveProject; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"setcolourPalette\", function() { return setcolourPalette; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"clearCustomColors\", function() { return clearCustomColors; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"setProjectStatus\", function() { return setProjectStatus; });\n/* harmony import */ var _constants_project__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../constants/project */ \"./src/constants/project.js\");\n\nvar updateSettings = function updateSettings(form) {\n return {\n type: _constants_project__WEBPACK_IMPORTED_MODULE_0__[\"UPDATE_SETTINGS\"],\n payload: form\n };\n};\nvar setLanguage = function setLanguage(language) {\n return {\n type: _constants_project__WEBPACK_IMPORTED_MODULE_0__[\"SET_LANGUAGE\"],\n payload: {\n language: language\n }\n };\n};\nvar setTitle = function setTitle(title) {\n return {\n type: _constants_project__WEBPACK_IMPORTED_MODULE_0__[\"SET_TITLE\"],\n payload: {\n title: title\n }\n };\n};\nvar setDescription = function setDescription(description) {\n return {\n type: _constants_project__WEBPACK_IMPORTED_MODULE_0__[\"SET_DESCRIPTION\"],\n payload: {\n description: description\n }\n };\n};\nvar resetDocument = function resetDocument() {\n return {\n type: _constants_project__WEBPACK_IMPORTED_MODULE_0__[\"RESET_DOCUMENT\"]\n };\n};\nvar importDocument = function importDocument(manifest, source) {\n return {\n type: _constants_project__WEBPACK_IMPORTED_MODULE_0__[\"IMPORT_DOCUMENT\"],\n manifest: manifest,\n source: source\n };\n};\nvar importError = function importError(error) {\n return {\n type: _constants_project__WEBPACK_IMPORTED_MODULE_0__[\"IMPORT_ERROR\"],\n payload: {\n error: error.toString()\n }\n };\n};\nvar exportDocument = function exportDocument() {\n return {\n type: _constants_project__WEBPACK_IMPORTED_MODULE_0__[\"EXPORT_DOCUMENT\"]\n };\n};\nvar loadProject = function loadProject(state) {\n return {\n type: _constants_project__WEBPACK_IMPORTED_MODULE_0__[\"LOAD_PROJECT\"],\n state: state\n };\n};\nvar saveProject = function saveProject() {\n return {\n type: _constants_project__WEBPACK_IMPORTED_MODULE_0__[\"SAVE_PROJECT\"]\n };\n};\nvar setcolourPalette = function setcolourPalette(pallet) {\n return {\n type: _constants_project__WEBPACK_IMPORTED_MODULE_0__[\"SET_COLOUR_PALETTE\"],\n payload: {\n pallet: pallet\n }\n };\n};\nvar clearCustomColors = function clearCustomColors() {\n return {\n type: _constants_project__WEBPACK_IMPORTED_MODULE_0__[\"CLEAR_CUSTOM_COLORS\"]\n };\n};\nvar setProjectStatus = function setProjectStatus(isSaved) {\n return {\n type: _constants_project__WEBPACK_IMPORTED_MODULE_0__[\"SET_IS_SAVED\"],\n payload: {\n isSaved: isSaved\n }\n };\n};\n\n//# sourceURL=webpack:///./src/actions/project.js?"); + eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"updateSettings\", function() { return updateSettings; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"setLanguage\", function() { return setLanguage; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"setTitle\", function() { return setTitle; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"setDescription\", function() { return setDescription; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"resetDocument\", function() { return resetDocument; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"importDocument\", function() { return importDocument; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"importError\", function() { return importError; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"exportDocument\", function() { return exportDocument; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"loadProject\", function() { return loadProject; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"saveProject\", function() { return saveProject; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"setcolourPalette\", function() { return setcolourPalette; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"clearCustomColors\", function() { return clearCustomColors; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"setProjectChanged\", function() { return setProjectChanged; });\n/* harmony import */ var _constants_project__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../constants/project */ \"./src/constants/project.js\");\n\nvar updateSettings = function updateSettings(form) {\n return {\n type: _constants_project__WEBPACK_IMPORTED_MODULE_0__[\"UPDATE_SETTINGS\"],\n payload: form\n };\n};\nvar setLanguage = function setLanguage(language) {\n return {\n type: _constants_project__WEBPACK_IMPORTED_MODULE_0__[\"SET_LANGUAGE\"],\n payload: {\n language: language\n }\n };\n};\nvar setTitle = function setTitle(title) {\n return {\n type: _constants_project__WEBPACK_IMPORTED_MODULE_0__[\"SET_TITLE\"],\n payload: {\n title: title\n }\n };\n};\nvar setDescription = function setDescription(description) {\n return {\n type: _constants_project__WEBPACK_IMPORTED_MODULE_0__[\"SET_DESCRIPTION\"],\n payload: {\n description: description\n }\n };\n};\nvar resetDocument = function resetDocument() {\n return {\n type: _constants_project__WEBPACK_IMPORTED_MODULE_0__[\"RESET_DOCUMENT\"]\n };\n};\nvar importDocument = function importDocument(manifest, source) {\n return {\n type: _constants_project__WEBPACK_IMPORTED_MODULE_0__[\"IMPORT_DOCUMENT\"],\n manifest: manifest,\n source: source\n };\n};\nvar importError = function importError(error) {\n return {\n type: _constants_project__WEBPACK_IMPORTED_MODULE_0__[\"IMPORT_ERROR\"],\n payload: {\n error: error.toString()\n }\n };\n};\nvar exportDocument = function exportDocument() {\n return {\n type: _constants_project__WEBPACK_IMPORTED_MODULE_0__[\"EXPORT_DOCUMENT\"]\n };\n};\nvar loadProject = function loadProject(state) {\n return {\n type: _constants_project__WEBPACK_IMPORTED_MODULE_0__[\"LOAD_PROJECT\"],\n state: state\n };\n};\nvar saveProject = function saveProject() {\n return {\n type: _constants_project__WEBPACK_IMPORTED_MODULE_0__[\"SAVE_PROJECT\"]\n };\n};\nvar setcolourPalette = function setcolourPalette(pallet) {\n return {\n type: _constants_project__WEBPACK_IMPORTED_MODULE_0__[\"SET_COLOUR_PALETTE\"],\n payload: {\n pallet: pallet\n }\n };\n};\nvar clearCustomColors = function clearCustomColors() {\n return {\n type: _constants_project__WEBPACK_IMPORTED_MODULE_0__[\"CLEAR_CUSTOM_COLORS\"]\n };\n};\nvar setProjectChanged = function setProjectChanged(isSaved) {\n return {\n type: _constants_project__WEBPACK_IMPORTED_MODULE_0__[\"PROJECT_CHANGED\"],\n payload: {\n isSaved: isSaved\n }\n };\n};\n\n//# sourceURL=webpack:///./src/actions/project.js?"); /***/ }), @@ -10865,11 +10865,11 @@ import "../iiif-timeliner-styles.css" /*!**********************************!*\ !*** ./src/constants/project.js ***! \**********************************/ - /*! exports provided: BUBBLE_STYLES, PROJECT, RDF_NAMESPACE, PROJECT_SETTINGS_KEYS, SETTINGS_ATTRIBUTE, DEFAULT_SETTINGS, DEFAULT_PROJECT_STATE, UPDATE_SETTINGS, SET_LANGUAGE, SET_TITLE, SET_DESCRIPTION, RESET_DOCUMENT, EXPORT_DOCUMENT, IMPORT_DOCUMENT, LOAD_PROJECT, IMPORT_ERROR, SAVE_PROJECT, SET_COLOUR_PALETTE, CLEAR_CUSTOM_COLORS, SET_IS_SAVED */ + /*! exports provided: BUBBLE_STYLES, PROJECT, RDF_NAMESPACE, PROJECT_SETTINGS_KEYS, SETTINGS_ATTRIBUTE, DEFAULT_SETTINGS, DEFAULT_PROJECT_STATE, UPDATE_SETTINGS, SET_LANGUAGE, SET_TITLE, SET_DESCRIPTION, RESET_DOCUMENT, EXPORT_DOCUMENT, IMPORT_DOCUMENT, LOAD_PROJECT, IMPORT_ERROR, SAVE_PROJECT, SET_COLOUR_PALETTE, CLEAR_CUSTOM_COLORS, PROJECT_CHANGED */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; - eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"BUBBLE_STYLES\", function() { return BUBBLE_STYLES; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"PROJECT\", function() { return PROJECT; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"RDF_NAMESPACE\", function() { return RDF_NAMESPACE; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"PROJECT_SETTINGS_KEYS\", function() { return PROJECT_SETTINGS_KEYS; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"SETTINGS_ATTRIBUTE\", function() { return SETTINGS_ATTRIBUTE; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"DEFAULT_SETTINGS\", function() { return DEFAULT_SETTINGS; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"DEFAULT_PROJECT_STATE\", function() { return DEFAULT_PROJECT_STATE; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"UPDATE_SETTINGS\", function() { return UPDATE_SETTINGS; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"SET_LANGUAGE\", function() { return SET_LANGUAGE; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"SET_TITLE\", function() { return SET_TITLE; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"SET_DESCRIPTION\", function() { return SET_DESCRIPTION; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"RESET_DOCUMENT\", function() { return RESET_DOCUMENT; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"EXPORT_DOCUMENT\", function() { return EXPORT_DOCUMENT; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"IMPORT_DOCUMENT\", function() { return IMPORT_DOCUMENT; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"LOAD_PROJECT\", function() { return LOAD_PROJECT; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"IMPORT_ERROR\", function() { return IMPORT_ERROR; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"SAVE_PROJECT\", function() { return SAVE_PROJECT; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"SET_COLOUR_PALETTE\", function() { return SET_COLOUR_PALETTE; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"CLEAR_CUSTOM_COLORS\", function() { return CLEAR_CUSTOM_COLORS; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"SET_IS_SAVED\", function() { return SET_IS_SAVED; });\n/* harmony import */ var _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_objectSpread__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./node_modules/babel-preset-react-app/node_modules/@babel/runtime/helpers/esm/objectSpread */ \"./node_modules/babel-preset-react-app/node_modules/@babel/runtime/helpers/esm/objectSpread.js\");\n/* harmony import */ var _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./node_modules/babel-preset-react-app/node_modules/@babel/runtime/helpers/esm/defineProperty */ \"./node_modules/babel-preset-react-app/node_modules/@babel/runtime/helpers/esm/defineProperty.js\");\n\n\n\nvar _DEFAULT_SETTINGS, _objectSpread2;\n\nvar BUBBLE_STYLES = {\n SQUARE: 'square',\n ROUNDED: 'rounded'\n};\nvar DEFAULT_BUBBLE_HEIGHT = 80;\nvar DEFAULT_LANGUAGE_CODE = 'en';\nvar DEFAULT_TITLE = 'Untitled Timeline';\nvar DEFAULT_BACKGROUND_COLOUR = '#fff';\nvar DESCRIPTION = 'description';\nvar TITLE = 'title';\nvar HOMEPAGE = 'homepage';\nvar HOMEPAGE_LABEL = 'homepageLabel';\nvar LOADED_JSON = 'loadedJson';\nvar BUBBLE_STYLE = 'bubblesStyle';\nvar BLACK_N_WHITE = 'blackAndWhite';\nvar SHOW_TIMES = 'showTimes';\nvar AUTO_SCALE_HEIGHT = 'autoScaleHeightOnResize';\nvar START_PLAYING_WHEN_BUBBLES_CLICKED = 'startPlayingWhenBubbleIsClicked';\nvar STOP_PLAYING_END_OF_SECTION = 'stopPlayingAtTheEndOfSection';\nvar START_PLAYING_END_OF_SECTION = 'startPlayingAtEndOfSection';\nvar ZOOM_TO_SECTION_INCREMENTALLY = 'zoomToSectionIncrementally';\nvar BUBBLE_HEIGHT = 'bubbleHeight';\nvar LANGUAGE = 'language';\nvar BACKGROUND_COLOUR = 'backgroundColour';\nvar SHOW_MARKERS = 'showMarkers';\nvar COLOUR_PALETTE = 'colourPalette';\nvar IS_SAVED = 'isSaved';\nvar PROJECT = {\n DESCRIPTION: DESCRIPTION,\n TITLE: TITLE,\n HOMEPAGE: HOMEPAGE,\n HOMEPAGE_LABEL: HOMEPAGE_LABEL,\n LOADED_JSON: LOADED_JSON,\n BUBBLE_STYLE: BUBBLE_STYLE,\n BLACK_N_WHITE: BLACK_N_WHITE,\n SHOW_TIMES: SHOW_TIMES,\n AUTO_SCALE_HEIGHT: AUTO_SCALE_HEIGHT,\n START_PLAYING_WHEN_BUBBLES_CLICKED: START_PLAYING_WHEN_BUBBLES_CLICKED,\n STOP_PLAYING_END_OF_SECTION: STOP_PLAYING_END_OF_SECTION,\n START_PLAYING_END_OF_SECTION: START_PLAYING_END_OF_SECTION,\n ZOOM_TO_SECTION_INCREMENTALLY: ZOOM_TO_SECTION_INCREMENTALLY,\n SHOW_MARKERS: SHOW_MARKERS,\n BUBBLE_HEIGHT: BUBBLE_HEIGHT,\n LANGUAGE: LANGUAGE,\n BACKGROUND_COLOUR: BACKGROUND_COLOUR,\n COLOUR_PALETTE: COLOUR_PALETTE,\n IS_SAVED: IS_SAVED\n};\nvar RDF_NAMESPACE = 'tl';\nvar PROJECT_SETTINGS_KEYS = [BUBBLE_STYLE, BLACK_N_WHITE, SHOW_TIMES, AUTO_SCALE_HEIGHT, START_PLAYING_WHEN_BUBBLES_CLICKED, STOP_PLAYING_END_OF_SECTION, START_PLAYING_END_OF_SECTION, ZOOM_TO_SECTION_INCREMENTALLY, SHOW_MARKERS, BUBBLE_HEIGHT, BACKGROUND_COLOUR, COLOUR_PALETTE];\nvar SETTINGS_ATTRIBUTE = \"\".concat(RDF_NAMESPACE, \":settings\");\nvar DEFAULT_SETTINGS = (_DEFAULT_SETTINGS = {}, Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_1__[\"default\"])(_DEFAULT_SETTINGS, BUBBLE_HEIGHT, DEFAULT_BUBBLE_HEIGHT), Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_1__[\"default\"])(_DEFAULT_SETTINGS, SHOW_TIMES, false), Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_1__[\"default\"])(_DEFAULT_SETTINGS, BLACK_N_WHITE, false), Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_1__[\"default\"])(_DEFAULT_SETTINGS, BUBBLE_STYLE, BUBBLE_STYLES.ROUNDED), Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_1__[\"default\"])(_DEFAULT_SETTINGS, AUTO_SCALE_HEIGHT, false), Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_1__[\"default\"])(_DEFAULT_SETTINGS, START_PLAYING_WHEN_BUBBLES_CLICKED, false), Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_1__[\"default\"])(_DEFAULT_SETTINGS, STOP_PLAYING_END_OF_SECTION, false), Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_1__[\"default\"])(_DEFAULT_SETTINGS, START_PLAYING_END_OF_SECTION, false), Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_1__[\"default\"])(_DEFAULT_SETTINGS, ZOOM_TO_SECTION_INCREMENTALLY, false), Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_1__[\"default\"])(_DEFAULT_SETTINGS, BACKGROUND_COLOUR, DEFAULT_BACKGROUND_COLOUR), Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_1__[\"default\"])(_DEFAULT_SETTINGS, SHOW_MARKERS, true), Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_1__[\"default\"])(_DEFAULT_SETTINGS, COLOUR_PALETTE, 'default'), _DEFAULT_SETTINGS);\nvar DEFAULT_PROJECT_STATE = Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_objectSpread__WEBPACK_IMPORTED_MODULE_0__[\"default\"])({}, DEFAULT_SETTINGS, (_objectSpread2 = {}, Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_1__[\"default\"])(_objectSpread2, LANGUAGE, DEFAULT_LANGUAGE_CODE), Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_1__[\"default\"])(_objectSpread2, TITLE, DEFAULT_TITLE), Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_1__[\"default\"])(_objectSpread2, DESCRIPTION, ''), Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_1__[\"default\"])(_objectSpread2, LOADED_JSON, {}), Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_1__[\"default\"])(_objectSpread2, IS_SAVED, true), _objectSpread2));\nvar UPDATE_SETTINGS = 'UPDATE_SETTINGS';\nvar SET_LANGUAGE = 'SET_LANGUAGE';\nvar SET_TITLE = 'SET_TITLE';\nvar SET_DESCRIPTION = 'SET_DESCRIPTION';\nvar RESET_DOCUMENT = 'RESET_DOCUMENT';\nvar EXPORT_DOCUMENT = 'EXPORT_DOCUMENT';\nvar IMPORT_DOCUMENT = 'IMPORT_DOCUMENT';\nvar LOAD_PROJECT = 'LOAD_PROJECT';\nvar IMPORT_ERROR = 'IMPORT_ERROR';\nvar SAVE_PROJECT = 'SAVE_PROJECT';\nvar SET_COLOUR_PALETTE = 'SET_COLOUR_PALETTE';\nvar CLEAR_CUSTOM_COLORS = 'CLEAR_CUSTOM_COLORS';\nvar SET_IS_SAVED = 'SET_IS_SAVED';\n\n//# sourceURL=webpack:///./src/constants/project.js?"); + eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"BUBBLE_STYLES\", function() { return BUBBLE_STYLES; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"PROJECT\", function() { return PROJECT; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"RDF_NAMESPACE\", function() { return RDF_NAMESPACE; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"PROJECT_SETTINGS_KEYS\", function() { return PROJECT_SETTINGS_KEYS; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"SETTINGS_ATTRIBUTE\", function() { return SETTINGS_ATTRIBUTE; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"DEFAULT_SETTINGS\", function() { return DEFAULT_SETTINGS; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"DEFAULT_PROJECT_STATE\", function() { return DEFAULT_PROJECT_STATE; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"UPDATE_SETTINGS\", function() { return UPDATE_SETTINGS; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"SET_LANGUAGE\", function() { return SET_LANGUAGE; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"SET_TITLE\", function() { return SET_TITLE; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"SET_DESCRIPTION\", function() { return SET_DESCRIPTION; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"RESET_DOCUMENT\", function() { return RESET_DOCUMENT; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"EXPORT_DOCUMENT\", function() { return EXPORT_DOCUMENT; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"IMPORT_DOCUMENT\", function() { return IMPORT_DOCUMENT; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"LOAD_PROJECT\", function() { return LOAD_PROJECT; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"IMPORT_ERROR\", function() { return IMPORT_ERROR; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"SAVE_PROJECT\", function() { return SAVE_PROJECT; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"SET_COLOUR_PALETTE\", function() { return SET_COLOUR_PALETTE; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"CLEAR_CUSTOM_COLORS\", function() { return CLEAR_CUSTOM_COLORS; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"PROJECT_CHANGED\", function() { return PROJECT_CHANGED; });\n/* harmony import */ var _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_objectSpread__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./node_modules/babel-preset-react-app/node_modules/@babel/runtime/helpers/esm/objectSpread */ \"./node_modules/babel-preset-react-app/node_modules/@babel/runtime/helpers/esm/objectSpread.js\");\n/* harmony import */ var _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./node_modules/babel-preset-react-app/node_modules/@babel/runtime/helpers/esm/defineProperty */ \"./node_modules/babel-preset-react-app/node_modules/@babel/runtime/helpers/esm/defineProperty.js\");\n\n\n\nvar _DEFAULT_SETTINGS, _objectSpread2;\n\nvar BUBBLE_STYLES = {\n SQUARE: 'square',\n ROUNDED: 'rounded'\n};\nvar DEFAULT_BUBBLE_HEIGHT = 80;\nvar DEFAULT_LANGUAGE_CODE = 'en';\nvar DEFAULT_TITLE = 'Untitled Timeline';\nvar DEFAULT_BACKGROUND_COLOUR = '#fff';\nvar DESCRIPTION = 'description';\nvar TITLE = 'title';\nvar HOMEPAGE = 'homepage';\nvar HOMEPAGE_LABEL = 'homepageLabel';\nvar LOADED_JSON = 'loadedJson';\nvar BUBBLE_STYLE = 'bubblesStyle';\nvar BLACK_N_WHITE = 'blackAndWhite';\nvar SHOW_TIMES = 'showTimes';\nvar AUTO_SCALE_HEIGHT = 'autoScaleHeightOnResize';\nvar START_PLAYING_WHEN_BUBBLES_CLICKED = 'startPlayingWhenBubbleIsClicked';\nvar STOP_PLAYING_END_OF_SECTION = 'stopPlayingAtTheEndOfSection';\nvar START_PLAYING_END_OF_SECTION = 'startPlayingAtEndOfSection';\nvar ZOOM_TO_SECTION_INCREMENTALLY = 'zoomToSectionIncrementally';\nvar BUBBLE_HEIGHT = 'bubbleHeight';\nvar LANGUAGE = 'language';\nvar BACKGROUND_COLOUR = 'backgroundColour';\nvar SHOW_MARKERS = 'showMarkers';\nvar COLOUR_PALETTE = 'colourPalette';\nvar IS_CHANGED = 'isChanged';\nvar PROJECT = {\n DESCRIPTION: DESCRIPTION,\n TITLE: TITLE,\n HOMEPAGE: HOMEPAGE,\n HOMEPAGE_LABEL: HOMEPAGE_LABEL,\n LOADED_JSON: LOADED_JSON,\n BUBBLE_STYLE: BUBBLE_STYLE,\n BLACK_N_WHITE: BLACK_N_WHITE,\n SHOW_TIMES: SHOW_TIMES,\n AUTO_SCALE_HEIGHT: AUTO_SCALE_HEIGHT,\n START_PLAYING_WHEN_BUBBLES_CLICKED: START_PLAYING_WHEN_BUBBLES_CLICKED,\n STOP_PLAYING_END_OF_SECTION: STOP_PLAYING_END_OF_SECTION,\n START_PLAYING_END_OF_SECTION: START_PLAYING_END_OF_SECTION,\n ZOOM_TO_SECTION_INCREMENTALLY: ZOOM_TO_SECTION_INCREMENTALLY,\n SHOW_MARKERS: SHOW_MARKERS,\n BUBBLE_HEIGHT: BUBBLE_HEIGHT,\n LANGUAGE: LANGUAGE,\n BACKGROUND_COLOUR: BACKGROUND_COLOUR,\n COLOUR_PALETTE: COLOUR_PALETTE,\n IS_CHANGED: IS_CHANGED\n};\nvar RDF_NAMESPACE = 'tl';\nvar PROJECT_SETTINGS_KEYS = [BUBBLE_STYLE, BLACK_N_WHITE, SHOW_TIMES, AUTO_SCALE_HEIGHT, START_PLAYING_WHEN_BUBBLES_CLICKED, STOP_PLAYING_END_OF_SECTION, START_PLAYING_END_OF_SECTION, ZOOM_TO_SECTION_INCREMENTALLY, SHOW_MARKERS, BUBBLE_HEIGHT, BACKGROUND_COLOUR, COLOUR_PALETTE];\nvar SETTINGS_ATTRIBUTE = \"\".concat(RDF_NAMESPACE, \":settings\");\nvar DEFAULT_SETTINGS = (_DEFAULT_SETTINGS = {}, Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_1__[\"default\"])(_DEFAULT_SETTINGS, BUBBLE_HEIGHT, DEFAULT_BUBBLE_HEIGHT), Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_1__[\"default\"])(_DEFAULT_SETTINGS, SHOW_TIMES, false), Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_1__[\"default\"])(_DEFAULT_SETTINGS, BLACK_N_WHITE, false), Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_1__[\"default\"])(_DEFAULT_SETTINGS, BUBBLE_STYLE, BUBBLE_STYLES.ROUNDED), Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_1__[\"default\"])(_DEFAULT_SETTINGS, AUTO_SCALE_HEIGHT, false), Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_1__[\"default\"])(_DEFAULT_SETTINGS, START_PLAYING_WHEN_BUBBLES_CLICKED, false), Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_1__[\"default\"])(_DEFAULT_SETTINGS, STOP_PLAYING_END_OF_SECTION, false), Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_1__[\"default\"])(_DEFAULT_SETTINGS, START_PLAYING_END_OF_SECTION, false), Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_1__[\"default\"])(_DEFAULT_SETTINGS, ZOOM_TO_SECTION_INCREMENTALLY, false), Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_1__[\"default\"])(_DEFAULT_SETTINGS, BACKGROUND_COLOUR, DEFAULT_BACKGROUND_COLOUR), Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_1__[\"default\"])(_DEFAULT_SETTINGS, SHOW_MARKERS, true), Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_1__[\"default\"])(_DEFAULT_SETTINGS, COLOUR_PALETTE, 'default'), _DEFAULT_SETTINGS);\nvar DEFAULT_PROJECT_STATE = Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_objectSpread__WEBPACK_IMPORTED_MODULE_0__[\"default\"])({}, DEFAULT_SETTINGS, (_objectSpread2 = {}, Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_1__[\"default\"])(_objectSpread2, LANGUAGE, DEFAULT_LANGUAGE_CODE), Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_1__[\"default\"])(_objectSpread2, TITLE, DEFAULT_TITLE), Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_1__[\"default\"])(_objectSpread2, DESCRIPTION, ''), Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_1__[\"default\"])(_objectSpread2, LOADED_JSON, {}), Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_1__[\"default\"])(_objectSpread2, IS_CHANGED, true), _objectSpread2));\nvar UPDATE_SETTINGS = 'UPDATE_SETTINGS';\nvar SET_LANGUAGE = 'SET_LANGUAGE';\nvar SET_TITLE = 'SET_TITLE';\nvar SET_DESCRIPTION = 'SET_DESCRIPTION';\nvar RESET_DOCUMENT = 'RESET_DOCUMENT';\nvar EXPORT_DOCUMENT = 'EXPORT_DOCUMENT';\nvar IMPORT_DOCUMENT = 'IMPORT_DOCUMENT';\nvar LOAD_PROJECT = 'LOAD_PROJECT';\nvar IMPORT_ERROR = 'IMPORT_ERROR';\nvar SAVE_PROJECT = 'SAVE_PROJECT';\nvar SET_COLOUR_PALETTE = 'SET_COLOUR_PALETTE';\nvar CLEAR_CUSTOM_COLORS = 'CLEAR_CUSTOM_COLORS';\nvar PROJECT_CHANGED = 'PROJECT_CHANGED';\n\n//# sourceURL=webpack:///./src/constants/project.js?"); /***/ }), @@ -10953,7 +10953,7 @@ import "../iiif-timeliner-styles.css" /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; - eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_classCallCheck__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./node_modules/babel-preset-react-app/node_modules/@babel/runtime/helpers/esm/classCallCheck */ \"./node_modules/babel-preset-react-app/node_modules/@babel/runtime/helpers/esm/classCallCheck.js\");\n/* harmony import */ var _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_createClass__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./node_modules/babel-preset-react-app/node_modules/@babel/runtime/helpers/esm/createClass */ \"./node_modules/babel-preset-react-app/node_modules/@babel/runtime/helpers/esm/createClass.js\");\n/* harmony import */ var _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_possibleConstructorReturn__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./node_modules/babel-preset-react-app/node_modules/@babel/runtime/helpers/esm/possibleConstructorReturn */ \"./node_modules/babel-preset-react-app/node_modules/@babel/runtime/helpers/esm/possibleConstructorReturn.js\");\n/* harmony import */ var _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_getPrototypeOf__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./node_modules/babel-preset-react-app/node_modules/@babel/runtime/helpers/esm/getPrototypeOf */ \"./node_modules/babel-preset-react-app/node_modules/@babel/runtime/helpers/esm/getPrototypeOf.js\");\n/* harmony import */ var _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_inherits__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./node_modules/babel-preset-react-app/node_modules/@babel/runtime/helpers/esm/inherits */ \"./node_modules/babel-preset-react-app/node_modules/@babel/runtime/helpers/esm/inherits.js\");\n/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! react */ \"./node_modules/react/index.js\");\n/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_5___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_5__);\n/* harmony import */ var prop_types__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! prop-types */ \"./node_modules/prop-types/index.js\");\n/* harmony import */ var prop_types__WEBPACK_IMPORTED_MODULE_6___default = /*#__PURE__*/__webpack_require__.n(prop_types__WEBPACK_IMPORTED_MODULE_6__);\n/* harmony import */ var _material_ui_core_styles__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! @material-ui/core/styles */ \"./node_modules/@material-ui/core/styles/index.js\");\n/* harmony import */ var _material_ui_core_styles__WEBPACK_IMPORTED_MODULE_7___default = /*#__PURE__*/__webpack_require__.n(_material_ui_core_styles__WEBPACK_IMPORTED_MODULE_7__);\n/* harmony import */ var react_redux__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(/*! react-redux */ \"./node_modules/react-redux/es/index.js\");\n/* harmony import */ var _components_VariationsAppBar_VariationsAppBar__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(/*! ../../components/VariationsAppBar/VariationsAppBar */ \"./src/components/VariationsAppBar/VariationsAppBar.js\");\n/* harmony import */ var _components_AudioTransportBar_AudioTransportBar__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(/*! ../../components/AudioTransportBar/AudioTransportBar */ \"./src/components/AudioTransportBar/AudioTransportBar.js\");\n/* harmony import */ var _components_Metadata_Metadata__WEBPACK_IMPORTED_MODULE_11__ = __webpack_require__(/*! ../../components/Metadata/Metadata */ \"./src/components/Metadata/Metadata.js\");\n/* harmony import */ var _components_AudioImporter_AudioImporter__WEBPACK_IMPORTED_MODULE_12__ = __webpack_require__(/*! ../../components/AudioImporter/AudioImporter */ \"./src/components/AudioImporter/AudioImporter.js\");\n/* harmony import */ var _components_SettingsPopoup_SettingsPopup__WEBPACK_IMPORTED_MODULE_13__ = __webpack_require__(/*! ../../components/SettingsPopoup/SettingsPopup */ \"./src/components/SettingsPopoup/SettingsPopup.js\");\n/* harmony import */ var _components_Footer_Footer__WEBPACK_IMPORTED_MODULE_14__ = __webpack_require__(/*! ../../components/Footer/Footer */ \"./src/components/Footer/Footer.js\");\n/* harmony import */ var _components_ContentOverlay_ContentOverlay__WEBPACK_IMPORTED_MODULE_15__ = __webpack_require__(/*! ../../components/ContentOverlay/ContentOverlay */ \"./src/components/ContentOverlay/ContentOverlay.js\");\n/* harmony import */ var _components_VerifyDialog_VerifyDialog__WEBPACK_IMPORTED_MODULE_16__ = __webpack_require__(/*! ../../components/VerifyDialog/VerifyDialog */ \"./src/components/VerifyDialog/VerifyDialog.js\");\n/* harmony import */ var _BubbleEditor_BubbleEditor__WEBPACK_IMPORTED_MODULE_17__ = __webpack_require__(/*! ../BubbleEditor/BubbleEditor */ \"./src/containers/BubbleEditor/BubbleEditor.js\");\n/* harmony import */ var _Audio_Audio__WEBPACK_IMPORTED_MODULE_18__ = __webpack_require__(/*! ../Audio/Audio */ \"./src/containers/Audio/Audio.js\");\n/* harmony import */ var _actions_range__WEBPACK_IMPORTED_MODULE_19__ = __webpack_require__(/*! ../../actions/range */ \"./src/actions/range.js\");\n/* harmony import */ var _actions_project__WEBPACK_IMPORTED_MODULE_20__ = __webpack_require__(/*! ../../actions/project */ \"./src/actions/project.js\");\n/* harmony import */ var _constants_range__WEBPACK_IMPORTED_MODULE_21__ = __webpack_require__(/*! ../../constants/range */ \"./src/constants/range.js\");\n/* harmony import */ var _constants_project__WEBPACK_IMPORTED_MODULE_22__ = __webpack_require__(/*! ../../constants/project */ \"./src/constants/project.js\");\n/* harmony import */ var _constants_viewState__WEBPACK_IMPORTED_MODULE_23__ = __webpack_require__(/*! ../../constants/viewState */ \"./src/constants/viewState.js\");\n/* harmony import */ var _actions_viewState__WEBPACK_IMPORTED_MODULE_24__ = __webpack_require__(/*! ../../actions/viewState */ \"./src/actions/viewState.js\");\n/* harmony import */ var _actions_markers__WEBPACK_IMPORTED_MODULE_25__ = __webpack_require__(/*! ../../actions/markers */ \"./src/actions/markers.js\");\n/* harmony import */ var _VariationsMainView_scss__WEBPACK_IMPORTED_MODULE_26__ = __webpack_require__(/*! ./VariationsMainView.scss */ \"./src/containers/VariationsMainView/VariationsMainView.scss\");\n/* harmony import */ var _VariationsMainView_scss__WEBPACK_IMPORTED_MODULE_26___default = /*#__PURE__*/__webpack_require__.n(_VariationsMainView_scss__WEBPACK_IMPORTED_MODULE_26__);\n/* harmony import */ var _reducers_range__WEBPACK_IMPORTED_MODULE_27__ = __webpack_require__(/*! ../../reducers/range */ \"./src/reducers/range.js\");\n/* harmony import */ var _AuthResource_AuthResource__WEBPACK_IMPORTED_MODULE_28__ = __webpack_require__(/*! ../AuthResource/AuthResource */ \"./src/containers/AuthResource/AuthResource.js\");\n/* harmony import */ var redux_undo_redo__WEBPACK_IMPORTED_MODULE_29__ = __webpack_require__(/*! redux-undo-redo */ \"./node_modules/redux-undo-redo/lib/index.js\");\n/* harmony import */ var redux_undo_redo__WEBPACK_IMPORTED_MODULE_29___default = /*#__PURE__*/__webpack_require__.n(redux_undo_redo__WEBPACK_IMPORTED_MODULE_29__);\n/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_30__ = __webpack_require__(/*! ../../config */ \"./src/config.js\");\n\n\n\n\n\nvar _jsxFileName = \"/home/dwithana/github/iu/timeliner/src/containers/VariationsMainView/VariationsMainView.js\";\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nvar _ref =\n/*#__PURE__*/\nreact__WEBPACK_IMPORTED_MODULE_5___default.a.createElement(_components_Footer_Footer__WEBPACK_IMPORTED_MODULE_14__[\"default\"], {\n __source: {\n fileName: _jsxFileName,\n lineNumber: 321\n },\n __self: undefined\n});\n\nvar VariationsMainView =\n/*#__PURE__*/\nfunction (_React$Component) {\n Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_inherits__WEBPACK_IMPORTED_MODULE_4__[\"default\"])(VariationsMainView, _React$Component);\n\n function VariationsMainView(props) {\n var _this;\n\n Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_classCallCheck__WEBPACK_IMPORTED_MODULE_0__[\"default\"])(this, VariationsMainView);\n\n _this = Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_possibleConstructorReturn__WEBPACK_IMPORTED_MODULE_2__[\"default\"])(this, Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_getPrototypeOf__WEBPACK_IMPORTED_MODULE_3__[\"default\"])(VariationsMainView).call(this, props));\n\n _this.addRange = function (selected) {\n return function () {\n _this.props.splitRangeAt((selected[_constants_range__WEBPACK_IMPORTED_MODULE_21__[\"RANGE\"].END_TIME] - selected[_constants_range__WEBPACK_IMPORTED_MODULE_21__[\"RANGE\"].START_TIME]) / 2 + selected[_constants_range__WEBPACK_IMPORTED_MODULE_21__[\"RANGE\"].START_TIME]);\n };\n };\n\n _this.groupSelectedRanges = function () {\n _this.props.groupSelectedRanges();\n };\n\n _this.deleteRanges = function (ranges) {\n return function () {\n _this.props.deleteRanges(ranges);\n };\n };\n\n _this.isGroupingPossible = function (selectedRanges) {\n var newRangeMinMax = selectedRanges.reduce(function (newRange, bubble) {\n newRange.startTime = Math.min(newRange.startTime, bubble.startTime);\n newRange.endTime = Math.max(newRange.endTime, bubble.endTime);\n return newRange;\n }, {\n startTime: Number.MAX_SAFE_INTEGER,\n endTime: Number.MIN_SAFE_INTEGER\n });\n var tallBubbles = Object.values(_this.props.points).filter(function (bubble) {\n return bubble.depth > 1;\n });\n return tallBubbles.filter(function (bubble) {\n return bubble.startTime < newRangeMinMax.startTime && newRangeMinMax.startTime < bubble.endTime && bubble.endTime < newRangeMinMax.endTime || newRangeMinMax.startTime < bubble.startTime && bubble.startTime < newRangeMinMax.endTime && newRangeMinMax.endTime < bubble.endTime || bubble.startTime === newRangeMinMax.startTime && bubble.endTime === newRangeMinMax.endTime;\n }).length === 0;\n };\n\n _this.isSplittingPossible = function () {\n return !Object.values(_this.props.points).reduce(function (allPoints, range) {\n allPoints.add(range.startTime);\n allPoints.add(range.endTime);\n return allPoints;\n }, new Set([])).has(_this.props.currentTime);\n };\n\n _this.splitRange = function () {\n return _this.props.splitRangeAt(_this.props.currentTime);\n };\n\n _this.addMarker = function () {\n _this.props.addMarkerAtTime(_this.props.currentTime);\n\n _this.props.setProjectStatus(false);\n };\n\n _this.getAuthService = function () {\n var annotationPages = _this.props.annotationPages;\n\n if (annotationPages && annotationPages[0] && annotationPages[0].items && annotationPages[0].items[0]) {\n var avResource = Object(_AuthResource_AuthResource__WEBPACK_IMPORTED_MODULE_28__[\"resolveAvResource\"])(annotationPages[0].items[0]);\n return avResource.service;\n }\n };\n\n _this.getOnSave = function () {\n if (!_this.props.callback) {\n return null;\n }\n\n return _this.props.saveProject;\n };\n\n _this.theme = Object(_material_ui_core_styles__WEBPACK_IMPORTED_MODULE_7__[\"createMuiTheme\"])({\n palette: {\n primary: {\n light: '#757ce8',\n main: '#3f50b5',\n dark: '#002884',\n contrastText: '#fff'\n },\n secondary: {\n light: '#d2abf3',\n main: '#C797F0',\n dark: '#8b69a8',\n contrastText: '#000'\n }\n },\n status: {\n danger: 'orange'\n },\n spacing: {\n unit: 4\n },\n shape: {\n borderRadius: 2\n },\n typography: {\n fontSize: 12,\n useNextVariants: true\n },\n mixins: {\n toolbar: {\n minHeight: 36\n }\n }\n });\n return _this;\n }\n\n Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_createClass__WEBPACK_IMPORTED_MODULE_1__[\"default\"])(VariationsMainView, [{\n key: \"render\",\n value: function render() {\n var _points = this.props.points;\n var _this$props = this.props,\n isPlaying = _this$props.isPlaying,\n volume = _this$props.volume,\n currentTime = _this$props.currentTime,\n audioUrl = _this$props.audioUrl,\n runTime = _this$props.runTime,\n manifestLabel = _this$props.manifestLabel,\n manifestSummary = _this$props.manifestSummary,\n homepage = _this$props.homepage,\n homepageLabel = _this$props.homepageLabel,\n isImportOpen = _this$props.isImportOpen,\n isSettingsOpen = _this$props.isSettingsOpen,\n audioError = _this$props.audioError,\n loadingPercent = _this$props.loadingPercent,\n isLoaded = _this$props.isLoaded,\n rangeToEdit = _this$props.rangeToEdit,\n settings = _this$props.settings,\n selectedRanges = _this$props.selectedRanges,\n hasResource = _this$props.hasResource,\n colourPalette = _this$props.colourPalette,\n noFooter = _this$props.noFooter,\n noHeader = _this$props.noHeader,\n noSourceLink = _this$props.noSourceLink,\n zoom = _this$props.zoom;\n return react__WEBPACK_IMPORTED_MODULE_5___default.a.createElement(\"div\", {\n className: \"variations-app\",\n __source: {\n fileName: _jsxFileName,\n lineNumber: 224\n },\n __self: this\n }, react__WEBPACK_IMPORTED_MODULE_5___default.a.createElement(_material_ui_core_styles__WEBPACK_IMPORTED_MODULE_7__[\"MuiThemeProvider\"], {\n theme: this.theme,\n __source: {\n fileName: _jsxFileName,\n lineNumber: 225\n },\n __self: this\n }, react__WEBPACK_IMPORTED_MODULE_5___default.a.createElement(_components_VariationsAppBar_VariationsAppBar__WEBPACK_IMPORTED_MODULE_9__[\"default\"], {\n title: manifestLabel,\n onImportButtonClicked: this.props.showImportModal,\n onSettingsButtonClicked: this.props.showSettingsModal,\n canUndo: this.props.canUndo,\n canRedo: this.props.canRedo,\n onRedo: this.props.onRedo,\n onUndo: this.props.onUndo,\n onSave: this.getOnSave(),\n onTitleChange: function onTitleChange() {},\n hasResource: this.props.hasResource,\n noHeader: this.props.noHeader,\n __source: {\n fileName: _jsxFileName,\n lineNumber: 226\n },\n __self: this\n }), react__WEBPACK_IMPORTED_MODULE_5___default.a.createElement(\"div\", {\n className: \"variations-app__content\",\n __source: {\n fileName: _jsxFileName,\n lineNumber: 239\n },\n __self: this\n }, react__WEBPACK_IMPORTED_MODULE_5___default.a.createElement(_AuthResource_AuthResource__WEBPACK_IMPORTED_MODULE_28__[\"AuthCookieService1\"], {\n key: this.props.url,\n resource: this.props.url,\n service: this.props.authService ? this.props.authService[0] : null,\n __source: {\n fileName: _jsxFileName,\n lineNumber: 240\n },\n __self: this\n }, react__WEBPACK_IMPORTED_MODULE_5___default.a.createElement(_BubbleEditor_BubbleEditor__WEBPACK_IMPORTED_MODULE_17__[\"default\"], {\n key: 'bubble--' + this.props.url,\n __source: {\n fileName: _jsxFileName,\n lineNumber: 247\n },\n __self: this\n }), this.props.url ? react__WEBPACK_IMPORTED_MODULE_5___default.a.createElement(_Audio_Audio__WEBPACK_IMPORTED_MODULE_18__[\"default\"], {\n key: 'audio--' + this.props.url,\n __source: {\n fileName: _jsxFileName,\n lineNumber: 249\n },\n __self: this\n }) : null, react__WEBPACK_IMPORTED_MODULE_5___default.a.createElement(_components_AudioTransportBar_AudioTransportBar__WEBPACK_IMPORTED_MODULE_10__[\"default\"], {\n isPlaying: isPlaying,\n volume: volume,\n currentTime: currentTime,\n runTime: runTime,\n onVolumeChanged: this.props.setVolume,\n onPlay: this.props.play,\n onPause: this.props.pause,\n onNextBubble: this.props.nextBubble,\n onPreviousBubble: this.props.previousBubble,\n onScrubAhead: this.props.fastForward,\n onScrubBackwards: this.props.fastReward,\n onAddBubble: this.isSplittingPossible() ? this.splitRange : null,\n onGroupBubble: selectedRanges.length > 1 && this.isGroupingPossible(selectedRanges) ? this.props.groupSelectedRanges : null,\n onDeleteBubble: selectedRanges.length > 0 && _points.length > 1 && _points.length - selectedRanges.length > 0 ? this.deleteRanges(selectedRanges) : null,\n onAddMarker: this.addMarker,\n zoom: zoom,\n zoomIn: this.props.zoomIn,\n zoomOut: this.props.zoomOut,\n resetZoom: this.props.resetZoom,\n __source: {\n fileName: _jsxFileName,\n lineNumber: 251\n },\n __self: this\n })), react__WEBPACK_IMPORTED_MODULE_5___default.a.createElement(\"div\", {\n className: \"variations-app__metadata-editor\",\n __source: {\n fileName: _jsxFileName,\n lineNumber: 286\n },\n __self: this\n }, react__WEBPACK_IMPORTED_MODULE_5___default.a.createElement(_components_Metadata_Metadata__WEBPACK_IMPORTED_MODULE_11__[\"default\"], {\n colourPalette: colourPalette,\n currentTime: currentTime,\n runTime: runTime,\n manifestLabel: manifestLabel,\n manifestSummary: manifestSummary,\n homepage: homepage,\n homepageLabel: homepageLabel,\n noSourceLink: noSourceLink,\n ranges: _points,\n onEdit: this.props.editMetadata,\n rangeToEdit: rangeToEdit,\n onUpdateRange: this.props.updateRange,\n blackAndWhiteMode: this.props.settings[_constants_project__WEBPACK_IMPORTED_MODULE_22__[\"PROJECT\"].BLACK_N_WHITE],\n projectMetadataEditorOpen: this.props.showMetadataEditor,\n onEditProjectMetadata: this.props.editProjectMetadata,\n onSaveProjectMetadata: this.props.saveProjectMetadata,\n onEraseButtonClicked: this.props.resetDocument,\n canSave: !this.props.callback,\n canErase: !this.props.callback,\n hasResource: this.props.hasResource,\n onSaveButtonClicked: this.props.exportDocument,\n onCancelEditingProjectMetadata: this.props.cancelProjectMetadataEdits,\n url: this.props.url,\n markers: this.props.markers,\n updateMarker: this.props.updateMarker,\n deleteMarker: this.props.deleteMarker,\n updateProjectStatus: this.props.setProjectStatus,\n setCurrentTime: this.props.setCurrentTime,\n undoAll: this.props.canUndo ? this.props.undoAll : null,\n swatch: this.props.colourPalette.colours,\n __source: {\n fileName: _jsxFileName,\n lineNumber: 287\n },\n __self: this\n }), !noFooter && _ref), (audioError.code || !isLoaded) && react__WEBPACK_IMPORTED_MODULE_5___default.a.createElement(_components_ContentOverlay_ContentOverlay__WEBPACK_IMPORTED_MODULE_15__[\"default\"], Object.assign({\n loadingPercent: loadingPercent,\n isLoaded: isLoaded,\n audioUrl: audioUrl\n }, {\n error: audioError,\n __source: {\n fileName: _jsxFileName,\n lineNumber: 324\n },\n __self: this\n }))), (!hasResource || isLoaded) && react__WEBPACK_IMPORTED_MODULE_5___default.a.createElement(_components_AudioImporter_AudioImporter__WEBPACK_IMPORTED_MODULE_12__[\"default\"], {\n open: isImportOpen,\n error: this.props.importError,\n onClose: this.props.url ? this.props.dismissImportModal : null,\n onImport: this.props.importDocument,\n __source: {\n fileName: _jsxFileName,\n lineNumber: 331\n },\n __self: this\n }), react__WEBPACK_IMPORTED_MODULE_5___default.a.createElement(_components_SettingsPopoup_SettingsPopup__WEBPACK_IMPORTED_MODULE_13__[\"default\"], {\n open: isSettingsOpen,\n onClose: this.props.dismissSettingsModal,\n onSave: this.props.updateSettings,\n clearCustomColors: this.props.clearCustomColors,\n settings: settings,\n __source: {\n fileName: _jsxFileName,\n lineNumber: 338\n },\n __self: this\n }), react__WEBPACK_IMPORTED_MODULE_5___default.a.createElement(_components_VerifyDialog_VerifyDialog__WEBPACK_IMPORTED_MODULE_16__[\"default\"], {\n open: this.props.verifyDialog.open,\n title: this.props.verifyDialog.title,\n doCancel: this.props.verifyDialog.doCancel,\n onClose: this.props.confirmNo,\n onProceed: this.props.confirmYes,\n __source: {\n fileName: _jsxFileName,\n lineNumber: 345\n },\n __self: this\n })));\n }\n }]);\n\n return VariationsMainView;\n}(react__WEBPACK_IMPORTED_MODULE_5___default.a.Component);\n\nVariationsMainView.propTypes = {\n updateSettings: prop_types__WEBPACK_IMPORTED_MODULE_6___default.a.func,\n showImportModal: prop_types__WEBPACK_IMPORTED_MODULE_6___default.a.func,\n showSettingsModal: prop_types__WEBPACK_IMPORTED_MODULE_6___default.a.func,\n setVolume: prop_types__WEBPACK_IMPORTED_MODULE_6___default.a.func.isRequired,\n url: prop_types__WEBPACK_IMPORTED_MODULE_6___default.a.string,\n play: prop_types__WEBPACK_IMPORTED_MODULE_6___default.a.func,\n pause: prop_types__WEBPACK_IMPORTED_MODULE_6___default.a.func,\n dismissImportModal: prop_types__WEBPACK_IMPORTED_MODULE_6___default.a.func,\n dismissSettingsModal: prop_types__WEBPACK_IMPORTED_MODULE_6___default.a.func,\n volume: prop_types__WEBPACK_IMPORTED_MODULE_6___default.a.number.isRequired,\n isPlaying: prop_types__WEBPACK_IMPORTED_MODULE_6___default.a.bool.isRequired,\n isLoaded: prop_types__WEBPACK_IMPORTED_MODULE_6___default.a.bool.isRequired,\n importError: prop_types__WEBPACK_IMPORTED_MODULE_6___default.a.string,\n currentTime: prop_types__WEBPACK_IMPORTED_MODULE_6___default.a.number.isRequired,\n runTime: prop_types__WEBPACK_IMPORTED_MODULE_6___default.a.number.isRequired,\n manifestLabel: prop_types__WEBPACK_IMPORTED_MODULE_6___default.a.string.isRequired,\n manifestSummary: prop_types__WEBPACK_IMPORTED_MODULE_6___default.a.string.isRequired,\n homepage: prop_types__WEBPACK_IMPORTED_MODULE_6___default.a.string,\n homepageLabel: prop_types__WEBPACK_IMPORTED_MODULE_6___default.a.string,\n points: prop_types__WEBPACK_IMPORTED_MODULE_6___default.a.array,\n isImportOpen: prop_types__WEBPACK_IMPORTED_MODULE_6___default.a.bool.isRequired,\n isSettingsOpen: prop_types__WEBPACK_IMPORTED_MODULE_6___default.a.bool.isRequired,\n fastForward: prop_types__WEBPACK_IMPORTED_MODULE_6___default.a.func.isRequired,\n fastReward: prop_types__WEBPACK_IMPORTED_MODULE_6___default.a.func.isRequired,\n importDocument: prop_types__WEBPACK_IMPORTED_MODULE_6___default.a.func.isRequired,\n exportDocument: prop_types__WEBPACK_IMPORTED_MODULE_6___default.a.func.isRequired,\n addMarkerAtTime: prop_types__WEBPACK_IMPORTED_MODULE_6___default.a.func.isRequired,\n saveProject: prop_types__WEBPACK_IMPORTED_MODULE_6___default.a.func.isRequired,\n settings: prop_types__WEBPACK_IMPORTED_MODULE_6___default.a.object,\n zoom: prop_types__WEBPACK_IMPORTED_MODULE_6___default.a.number.isRequired\n};\n\nvar mapStateProps = function mapStateProps(state) {\n return {\n volume: state.viewState.volume,\n isPlaying: state.viewState.isPlaying,\n currentTime: state.viewState.currentTime,\n authService: state.canvas.service,\n annotationPages: state.canvas.items,\n url: state.canvas.url,\n runTime: state.viewState.runTime,\n manifestLabel: state.project.title,\n importError: state.project.error,\n manifestSummary: state.project.description,\n homepage: state.project.homepage,\n homepageLabel: state.project.homepageLabel,\n points: Object.values(Object(_reducers_range__WEBPACK_IMPORTED_MODULE_27__[\"getRangeList\"])(state)),\n selectedRanges: Object(_reducers_range__WEBPACK_IMPORTED_MODULE_27__[\"getSelectedRanges\"])(state),\n isImportOpen: state.viewState.isImportOpen,\n isSettingsOpen: state.viewState.isSettingsOpen,\n audioUrl: state.canvas.url,\n audioError: state.canvas.error,\n loadingPercent: state.canvas.loadingPercent,\n isLoaded: state.canvas.isLoaded,\n rangeToEdit: state.viewState.metadataToEdit,\n verifyDialog: state.viewState.verifyDialog,\n showMetadataEditor: state.viewState[_constants_viewState__WEBPACK_IMPORTED_MODULE_23__[\"VIEWSTATE\"].PROJECT_METADATA_EDITOR_OPEN],\n canUndo: state.undoHistory.undoQueue.length > 0,\n canRedo: state.undoHistory.redoQueue.length > 0,\n markers: state.markers.visible ? state.markers.list : {},\n zoom: state.viewState[_constants_viewState__WEBPACK_IMPORTED_MODULE_23__[\"VIEWSTATE\"].ZOOM],\n //settings\n settings: _constants_project__WEBPACK_IMPORTED_MODULE_22__[\"PROJECT_SETTINGS_KEYS\"].reduce(function (acc, next) {\n acc[next] = state.project[next];\n return acc;\n }, {}),\n colourPalette: _config__WEBPACK_IMPORTED_MODULE_30__[\"colourPalettes\"][state.project[_constants_project__WEBPACK_IMPORTED_MODULE_22__[\"PROJECT\"].COLOUR_PALETTE]] || _config__WEBPACK_IMPORTED_MODULE_30__[\"colourPalettes\"].default\n };\n};\n\nvar mapDispatchToProps = {\n //project actions\n importDocument: _actions_project__WEBPACK_IMPORTED_MODULE_20__[\"importDocument\"],\n updateSettings: _actions_project__WEBPACK_IMPORTED_MODULE_20__[\"updateSettings\"],\n resetDocument: _actions_project__WEBPACK_IMPORTED_MODULE_20__[\"resetDocument\"],\n exportDocument: _actions_project__WEBPACK_IMPORTED_MODULE_20__[\"exportDocument\"],\n editProjectMetadata: _actions_viewState__WEBPACK_IMPORTED_MODULE_24__[\"editProjectMetadata\"],\n cancelProjectMetadataEdits: _actions_viewState__WEBPACK_IMPORTED_MODULE_24__[\"cancelProjectMetadataEdits\"],\n saveProjectMetadata: _actions_viewState__WEBPACK_IMPORTED_MODULE_24__[\"saveProjectMetadata\"],\n clearCustomColors: _actions_project__WEBPACK_IMPORTED_MODULE_20__[\"clearCustomColors\"],\n setProjectStatus: _actions_project__WEBPACK_IMPORTED_MODULE_20__[\"setProjectStatus\"],\n //view state actions\n showImportModal: _actions_viewState__WEBPACK_IMPORTED_MODULE_24__[\"showImportModal\"],\n showSettingsModal: _actions_viewState__WEBPACK_IMPORTED_MODULE_24__[\"showSettingsModal\"],\n setVolume: _actions_viewState__WEBPACK_IMPORTED_MODULE_24__[\"setVolume\"],\n play: _actions_viewState__WEBPACK_IMPORTED_MODULE_24__[\"play\"],\n pause: _actions_viewState__WEBPACK_IMPORTED_MODULE_24__[\"pause\"],\n dismissImportModal: _actions_viewState__WEBPACK_IMPORTED_MODULE_24__[\"dismissImportModal\"],\n dismissSettingsModal: _actions_viewState__WEBPACK_IMPORTED_MODULE_24__[\"dismissSettingsModal\"],\n fastForward: _actions_viewState__WEBPACK_IMPORTED_MODULE_24__[\"fastForward\"],\n fastReward: _actions_viewState__WEBPACK_IMPORTED_MODULE_24__[\"fastReward\"],\n editMetadata: _actions_viewState__WEBPACK_IMPORTED_MODULE_24__[\"editMetadata\"],\n previousBubble: _actions_viewState__WEBPACK_IMPORTED_MODULE_24__[\"previousBubble\"],\n nextBubble: _actions_viewState__WEBPACK_IMPORTED_MODULE_24__[\"nextBubble\"],\n confirmYes: _actions_viewState__WEBPACK_IMPORTED_MODULE_24__[\"confirmYes\"],\n confirmNo: _actions_viewState__WEBPACK_IMPORTED_MODULE_24__[\"confirmNo\"],\n setCurrentTime: _actions_viewState__WEBPACK_IMPORTED_MODULE_24__[\"setCurrentTime\"],\n zoomIn: _actions_viewState__WEBPACK_IMPORTED_MODULE_24__[\"zoomIn\"],\n zoomOut: _actions_viewState__WEBPACK_IMPORTED_MODULE_24__[\"zoomOut\"],\n resetZoom: _actions_viewState__WEBPACK_IMPORTED_MODULE_24__[\"resetZoom\"],\n //range\n splitRangeAt: _actions_range__WEBPACK_IMPORTED_MODULE_19__[\"splitRangeAt\"],\n groupSelectedRanges: _actions_range__WEBPACK_IMPORTED_MODULE_19__[\"groupSelectedRanges\"],\n deleteRanges: _actions_range__WEBPACK_IMPORTED_MODULE_19__[\"scheduleDeleteRanges\"],\n updateRange: _actions_range__WEBPACK_IMPORTED_MODULE_19__[\"updateRange\"],\n updateMarker: _actions_markers__WEBPACK_IMPORTED_MODULE_25__[\"updateMarker\"],\n deleteMarker: _actions_markers__WEBPACK_IMPORTED_MODULE_25__[\"deleteMarker\"],\n // markers\n addMarkerAtTime: _actions_markers__WEBPACK_IMPORTED_MODULE_25__[\"addMarkerAtTime\"],\n // Undo\n onUndo: redux_undo_redo__WEBPACK_IMPORTED_MODULE_29__[\"actions\"].undo,\n onRedo: redux_undo_redo__WEBPACK_IMPORTED_MODULE_29__[\"actions\"].redo,\n undoAll: _actions_viewState__WEBPACK_IMPORTED_MODULE_24__[\"undoAll\"],\n // Export\n saveProject: _actions_project__WEBPACK_IMPORTED_MODULE_20__[\"saveProject\"]\n};\n/* harmony default export */ __webpack_exports__[\"default\"] = (Object(react_redux__WEBPACK_IMPORTED_MODULE_8__[\"connect\"])(mapStateProps, mapDispatchToProps)(VariationsMainView));\n\n//# sourceURL=webpack:///./src/containers/VariationsMainView/VariationsMainView.js?"); + eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_classCallCheck__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./node_modules/babel-preset-react-app/node_modules/@babel/runtime/helpers/esm/classCallCheck */ \"./node_modules/babel-preset-react-app/node_modules/@babel/runtime/helpers/esm/classCallCheck.js\");\n/* harmony import */ var _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_createClass__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./node_modules/babel-preset-react-app/node_modules/@babel/runtime/helpers/esm/createClass */ \"./node_modules/babel-preset-react-app/node_modules/@babel/runtime/helpers/esm/createClass.js\");\n/* harmony import */ var _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_possibleConstructorReturn__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./node_modules/babel-preset-react-app/node_modules/@babel/runtime/helpers/esm/possibleConstructorReturn */ \"./node_modules/babel-preset-react-app/node_modules/@babel/runtime/helpers/esm/possibleConstructorReturn.js\");\n/* harmony import */ var _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_getPrototypeOf__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./node_modules/babel-preset-react-app/node_modules/@babel/runtime/helpers/esm/getPrototypeOf */ \"./node_modules/babel-preset-react-app/node_modules/@babel/runtime/helpers/esm/getPrototypeOf.js\");\n/* harmony import */ var _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_inherits__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./node_modules/babel-preset-react-app/node_modules/@babel/runtime/helpers/esm/inherits */ \"./node_modules/babel-preset-react-app/node_modules/@babel/runtime/helpers/esm/inherits.js\");\n/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! react */ \"./node_modules/react/index.js\");\n/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_5___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_5__);\n/* harmony import */ var prop_types__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! prop-types */ \"./node_modules/prop-types/index.js\");\n/* harmony import */ var prop_types__WEBPACK_IMPORTED_MODULE_6___default = /*#__PURE__*/__webpack_require__.n(prop_types__WEBPACK_IMPORTED_MODULE_6__);\n/* harmony import */ var _material_ui_core_styles__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! @material-ui/core/styles */ \"./node_modules/@material-ui/core/styles/index.js\");\n/* harmony import */ var _material_ui_core_styles__WEBPACK_IMPORTED_MODULE_7___default = /*#__PURE__*/__webpack_require__.n(_material_ui_core_styles__WEBPACK_IMPORTED_MODULE_7__);\n/* harmony import */ var react_redux__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(/*! react-redux */ \"./node_modules/react-redux/es/index.js\");\n/* harmony import */ var _components_VariationsAppBar_VariationsAppBar__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(/*! ../../components/VariationsAppBar/VariationsAppBar */ \"./src/components/VariationsAppBar/VariationsAppBar.js\");\n/* harmony import */ var _components_AudioTransportBar_AudioTransportBar__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(/*! ../../components/AudioTransportBar/AudioTransportBar */ \"./src/components/AudioTransportBar/AudioTransportBar.js\");\n/* harmony import */ var _components_Metadata_Metadata__WEBPACK_IMPORTED_MODULE_11__ = __webpack_require__(/*! ../../components/Metadata/Metadata */ \"./src/components/Metadata/Metadata.js\");\n/* harmony import */ var _components_AudioImporter_AudioImporter__WEBPACK_IMPORTED_MODULE_12__ = __webpack_require__(/*! ../../components/AudioImporter/AudioImporter */ \"./src/components/AudioImporter/AudioImporter.js\");\n/* harmony import */ var _components_SettingsPopoup_SettingsPopup__WEBPACK_IMPORTED_MODULE_13__ = __webpack_require__(/*! ../../components/SettingsPopoup/SettingsPopup */ \"./src/components/SettingsPopoup/SettingsPopup.js\");\n/* harmony import */ var _components_Footer_Footer__WEBPACK_IMPORTED_MODULE_14__ = __webpack_require__(/*! ../../components/Footer/Footer */ \"./src/components/Footer/Footer.js\");\n/* harmony import */ var _components_ContentOverlay_ContentOverlay__WEBPACK_IMPORTED_MODULE_15__ = __webpack_require__(/*! ../../components/ContentOverlay/ContentOverlay */ \"./src/components/ContentOverlay/ContentOverlay.js\");\n/* harmony import */ var _components_VerifyDialog_VerifyDialog__WEBPACK_IMPORTED_MODULE_16__ = __webpack_require__(/*! ../../components/VerifyDialog/VerifyDialog */ \"./src/components/VerifyDialog/VerifyDialog.js\");\n/* harmony import */ var _BubbleEditor_BubbleEditor__WEBPACK_IMPORTED_MODULE_17__ = __webpack_require__(/*! ../BubbleEditor/BubbleEditor */ \"./src/containers/BubbleEditor/BubbleEditor.js\");\n/* harmony import */ var _Audio_Audio__WEBPACK_IMPORTED_MODULE_18__ = __webpack_require__(/*! ../Audio/Audio */ \"./src/containers/Audio/Audio.js\");\n/* harmony import */ var _actions_range__WEBPACK_IMPORTED_MODULE_19__ = __webpack_require__(/*! ../../actions/range */ \"./src/actions/range.js\");\n/* harmony import */ var _actions_project__WEBPACK_IMPORTED_MODULE_20__ = __webpack_require__(/*! ../../actions/project */ \"./src/actions/project.js\");\n/* harmony import */ var _constants_range__WEBPACK_IMPORTED_MODULE_21__ = __webpack_require__(/*! ../../constants/range */ \"./src/constants/range.js\");\n/* harmony import */ var _constants_project__WEBPACK_IMPORTED_MODULE_22__ = __webpack_require__(/*! ../../constants/project */ \"./src/constants/project.js\");\n/* harmony import */ var _constants_viewState__WEBPACK_IMPORTED_MODULE_23__ = __webpack_require__(/*! ../../constants/viewState */ \"./src/constants/viewState.js\");\n/* harmony import */ var _actions_viewState__WEBPACK_IMPORTED_MODULE_24__ = __webpack_require__(/*! ../../actions/viewState */ \"./src/actions/viewState.js\");\n/* harmony import */ var _actions_markers__WEBPACK_IMPORTED_MODULE_25__ = __webpack_require__(/*! ../../actions/markers */ \"./src/actions/markers.js\");\n/* harmony import */ var _VariationsMainView_scss__WEBPACK_IMPORTED_MODULE_26__ = __webpack_require__(/*! ./VariationsMainView.scss */ \"./src/containers/VariationsMainView/VariationsMainView.scss\");\n/* harmony import */ var _VariationsMainView_scss__WEBPACK_IMPORTED_MODULE_26___default = /*#__PURE__*/__webpack_require__.n(_VariationsMainView_scss__WEBPACK_IMPORTED_MODULE_26__);\n/* harmony import */ var _reducers_range__WEBPACK_IMPORTED_MODULE_27__ = __webpack_require__(/*! ../../reducers/range */ \"./src/reducers/range.js\");\n/* harmony import */ var _AuthResource_AuthResource__WEBPACK_IMPORTED_MODULE_28__ = __webpack_require__(/*! ../AuthResource/AuthResource */ \"./src/containers/AuthResource/AuthResource.js\");\n/* harmony import */ var redux_undo_redo__WEBPACK_IMPORTED_MODULE_29__ = __webpack_require__(/*! redux-undo-redo */ \"./node_modules/redux-undo-redo/lib/index.js\");\n/* harmony import */ var redux_undo_redo__WEBPACK_IMPORTED_MODULE_29___default = /*#__PURE__*/__webpack_require__.n(redux_undo_redo__WEBPACK_IMPORTED_MODULE_29__);\n/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_30__ = __webpack_require__(/*! ../../config */ \"./src/config.js\");\n\n\n\n\n\nvar _jsxFileName = \"/home/dwithana/github/iu/timeliner/src/containers/VariationsMainView/VariationsMainView.js\";\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nvar _ref =\n/*#__PURE__*/\nreact__WEBPACK_IMPORTED_MODULE_5___default.a.createElement(_components_Footer_Footer__WEBPACK_IMPORTED_MODULE_14__[\"default\"], {\n __source: {\n fileName: _jsxFileName,\n lineNumber: 321\n },\n __self: undefined\n});\n\nvar VariationsMainView =\n/*#__PURE__*/\nfunction (_React$Component) {\n Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_inherits__WEBPACK_IMPORTED_MODULE_4__[\"default\"])(VariationsMainView, _React$Component);\n\n function VariationsMainView(props) {\n var _this;\n\n Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_classCallCheck__WEBPACK_IMPORTED_MODULE_0__[\"default\"])(this, VariationsMainView);\n\n _this = Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_possibleConstructorReturn__WEBPACK_IMPORTED_MODULE_2__[\"default\"])(this, Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_getPrototypeOf__WEBPACK_IMPORTED_MODULE_3__[\"default\"])(VariationsMainView).call(this, props));\n\n _this.addRange = function (selected) {\n return function () {\n _this.props.splitRangeAt((selected[_constants_range__WEBPACK_IMPORTED_MODULE_21__[\"RANGE\"].END_TIME] - selected[_constants_range__WEBPACK_IMPORTED_MODULE_21__[\"RANGE\"].START_TIME]) / 2 + selected[_constants_range__WEBPACK_IMPORTED_MODULE_21__[\"RANGE\"].START_TIME]);\n };\n };\n\n _this.groupSelectedRanges = function () {\n _this.props.groupSelectedRanges();\n };\n\n _this.deleteRanges = function (ranges) {\n return function () {\n _this.props.deleteRanges(ranges);\n };\n };\n\n _this.isGroupingPossible = function (selectedRanges) {\n var newRangeMinMax = selectedRanges.reduce(function (newRange, bubble) {\n newRange.startTime = Math.min(newRange.startTime, bubble.startTime);\n newRange.endTime = Math.max(newRange.endTime, bubble.endTime);\n return newRange;\n }, {\n startTime: Number.MAX_SAFE_INTEGER,\n endTime: Number.MIN_SAFE_INTEGER\n });\n var tallBubbles = Object.values(_this.props.points).filter(function (bubble) {\n return bubble.depth > 1;\n });\n return tallBubbles.filter(function (bubble) {\n return bubble.startTime < newRangeMinMax.startTime && newRangeMinMax.startTime < bubble.endTime && bubble.endTime < newRangeMinMax.endTime || newRangeMinMax.startTime < bubble.startTime && bubble.startTime < newRangeMinMax.endTime && newRangeMinMax.endTime < bubble.endTime || bubble.startTime === newRangeMinMax.startTime && bubble.endTime === newRangeMinMax.endTime;\n }).length === 0;\n };\n\n _this.isSplittingPossible = function () {\n return !Object.values(_this.props.points).reduce(function (allPoints, range) {\n allPoints.add(range.startTime);\n allPoints.add(range.endTime);\n return allPoints;\n }, new Set([])).has(_this.props.currentTime);\n };\n\n _this.splitRange = function () {\n return _this.props.splitRangeAt(_this.props.currentTime);\n };\n\n _this.addMarker = function () {\n _this.props.addMarkerAtTime(_this.props.currentTime);\n\n _this.props.setProjectChanged(false);\n };\n\n _this.getAuthService = function () {\n var annotationPages = _this.props.annotationPages;\n\n if (annotationPages && annotationPages[0] && annotationPages[0].items && annotationPages[0].items[0]) {\n var avResource = Object(_AuthResource_AuthResource__WEBPACK_IMPORTED_MODULE_28__[\"resolveAvResource\"])(annotationPages[0].items[0]);\n return avResource.service;\n }\n };\n\n _this.getOnSave = function () {\n if (!_this.props.callback) {\n return null;\n }\n\n return _this.props.saveProject;\n };\n\n _this.theme = Object(_material_ui_core_styles__WEBPACK_IMPORTED_MODULE_7__[\"createMuiTheme\"])({\n palette: {\n primary: {\n light: '#757ce8',\n main: '#3f50b5',\n dark: '#002884',\n contrastText: '#fff'\n },\n secondary: {\n light: '#d2abf3',\n main: '#C797F0',\n dark: '#8b69a8',\n contrastText: '#000'\n }\n },\n status: {\n danger: 'orange'\n },\n spacing: {\n unit: 4\n },\n shape: {\n borderRadius: 2\n },\n typography: {\n fontSize: 12,\n useNextVariants: true\n },\n mixins: {\n toolbar: {\n minHeight: 36\n }\n }\n });\n return _this;\n }\n\n Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_createClass__WEBPACK_IMPORTED_MODULE_1__[\"default\"])(VariationsMainView, [{\n key: \"render\",\n value: function render() {\n var _points = this.props.points;\n var _this$props = this.props,\n isPlaying = _this$props.isPlaying,\n volume = _this$props.volume,\n currentTime = _this$props.currentTime,\n audioUrl = _this$props.audioUrl,\n runTime = _this$props.runTime,\n manifestLabel = _this$props.manifestLabel,\n manifestSummary = _this$props.manifestSummary,\n homepage = _this$props.homepage,\n homepageLabel = _this$props.homepageLabel,\n isImportOpen = _this$props.isImportOpen,\n isSettingsOpen = _this$props.isSettingsOpen,\n audioError = _this$props.audioError,\n loadingPercent = _this$props.loadingPercent,\n isLoaded = _this$props.isLoaded,\n rangeToEdit = _this$props.rangeToEdit,\n settings = _this$props.settings,\n selectedRanges = _this$props.selectedRanges,\n hasResource = _this$props.hasResource,\n colourPalette = _this$props.colourPalette,\n noFooter = _this$props.noFooter,\n noHeader = _this$props.noHeader,\n noSourceLink = _this$props.noSourceLink,\n zoom = _this$props.zoom;\n return react__WEBPACK_IMPORTED_MODULE_5___default.a.createElement(\"div\", {\n className: \"variations-app\",\n __source: {\n fileName: _jsxFileName,\n lineNumber: 224\n },\n __self: this\n }, react__WEBPACK_IMPORTED_MODULE_5___default.a.createElement(_material_ui_core_styles__WEBPACK_IMPORTED_MODULE_7__[\"MuiThemeProvider\"], {\n theme: this.theme,\n __source: {\n fileName: _jsxFileName,\n lineNumber: 225\n },\n __self: this\n }, react__WEBPACK_IMPORTED_MODULE_5___default.a.createElement(_components_VariationsAppBar_VariationsAppBar__WEBPACK_IMPORTED_MODULE_9__[\"default\"], {\n title: manifestLabel,\n onImportButtonClicked: this.props.showImportModal,\n onSettingsButtonClicked: this.props.showSettingsModal,\n canUndo: this.props.canUndo,\n canRedo: this.props.canRedo,\n onRedo: this.props.onRedo,\n onUndo: this.props.onUndo,\n onSave: this.getOnSave(),\n onTitleChange: function onTitleChange() {},\n hasResource: this.props.hasResource,\n noHeader: this.props.noHeader,\n __source: {\n fileName: _jsxFileName,\n lineNumber: 226\n },\n __self: this\n }), react__WEBPACK_IMPORTED_MODULE_5___default.a.createElement(\"div\", {\n className: \"variations-app__content\",\n __source: {\n fileName: _jsxFileName,\n lineNumber: 239\n },\n __self: this\n }, react__WEBPACK_IMPORTED_MODULE_5___default.a.createElement(_AuthResource_AuthResource__WEBPACK_IMPORTED_MODULE_28__[\"AuthCookieService1\"], {\n key: this.props.url,\n resource: this.props.url,\n service: this.props.authService ? this.props.authService[0] : null,\n __source: {\n fileName: _jsxFileName,\n lineNumber: 240\n },\n __self: this\n }, react__WEBPACK_IMPORTED_MODULE_5___default.a.createElement(_BubbleEditor_BubbleEditor__WEBPACK_IMPORTED_MODULE_17__[\"default\"], {\n key: 'bubble--' + this.props.url,\n __source: {\n fileName: _jsxFileName,\n lineNumber: 247\n },\n __self: this\n }), this.props.url ? react__WEBPACK_IMPORTED_MODULE_5___default.a.createElement(_Audio_Audio__WEBPACK_IMPORTED_MODULE_18__[\"default\"], {\n key: 'audio--' + this.props.url,\n __source: {\n fileName: _jsxFileName,\n lineNumber: 249\n },\n __self: this\n }) : null, react__WEBPACK_IMPORTED_MODULE_5___default.a.createElement(_components_AudioTransportBar_AudioTransportBar__WEBPACK_IMPORTED_MODULE_10__[\"default\"], {\n isPlaying: isPlaying,\n volume: volume,\n currentTime: currentTime,\n runTime: runTime,\n onVolumeChanged: this.props.setVolume,\n onPlay: this.props.play,\n onPause: this.props.pause,\n onNextBubble: this.props.nextBubble,\n onPreviousBubble: this.props.previousBubble,\n onScrubAhead: this.props.fastForward,\n onScrubBackwards: this.props.fastReward,\n onAddBubble: this.isSplittingPossible() ? this.splitRange : null,\n onGroupBubble: selectedRanges.length > 1 && this.isGroupingPossible(selectedRanges) ? this.props.groupSelectedRanges : null,\n onDeleteBubble: selectedRanges.length > 0 && _points.length > 1 && _points.length - selectedRanges.length > 0 ? this.deleteRanges(selectedRanges) : null,\n onAddMarker: this.addMarker,\n zoom: zoom,\n zoomIn: this.props.zoomIn,\n zoomOut: this.props.zoomOut,\n resetZoom: this.props.resetZoom,\n __source: {\n fileName: _jsxFileName,\n lineNumber: 251\n },\n __self: this\n })), react__WEBPACK_IMPORTED_MODULE_5___default.a.createElement(\"div\", {\n className: \"variations-app__metadata-editor\",\n __source: {\n fileName: _jsxFileName,\n lineNumber: 286\n },\n __self: this\n }, react__WEBPACK_IMPORTED_MODULE_5___default.a.createElement(_components_Metadata_Metadata__WEBPACK_IMPORTED_MODULE_11__[\"default\"], {\n colourPalette: colourPalette,\n currentTime: currentTime,\n runTime: runTime,\n manifestLabel: manifestLabel,\n manifestSummary: manifestSummary,\n homepage: homepage,\n homepageLabel: homepageLabel,\n noSourceLink: noSourceLink,\n ranges: _points,\n onEdit: this.props.editMetadata,\n rangeToEdit: rangeToEdit,\n onUpdateRange: this.props.updateRange,\n blackAndWhiteMode: this.props.settings[_constants_project__WEBPACK_IMPORTED_MODULE_22__[\"PROJECT\"].BLACK_N_WHITE],\n projectMetadataEditorOpen: this.props.showMetadataEditor,\n onEditProjectMetadata: this.props.editProjectMetadata,\n onSaveProjectMetadata: this.props.saveProjectMetadata,\n onEraseButtonClicked: this.props.resetDocument,\n canSave: !this.props.callback,\n canErase: !this.props.callback,\n hasResource: this.props.hasResource,\n onSaveButtonClicked: this.props.exportDocument,\n onCancelEditingProjectMetadata: this.props.cancelProjectMetadataEdits,\n url: this.props.url,\n markers: this.props.markers,\n updateMarker: this.props.updateMarker,\n deleteMarker: this.props.deleteMarker,\n updateProjectStatus: this.props.setProjectChanged,\n setCurrentTime: this.props.setCurrentTime,\n undoAll: this.props.canUndo ? this.props.undoAll : null,\n swatch: this.props.colourPalette.colours,\n __source: {\n fileName: _jsxFileName,\n lineNumber: 287\n },\n __self: this\n }), !noFooter && _ref), (audioError.code || !isLoaded) && react__WEBPACK_IMPORTED_MODULE_5___default.a.createElement(_components_ContentOverlay_ContentOverlay__WEBPACK_IMPORTED_MODULE_15__[\"default\"], Object.assign({\n loadingPercent: loadingPercent,\n isLoaded: isLoaded,\n audioUrl: audioUrl\n }, {\n error: audioError,\n __source: {\n fileName: _jsxFileName,\n lineNumber: 324\n },\n __self: this\n }))), (!hasResource || isLoaded) && react__WEBPACK_IMPORTED_MODULE_5___default.a.createElement(_components_AudioImporter_AudioImporter__WEBPACK_IMPORTED_MODULE_12__[\"default\"], {\n open: isImportOpen,\n error: this.props.importError,\n onClose: this.props.url ? this.props.dismissImportModal : null,\n onImport: this.props.importDocument,\n __source: {\n fileName: _jsxFileName,\n lineNumber: 331\n },\n __self: this\n }), react__WEBPACK_IMPORTED_MODULE_5___default.a.createElement(_components_SettingsPopoup_SettingsPopup__WEBPACK_IMPORTED_MODULE_13__[\"default\"], {\n open: isSettingsOpen,\n onClose: this.props.dismissSettingsModal,\n onSave: this.props.updateSettings,\n clearCustomColors: this.props.clearCustomColors,\n settings: settings,\n __source: {\n fileName: _jsxFileName,\n lineNumber: 338\n },\n __self: this\n }), react__WEBPACK_IMPORTED_MODULE_5___default.a.createElement(_components_VerifyDialog_VerifyDialog__WEBPACK_IMPORTED_MODULE_16__[\"default\"], {\n open: this.props.verifyDialog.open,\n title: this.props.verifyDialog.title,\n doCancel: this.props.verifyDialog.doCancel,\n onClose: this.props.confirmNo,\n onProceed: this.props.confirmYes,\n __source: {\n fileName: _jsxFileName,\n lineNumber: 345\n },\n __self: this\n })));\n }\n }]);\n\n return VariationsMainView;\n}(react__WEBPACK_IMPORTED_MODULE_5___default.a.Component);\n\nVariationsMainView.propTypes = {\n updateSettings: prop_types__WEBPACK_IMPORTED_MODULE_6___default.a.func,\n showImportModal: prop_types__WEBPACK_IMPORTED_MODULE_6___default.a.func,\n showSettingsModal: prop_types__WEBPACK_IMPORTED_MODULE_6___default.a.func,\n setVolume: prop_types__WEBPACK_IMPORTED_MODULE_6___default.a.func.isRequired,\n url: prop_types__WEBPACK_IMPORTED_MODULE_6___default.a.string,\n play: prop_types__WEBPACK_IMPORTED_MODULE_6___default.a.func,\n pause: prop_types__WEBPACK_IMPORTED_MODULE_6___default.a.func,\n dismissImportModal: prop_types__WEBPACK_IMPORTED_MODULE_6___default.a.func,\n dismissSettingsModal: prop_types__WEBPACK_IMPORTED_MODULE_6___default.a.func,\n volume: prop_types__WEBPACK_IMPORTED_MODULE_6___default.a.number.isRequired,\n isPlaying: prop_types__WEBPACK_IMPORTED_MODULE_6___default.a.bool.isRequired,\n isLoaded: prop_types__WEBPACK_IMPORTED_MODULE_6___default.a.bool.isRequired,\n importError: prop_types__WEBPACK_IMPORTED_MODULE_6___default.a.string,\n currentTime: prop_types__WEBPACK_IMPORTED_MODULE_6___default.a.number.isRequired,\n runTime: prop_types__WEBPACK_IMPORTED_MODULE_6___default.a.number.isRequired,\n manifestLabel: prop_types__WEBPACK_IMPORTED_MODULE_6___default.a.string.isRequired,\n manifestSummary: prop_types__WEBPACK_IMPORTED_MODULE_6___default.a.string.isRequired,\n homepage: prop_types__WEBPACK_IMPORTED_MODULE_6___default.a.string,\n homepageLabel: prop_types__WEBPACK_IMPORTED_MODULE_6___default.a.string,\n points: prop_types__WEBPACK_IMPORTED_MODULE_6___default.a.array,\n isImportOpen: prop_types__WEBPACK_IMPORTED_MODULE_6___default.a.bool.isRequired,\n isSettingsOpen: prop_types__WEBPACK_IMPORTED_MODULE_6___default.a.bool.isRequired,\n fastForward: prop_types__WEBPACK_IMPORTED_MODULE_6___default.a.func.isRequired,\n fastReward: prop_types__WEBPACK_IMPORTED_MODULE_6___default.a.func.isRequired,\n importDocument: prop_types__WEBPACK_IMPORTED_MODULE_6___default.a.func.isRequired,\n exportDocument: prop_types__WEBPACK_IMPORTED_MODULE_6___default.a.func.isRequired,\n addMarkerAtTime: prop_types__WEBPACK_IMPORTED_MODULE_6___default.a.func.isRequired,\n saveProject: prop_types__WEBPACK_IMPORTED_MODULE_6___default.a.func.isRequired,\n settings: prop_types__WEBPACK_IMPORTED_MODULE_6___default.a.object,\n zoom: prop_types__WEBPACK_IMPORTED_MODULE_6___default.a.number.isRequired\n};\n\nvar mapStateProps = function mapStateProps(state) {\n return {\n volume: state.viewState.volume,\n isPlaying: state.viewState.isPlaying,\n currentTime: state.viewState.currentTime,\n authService: state.canvas.service,\n annotationPages: state.canvas.items,\n url: state.canvas.url,\n runTime: state.viewState.runTime,\n manifestLabel: state.project.title,\n importError: state.project.error,\n manifestSummary: state.project.description,\n homepage: state.project.homepage,\n homepageLabel: state.project.homepageLabel,\n points: Object.values(Object(_reducers_range__WEBPACK_IMPORTED_MODULE_27__[\"getRangeList\"])(state)),\n selectedRanges: Object(_reducers_range__WEBPACK_IMPORTED_MODULE_27__[\"getSelectedRanges\"])(state),\n isImportOpen: state.viewState.isImportOpen,\n isSettingsOpen: state.viewState.isSettingsOpen,\n audioUrl: state.canvas.url,\n audioError: state.canvas.error,\n loadingPercent: state.canvas.loadingPercent,\n isLoaded: state.canvas.isLoaded,\n rangeToEdit: state.viewState.metadataToEdit,\n verifyDialog: state.viewState.verifyDialog,\n showMetadataEditor: state.viewState[_constants_viewState__WEBPACK_IMPORTED_MODULE_23__[\"VIEWSTATE\"].PROJECT_METADATA_EDITOR_OPEN],\n canUndo: state.undoHistory.undoQueue.length > 0,\n canRedo: state.undoHistory.redoQueue.length > 0,\n markers: state.markers.visible ? state.markers.list : {},\n zoom: state.viewState[_constants_viewState__WEBPACK_IMPORTED_MODULE_23__[\"VIEWSTATE\"].ZOOM],\n //settings\n settings: _constants_project__WEBPACK_IMPORTED_MODULE_22__[\"PROJECT_SETTINGS_KEYS\"].reduce(function (acc, next) {\n acc[next] = state.project[next];\n return acc;\n }, {}),\n colourPalette: _config__WEBPACK_IMPORTED_MODULE_30__[\"colourPalettes\"][state.project[_constants_project__WEBPACK_IMPORTED_MODULE_22__[\"PROJECT\"].COLOUR_PALETTE]] || _config__WEBPACK_IMPORTED_MODULE_30__[\"colourPalettes\"].default\n };\n};\n\nvar mapDispatchToProps = {\n //project actions\n importDocument: _actions_project__WEBPACK_IMPORTED_MODULE_20__[\"importDocument\"],\n updateSettings: _actions_project__WEBPACK_IMPORTED_MODULE_20__[\"updateSettings\"],\n resetDocument: _actions_project__WEBPACK_IMPORTED_MODULE_20__[\"resetDocument\"],\n exportDocument: _actions_project__WEBPACK_IMPORTED_MODULE_20__[\"exportDocument\"],\n editProjectMetadata: _actions_viewState__WEBPACK_IMPORTED_MODULE_24__[\"editProjectMetadata\"],\n cancelProjectMetadataEdits: _actions_viewState__WEBPACK_IMPORTED_MODULE_24__[\"cancelProjectMetadataEdits\"],\n saveProjectMetadata: _actions_viewState__WEBPACK_IMPORTED_MODULE_24__[\"saveProjectMetadata\"],\n clearCustomColors: _actions_project__WEBPACK_IMPORTED_MODULE_20__[\"clearCustomColors\"],\n setProjectChanged: _actions_project__WEBPACK_IMPORTED_MODULE_20__[\"setProjectChanged\"],\n //view state actions\n showImportModal: _actions_viewState__WEBPACK_IMPORTED_MODULE_24__[\"showImportModal\"],\n showSettingsModal: _actions_viewState__WEBPACK_IMPORTED_MODULE_24__[\"showSettingsModal\"],\n setVolume: _actions_viewState__WEBPACK_IMPORTED_MODULE_24__[\"setVolume\"],\n play: _actions_viewState__WEBPACK_IMPORTED_MODULE_24__[\"play\"],\n pause: _actions_viewState__WEBPACK_IMPORTED_MODULE_24__[\"pause\"],\n dismissImportModal: _actions_viewState__WEBPACK_IMPORTED_MODULE_24__[\"dismissImportModal\"],\n dismissSettingsModal: _actions_viewState__WEBPACK_IMPORTED_MODULE_24__[\"dismissSettingsModal\"],\n fastForward: _actions_viewState__WEBPACK_IMPORTED_MODULE_24__[\"fastForward\"],\n fastReward: _actions_viewState__WEBPACK_IMPORTED_MODULE_24__[\"fastReward\"],\n editMetadata: _actions_viewState__WEBPACK_IMPORTED_MODULE_24__[\"editMetadata\"],\n previousBubble: _actions_viewState__WEBPACK_IMPORTED_MODULE_24__[\"previousBubble\"],\n nextBubble: _actions_viewState__WEBPACK_IMPORTED_MODULE_24__[\"nextBubble\"],\n confirmYes: _actions_viewState__WEBPACK_IMPORTED_MODULE_24__[\"confirmYes\"],\n confirmNo: _actions_viewState__WEBPACK_IMPORTED_MODULE_24__[\"confirmNo\"],\n setCurrentTime: _actions_viewState__WEBPACK_IMPORTED_MODULE_24__[\"setCurrentTime\"],\n zoomIn: _actions_viewState__WEBPACK_IMPORTED_MODULE_24__[\"zoomIn\"],\n zoomOut: _actions_viewState__WEBPACK_IMPORTED_MODULE_24__[\"zoomOut\"],\n resetZoom: _actions_viewState__WEBPACK_IMPORTED_MODULE_24__[\"resetZoom\"],\n //range\n splitRangeAt: _actions_range__WEBPACK_IMPORTED_MODULE_19__[\"splitRangeAt\"],\n groupSelectedRanges: _actions_range__WEBPACK_IMPORTED_MODULE_19__[\"groupSelectedRanges\"],\n deleteRanges: _actions_range__WEBPACK_IMPORTED_MODULE_19__[\"scheduleDeleteRanges\"],\n updateRange: _actions_range__WEBPACK_IMPORTED_MODULE_19__[\"updateRange\"],\n updateMarker: _actions_markers__WEBPACK_IMPORTED_MODULE_25__[\"updateMarker\"],\n deleteMarker: _actions_markers__WEBPACK_IMPORTED_MODULE_25__[\"deleteMarker\"],\n // markers\n addMarkerAtTime: _actions_markers__WEBPACK_IMPORTED_MODULE_25__[\"addMarkerAtTime\"],\n // Undo\n onUndo: redux_undo_redo__WEBPACK_IMPORTED_MODULE_29__[\"actions\"].undo,\n onRedo: redux_undo_redo__WEBPACK_IMPORTED_MODULE_29__[\"actions\"].redo,\n undoAll: _actions_viewState__WEBPACK_IMPORTED_MODULE_24__[\"undoAll\"],\n // Export\n saveProject: _actions_project__WEBPACK_IMPORTED_MODULE_20__[\"saveProject\"]\n};\n/* harmony default export */ __webpack_exports__[\"default\"] = (Object(react_redux__WEBPACK_IMPORTED_MODULE_8__[\"connect\"])(mapStateProps, mapDispatchToProps)(VariationsMainView));\n\n//# sourceURL=webpack:///./src/containers/VariationsMainView/VariationsMainView.js?"); /***/ }), @@ -11095,7 +11095,7 @@ import "../iiif-timeliner-styles.css" /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; - eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./node_modules/babel-preset-react-app/node_modules/@babel/runtime/helpers/esm/defineProperty */ \"./node_modules/babel-preset-react-app/node_modules/@babel/runtime/helpers/esm/defineProperty.js\");\n/* harmony import */ var immutability_helper__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! immutability-helper */ \"./node_modules/immutability-helper/index.js\");\n/* harmony import */ var immutability_helper__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(immutability_helper__WEBPACK_IMPORTED_MODULE_1__);\n/* harmony import */ var _constants_project__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../constants/project */ \"./src/constants/project.js\");\n\n\n\n\nvar project = function project() {\n var _update, _update2, _update3, _update4;\n\n var state = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : _constants_project__WEBPACK_IMPORTED_MODULE_2__[\"DEFAULT_PROJECT_STATE\"];\n var action = arguments.length > 1 ? arguments[1] : undefined;\n\n switch (action.type) {\n case _constants_project__WEBPACK_IMPORTED_MODULE_2__[\"UPDATE_SETTINGS\"]:\n return immutability_helper__WEBPACK_IMPORTED_MODULE_1___default()(state, {\n $merge: action.payload\n });\n\n case _constants_project__WEBPACK_IMPORTED_MODULE_2__[\"SET_LANGUAGE\"]:\n return immutability_helper__WEBPACK_IMPORTED_MODULE_1___default()(state, (_update = {}, Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__[\"default\"])(_update, _constants_project__WEBPACK_IMPORTED_MODULE_2__[\"PROJECT\"].LANGUAGE, {\n $set: action.payload.language\n }), Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__[\"default\"])(_update, _constants_project__WEBPACK_IMPORTED_MODULE_2__[\"PROJECT\"].IS_SAVED, {\n $set: false\n }), _update));\n\n case _constants_project__WEBPACK_IMPORTED_MODULE_2__[\"SET_TITLE\"]:\n return immutability_helper__WEBPACK_IMPORTED_MODULE_1___default()(state, (_update2 = {}, Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__[\"default\"])(_update2, _constants_project__WEBPACK_IMPORTED_MODULE_2__[\"PROJECT\"].TITLE, {\n $set: action.payload.title\n }), Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__[\"default\"])(_update2, _constants_project__WEBPACK_IMPORTED_MODULE_2__[\"PROJECT\"].IS_SAVED, {\n $set: false\n }), _update2));\n\n case _constants_project__WEBPACK_IMPORTED_MODULE_2__[\"SET_DESCRIPTION\"]:\n return immutability_helper__WEBPACK_IMPORTED_MODULE_1___default()(state, (_update3 = {}, Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__[\"default\"])(_update3, _constants_project__WEBPACK_IMPORTED_MODULE_2__[\"PROJECT\"].DESCRIPTION, {\n $set: action.payload.description\n }), Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__[\"default\"])(_update3, _constants_project__WEBPACK_IMPORTED_MODULE_2__[\"PROJECT\"].IS_SAVED, {\n $set: false\n }), _update3));\n\n case _constants_project__WEBPACK_IMPORTED_MODULE_2__[\"IMPORT_ERROR\"]:\n return immutability_helper__WEBPACK_IMPORTED_MODULE_1___default()(state, {\n error: {\n $set: action.payload.error\n }\n });\n\n case _constants_project__WEBPACK_IMPORTED_MODULE_2__[\"RESET_DOCUMENT\"]:\n return state;\n\n case _constants_project__WEBPACK_IMPORTED_MODULE_2__[\"LOAD_PROJECT\"]:\n return immutability_helper__WEBPACK_IMPORTED_MODULE_1___default()(immutability_helper__WEBPACK_IMPORTED_MODULE_1___default()({}, {\n $merge: _constants_project__WEBPACK_IMPORTED_MODULE_2__[\"DEFAULT_PROJECT_STATE\"]\n }), {\n $merge: action.state\n });\n\n case _constants_project__WEBPACK_IMPORTED_MODULE_2__[\"SET_COLOUR_PALETTE\"]:\n return immutability_helper__WEBPACK_IMPORTED_MODULE_1___default()(state, (_update4 = {}, Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__[\"default\"])(_update4, _constants_project__WEBPACK_IMPORTED_MODULE_2__[\"PROJECT\"].COLOUR_PALETTE, action.payload.pallet), Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__[\"default\"])(_update4, _constants_project__WEBPACK_IMPORTED_MODULE_2__[\"PROJECT\"].IS_SAVED, {\n $set: false\n }), _update4));\n\n case _constants_project__WEBPACK_IMPORTED_MODULE_2__[\"EXPORT_DOCUMENT\"]:\n case _constants_project__WEBPACK_IMPORTED_MODULE_2__[\"SAVE_PROJECT\"]:\n return immutability_helper__WEBPACK_IMPORTED_MODULE_1___default()(state, Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__[\"default\"])({}, _constants_project__WEBPACK_IMPORTED_MODULE_2__[\"PROJECT\"].IS_SAVED, {\n $set: true\n }));\n\n case _constants_project__WEBPACK_IMPORTED_MODULE_2__[\"SET_IS_SAVED\"]:\n return immutability_helper__WEBPACK_IMPORTED_MODULE_1___default()(state, Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__[\"default\"])({}, _constants_project__WEBPACK_IMPORTED_MODULE_2__[\"PROJECT\"].IS_SAVED, {\n $set: action.payload.isSaved\n }));\n\n default:\n return state;\n }\n};\n\n/* harmony default export */ __webpack_exports__[\"default\"] = (project);\n\n//# sourceURL=webpack:///./src/reducers/project.js?"); + eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./node_modules/babel-preset-react-app/node_modules/@babel/runtime/helpers/esm/defineProperty */ \"./node_modules/babel-preset-react-app/node_modules/@babel/runtime/helpers/esm/defineProperty.js\");\n/* harmony import */ var immutability_helper__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! immutability-helper */ \"./node_modules/immutability-helper/index.js\");\n/* harmony import */ var immutability_helper__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(immutability_helper__WEBPACK_IMPORTED_MODULE_1__);\n/* harmony import */ var _constants_project__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../constants/project */ \"./src/constants/project.js\");\n\n\n\n\nvar project = function project() {\n var _update, _update2, _update3, _update4;\n\n var state = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : _constants_project__WEBPACK_IMPORTED_MODULE_2__[\"DEFAULT_PROJECT_STATE\"];\n var action = arguments.length > 1 ? arguments[1] : undefined;\n\n switch (action.type) {\n case _constants_project__WEBPACK_IMPORTED_MODULE_2__[\"UPDATE_SETTINGS\"]:\n return immutability_helper__WEBPACK_IMPORTED_MODULE_1___default()(state, {\n $merge: action.payload\n });\n\n case _constants_project__WEBPACK_IMPORTED_MODULE_2__[\"SET_LANGUAGE\"]:\n return immutability_helper__WEBPACK_IMPORTED_MODULE_1___default()(state, (_update = {}, Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__[\"default\"])(_update, _constants_project__WEBPACK_IMPORTED_MODULE_2__[\"PROJECT\"].LANGUAGE, {\n $set: action.payload.language\n }), Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__[\"default\"])(_update, _constants_project__WEBPACK_IMPORTED_MODULE_2__[\"PROJECT\"].IS_CHANGED, {\n $set: false\n }), _update));\n\n case _constants_project__WEBPACK_IMPORTED_MODULE_2__[\"SET_TITLE\"]:\n return immutability_helper__WEBPACK_IMPORTED_MODULE_1___default()(state, (_update2 = {}, Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__[\"default\"])(_update2, _constants_project__WEBPACK_IMPORTED_MODULE_2__[\"PROJECT\"].TITLE, {\n $set: action.payload.title\n }), Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__[\"default\"])(_update2, _constants_project__WEBPACK_IMPORTED_MODULE_2__[\"PROJECT\"].IS_CHANGED, {\n $set: false\n }), _update2));\n\n case _constants_project__WEBPACK_IMPORTED_MODULE_2__[\"SET_DESCRIPTION\"]:\n return immutability_helper__WEBPACK_IMPORTED_MODULE_1___default()(state, (_update3 = {}, Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__[\"default\"])(_update3, _constants_project__WEBPACK_IMPORTED_MODULE_2__[\"PROJECT\"].DESCRIPTION, {\n $set: action.payload.description\n }), Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__[\"default\"])(_update3, _constants_project__WEBPACK_IMPORTED_MODULE_2__[\"PROJECT\"].IS_CHANGED, {\n $set: false\n }), _update3));\n\n case _constants_project__WEBPACK_IMPORTED_MODULE_2__[\"IMPORT_ERROR\"]:\n return immutability_helper__WEBPACK_IMPORTED_MODULE_1___default()(state, {\n error: {\n $set: action.payload.error\n }\n });\n\n case _constants_project__WEBPACK_IMPORTED_MODULE_2__[\"RESET_DOCUMENT\"]:\n return state;\n\n case _constants_project__WEBPACK_IMPORTED_MODULE_2__[\"LOAD_PROJECT\"]:\n return immutability_helper__WEBPACK_IMPORTED_MODULE_1___default()(immutability_helper__WEBPACK_IMPORTED_MODULE_1___default()({}, {\n $merge: _constants_project__WEBPACK_IMPORTED_MODULE_2__[\"DEFAULT_PROJECT_STATE\"]\n }), {\n $merge: action.state\n });\n\n case _constants_project__WEBPACK_IMPORTED_MODULE_2__[\"SET_COLOUR_PALETTE\"]:\n return immutability_helper__WEBPACK_IMPORTED_MODULE_1___default()(state, (_update4 = {}, Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__[\"default\"])(_update4, _constants_project__WEBPACK_IMPORTED_MODULE_2__[\"PROJECT\"].COLOUR_PALETTE, action.payload.pallet), Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__[\"default\"])(_update4, _constants_project__WEBPACK_IMPORTED_MODULE_2__[\"PROJECT\"].IS_CHANGED, {\n $set: false\n }), _update4));\n\n case _constants_project__WEBPACK_IMPORTED_MODULE_2__[\"EXPORT_DOCUMENT\"]:\n case _constants_project__WEBPACK_IMPORTED_MODULE_2__[\"SAVE_PROJECT\"]:\n return immutability_helper__WEBPACK_IMPORTED_MODULE_1___default()(state, Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__[\"default\"])({}, _constants_project__WEBPACK_IMPORTED_MODULE_2__[\"PROJECT\"].IS_CHANGED, {\n $set: true\n }));\n\n case _constants_project__WEBPACK_IMPORTED_MODULE_2__[\"PROJECT_CHANGED\"]:\n return immutability_helper__WEBPACK_IMPORTED_MODULE_1___default()(state, Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_0__[\"default\"])({}, _constants_project__WEBPACK_IMPORTED_MODULE_2__[\"PROJECT\"].IS_CHANGED, {\n $set: action.payload.isSaved\n }));\n\n default:\n return state;\n }\n};\n\n/* harmony default export */ __webpack_exports__[\"default\"] = (project);\n\n//# sourceURL=webpack:///./src/reducers/project.js?"); /***/ }), @@ -11143,7 +11143,7 @@ import "../iiif-timeliner-styles.css" /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; - eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"showConfirmation\", function() { return showConfirmation; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"default\", function() { return root; });\n/* harmony import */ var _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_toConsumableArray__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./node_modules/babel-preset-react-app/node_modules/@babel/runtime/helpers/esm/toConsumableArray */ \"./node_modules/babel-preset-react-app/node_modules/@babel/runtime/helpers/esm/toConsumableArray.js\");\n/* harmony import */ var _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./node_modules/babel-preset-react-app/node_modules/@babel/runtime/regenerator */ \"./node_modules/babel-preset-react-app/node_modules/@babel/runtime/regenerator/index.js\");\n/* harmony import */ var _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1__);\n/* harmony import */ var redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! redux-saga/effects */ \"./node_modules/redux-saga/es/effects.js\");\n/* harmony import */ var _constants_range__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ../constants/range */ \"./src/constants/range.js\");\n/* harmony import */ var _utils_iiifLoader__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ../utils/iiifLoader */ \"./src/utils/iiifLoader.js\");\n/* harmony import */ var redux_undo_redo__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! redux-undo-redo */ \"./node_modules/redux-undo-redo/lib/index.js\");\n/* harmony import */ var redux_undo_redo__WEBPACK_IMPORTED_MODULE_5___default = /*#__PURE__*/__webpack_require__.n(redux_undo_redo__WEBPACK_IMPORTED_MODULE_5__);\n/* harmony import */ var _constants_project__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ../constants/project */ \"./src/constants/project.js\");\n/* harmony import */ var _utils_iiifSaver__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ../utils/iiifSaver */ \"./src/utils/iiifSaver.js\");\n/* harmony import */ var _actions_project__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(/*! ../actions/project */ \"./src/actions/project.js\");\n/* harmony import */ var _actions_canvas__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(/*! ../actions/canvas */ \"./src/actions/canvas.js\");\n/* harmony import */ var _actions_range__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(/*! ../actions/range */ \"./src/actions/range.js\");\n/* harmony import */ var _actions_viewState__WEBPACK_IMPORTED_MODULE_11__ = __webpack_require__(/*! ../actions/viewState */ \"./src/actions/viewState.js\");\n/* harmony import */ var _constants_viewState__WEBPACK_IMPORTED_MODULE_12__ = __webpack_require__(/*! ../constants/viewState */ \"./src/constants/viewState.js\");\n/* harmony import */ var _utils_iiifSerializer__WEBPACK_IMPORTED_MODULE_13__ = __webpack_require__(/*! ../utils/iiifSerializer */ \"./src/utils/iiifSerializer.js\");\n/* harmony import */ var _utils_fileDownload__WEBPACK_IMPORTED_MODULE_14__ = __webpack_require__(/*! ../utils/fileDownload */ \"./src/utils/fileDownload.js\");\n/* harmony import */ var _reducers_range__WEBPACK_IMPORTED_MODULE_15__ = __webpack_require__(/*! ../reducers/range */ \"./src/reducers/range.js\");\n/* harmony import */ var _range_saga__WEBPACK_IMPORTED_MODULE_16__ = __webpack_require__(/*! ./range-saga */ \"./src/sagas/range-saga.js\");\n/* harmony import */ var _constants_markers__WEBPACK_IMPORTED_MODULE_17__ = __webpack_require__(/*! ../constants/markers */ \"./src/constants/markers.js\");\n/* harmony import */ var _actions_markers__WEBPACK_IMPORTED_MODULE_18__ = __webpack_require__(/*! ../actions/markers */ \"./src/actions/markers.js\");\n\n\n\nvar _marked =\n/*#__PURE__*/\n_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default.a.mark(setIsSavedStatus),\n _marked2 =\n/*#__PURE__*/\n_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default.a.mark(importDocument),\n _marked3 =\n/*#__PURE__*/\n_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default.a.mark(showConfirmation),\n _marked4 =\n/*#__PURE__*/\n_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default.a.mark(resetDocument),\n _marked5 =\n/*#__PURE__*/\n_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default.a.mark(exportDocument),\n _marked6 =\n/*#__PURE__*/\n_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default.a.mark(saveProjectMetadata),\n _marked7 =\n/*#__PURE__*/\n_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default.a.mark(selectMarker),\n _marked8 =\n/*#__PURE__*/\n_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default.a.mark(updateMarkerTime),\n _marked9 =\n/*#__PURE__*/\n_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default.a.mark(updateSettings),\n _marked10 =\n/*#__PURE__*/\n_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default.a.mark(zoomSideEffects),\n _marked11 =\n/*#__PURE__*/\n_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default.a.mark(timeWithinSelection),\n _marked12 =\n/*#__PURE__*/\n_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default.a.mark(zoomToSelection),\n _marked13 =\n/*#__PURE__*/\n_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default.a.mark(zoomTowards),\n _marked14 =\n/*#__PURE__*/\n_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default.a.mark(zoomInOut),\n _marked15 =\n/*#__PURE__*/\n_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default.a.mark(saveProject),\n _marked16 =\n/*#__PURE__*/\n_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default.a.mark(undoAll),\n _marked17 =\n/*#__PURE__*/\n_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default.a.mark(root);\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nvar getDuration = function getDuration(state) {\n return state.viewState.runTime;\n};\n\nvar getCurrentTime = function getCurrentTime(state) {\n return state.viewState.currentTime;\n};\n\nfunction setIsSavedStatus() {\n return _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default.a.wrap(function setIsSavedStatus$(_context) {\n while (1) {\n switch (_context.prev = _context.next) {\n case 0:\n _context.next = 2;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"put\"])(Object(_actions_project__WEBPACK_IMPORTED_MODULE_8__[\"setProjectStatus\"])(false));\n\n case 2:\n case \"end\":\n return _context.stop();\n }\n }\n }, _marked);\n}\n\nfunction importDocument(_ref) {\n var manifest, source, _ref2, viewState, loadedState;\n\n return _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default.a.wrap(function importDocument$(_context2) {\n while (1) {\n switch (_context2.prev = _context2.next) {\n case 0:\n manifest = _ref.manifest, source = _ref.source;\n _context2.next = 3;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"select\"])();\n\n case 3:\n _ref2 = _context2.sent;\n viewState = _ref2.viewState;\n\n if (!(viewState.source === source)) {\n _context2.next = 7;\n break;\n }\n\n return _context2.abrupt(\"return\");\n\n case 7:\n _context2.next = 9;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"put\"])(redux_undo_redo__WEBPACK_IMPORTED_MODULE_5__[\"actions\"].clear());\n\n case 9:\n _context2.prev = 9;\n loadedState = Object(_utils_iiifLoader__WEBPACK_IMPORTED_MODULE_4__[\"loadProjectState\"])(manifest);\n _context2.next = 13;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"put\"])(Object(_actions_canvas__WEBPACK_IMPORTED_MODULE_9__[\"unloadAudio\"])());\n\n case 13:\n _context2.next = 15;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"put\"])(Object(_actions_project__WEBPACK_IMPORTED_MODULE_8__[\"loadProject\"])(loadedState.project));\n\n case 15:\n _context2.next = 17;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"put\"])(Object(_actions_viewState__WEBPACK_IMPORTED_MODULE_11__[\"loadViewState\"])(loadedState.viewState));\n\n case 17:\n _context2.next = 19;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"put\"])(Object(_actions_canvas__WEBPACK_IMPORTED_MODULE_9__[\"loadCanvas\"])(loadedState.canvas));\n\n case 19:\n _context2.next = 21;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"put\"])(Object(_actions_range__WEBPACK_IMPORTED_MODULE_10__[\"importRanges\"])(loadedState.range));\n\n case 21:\n _context2.next = 23;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"put\"])(Object(_actions_markers__WEBPACK_IMPORTED_MODULE_18__[\"clearMarkers\"])());\n\n case 23:\n _context2.next = 25;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"put\"])(Object(_actions_markers__WEBPACK_IMPORTED_MODULE_18__[\"importMarkers\"])(Object(_utils_iiifLoader__WEBPACK_IMPORTED_MODULE_4__[\"parseMarkers\"])(manifest)));\n\n case 25:\n _context2.next = 32;\n break;\n\n case 27:\n _context2.prev = 27;\n _context2.t0 = _context2[\"catch\"](9);\n console.error(_context2.t0);\n _context2.next = 32;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"put\"])(Object(_actions_project__WEBPACK_IMPORTED_MODULE_8__[\"importError\"])(_context2.t0));\n\n case 32:\n case \"end\":\n return _context2.stop();\n }\n }\n }, _marked2, null, [[9, 27]]);\n}\n\nfunction showConfirmation(message) {\n var doCancel,\n _ref3,\n yes,\n _args3 = arguments;\n\n return _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default.a.wrap(function showConfirmation$(_context3) {\n while (1) {\n switch (_context3.prev = _context3.next) {\n case 0:\n doCancel = _args3.length > 1 && _args3[1] !== undefined ? _args3[1] : true;\n _context3.next = 3;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"put\"])(Object(_actions_viewState__WEBPACK_IMPORTED_MODULE_11__[\"openVerifyDialog\"])(message, doCancel));\n\n case 3:\n _context3.next = 5;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"race\"])({\n yes: Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"take\"])(_constants_viewState__WEBPACK_IMPORTED_MODULE_12__[\"CONFIRM_YES\"]),\n no: Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"take\"])(_constants_viewState__WEBPACK_IMPORTED_MODULE_12__[\"CONFIRM_NO\"])\n });\n\n case 5:\n _ref3 = _context3.sent;\n yes = _ref3.yes;\n _context3.next = 9;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"put\"])(Object(_actions_viewState__WEBPACK_IMPORTED_MODULE_11__[\"closeVerifyDialog\"])());\n\n case 9:\n return _context3.abrupt(\"return\", !!yes);\n\n case 10:\n case \"end\":\n return _context3.stop();\n }\n }\n }, _marked3);\n}\n\nfunction resetDocument() {\n var confirmed, rangeIds, duration;\n return _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default.a.wrap(function resetDocument$(_context4) {\n while (1) {\n switch (_context4.prev = _context4.next) {\n case 0:\n _context4.next = 2;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"call\"])(showConfirmation, 'Are you sure you want to delete all sections?');\n\n case 2:\n confirmed = _context4.sent;\n\n if (!confirmed) {\n _context4.next = 14;\n break;\n }\n\n _context4.next = 6;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"select\"])(function (s) {\n return Object.keys(Object(_reducers_range__WEBPACK_IMPORTED_MODULE_15__[\"getRangeList\"])(s));\n });\n\n case 6:\n rangeIds = _context4.sent;\n _context4.next = 9;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"put\"])(redux_undo_redo__WEBPACK_IMPORTED_MODULE_5__[\"actions\"].clear());\n\n case 9:\n _context4.next = 11;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"select\"])(getDuration);\n\n case 11:\n duration = _context4.sent;\n _context4.next = 14;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"put\"])(Object(_actions_range__WEBPACK_IMPORTED_MODULE_10__[\"rangeMutations\"])([].concat(Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_toConsumableArray__WEBPACK_IMPORTED_MODULE_0__[\"default\"])(rangeIds.map(function (range) {\n return {\n type: 'DELETE_RANGE',\n payload: {\n id: range\n }\n };\n })), [Object(_actions_range__WEBPACK_IMPORTED_MODULE_10__[\"createRange\"])({\n startTime: 0,\n endTime: duration\n })])));\n\n case 14:\n case \"end\":\n return _context4.stop();\n }\n }\n }, _marked4);\n}\n\nfunction exportDocument() {\n var state, label, outputJSON;\n return _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default.a.wrap(function exportDocument$(_context5) {\n while (1) {\n switch (_context5.prev = _context5.next) {\n case 0:\n _context5.next = 2;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"select\"])();\n\n case 2:\n state = _context5.sent;\n _context5.next = 5;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"select\"])(function (s) {\n return \"\".concat(s.project[_constants_project__WEBPACK_IMPORTED_MODULE_6__[\"PROJECT\"].TITLE].replace(/[ ,.'\"]/g, '_') || 'manifest', \".json\");\n });\n\n case 5:\n label = _context5.sent;\n outputJSON = Object(_utils_iiifSaver__WEBPACK_IMPORTED_MODULE_7__[\"default\"])(state);\n Object(_utils_fileDownload__WEBPACK_IMPORTED_MODULE_14__[\"immediateDownload\"])(label, Object(_utils_iiifSerializer__WEBPACK_IMPORTED_MODULE_13__[\"serialize\"])(outputJSON));\n\n case 8:\n case \"end\":\n return _context5.stop();\n }\n }\n }, _marked5);\n}\n\nfunction saveProjectMetadata(_ref4) {\n var metadata, _ref5, title, description, manifestLabel, manifestSummary;\n\n return _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default.a.wrap(function saveProjectMetadata$(_context6) {\n while (1) {\n switch (_context6.prev = _context6.next) {\n case 0:\n metadata = _ref4.metadata;\n _context6.next = 3;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"select\"])(function (state) {\n return state.project;\n });\n\n case 3:\n _ref5 = _context6.sent;\n title = _ref5.title;\n description = _ref5.description;\n manifestLabel = metadata.manifestLabel, manifestSummary = metadata.manifestSummary;\n\n if (!(title !== manifestLabel)) {\n _context6.next = 10;\n break;\n }\n\n _context6.next = 10;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"put\"])(Object(_actions_project__WEBPACK_IMPORTED_MODULE_8__[\"setTitle\"])(manifestLabel));\n\n case 10:\n if (!(description !== manifestSummary)) {\n _context6.next = 13;\n break;\n }\n\n _context6.next = 13;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"put\"])(Object(_actions_project__WEBPACK_IMPORTED_MODULE_8__[\"setDescription\"])(manifestSummary));\n\n case 13:\n _context6.next = 15;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"put\"])(Object(_actions_viewState__WEBPACK_IMPORTED_MODULE_11__[\"cancelProjectMetadataEdits\"])());\n\n case 15:\n _context6.next = 17;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"call\"])(setIsSavedStatus);\n\n case 17:\n case \"end\":\n return _context6.stop();\n }\n }\n }, _marked6);\n}\n\nfunction selectMarker(_ref6) {\n var id, marker, startPlaying;\n return _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default.a.wrap(function selectMarker$(_context7) {\n while (1) {\n switch (_context7.prev = _context7.next) {\n case 0:\n id = _ref6.payload.id;\n _context7.next = 3;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"select\"])(function (state) {\n return state.markers.list[id];\n });\n\n case 3:\n marker = _context7.sent;\n _context7.next = 6;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"select\"])(function (state) {\n return state.project[_constants_project__WEBPACK_IMPORTED_MODULE_6__[\"PROJECT\"].START_PLAYING_WHEN_BUBBLES_CLICKED];\n });\n\n case 6:\n startPlaying = _context7.sent;\n _context7.next = 9;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"put\"])(Object(_actions_viewState__WEBPACK_IMPORTED_MODULE_11__[\"setCurrentTime\"])(marker.time));\n\n case 9:\n if (!startPlaying) {\n _context7.next = 12;\n break;\n }\n\n _context7.next = 12;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"put\"])(Object(_actions_viewState__WEBPACK_IMPORTED_MODULE_11__[\"play\"])());\n\n case 12:\n case \"end\":\n return _context7.stop();\n }\n }\n }, _marked7);\n}\n\nfunction updateMarkerTime(_ref7) {\n var _ref7$payload, id, time;\n\n return _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default.a.wrap(function updateMarkerTime$(_context8) {\n while (1) {\n switch (_context8.prev = _context8.next) {\n case 0:\n _ref7$payload = _ref7.payload, id = _ref7$payload.id, time = _ref7$payload.time;\n\n if (!time) {\n _context8.next = 4;\n break;\n }\n\n _context8.next = 4;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"put\"])(Object(_actions_viewState__WEBPACK_IMPORTED_MODULE_11__[\"setCurrentTime\"])(time));\n\n case 4:\n _context8.next = 6;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"call\"])(setIsSavedStatus);\n\n case 6:\n case \"end\":\n return _context8.stop();\n }\n }\n }, _marked8);\n}\n\nfunction updateSettings(_ref8) {\n var payload;\n return _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default.a.wrap(function updateSettings$(_context9) {\n while (1) {\n switch (_context9.prev = _context9.next) {\n case 0:\n payload = _ref8.payload;\n\n if (!payload.showMarkers) {\n _context9.next = 6;\n break;\n }\n\n _context9.next = 4;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"put\"])(Object(_actions_markers__WEBPACK_IMPORTED_MODULE_18__[\"showMarkers\"])());\n\n case 4:\n _context9.next = 8;\n break;\n\n case 6:\n _context9.next = 8;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"put\"])(Object(_actions_markers__WEBPACK_IMPORTED_MODULE_18__[\"hideMarkers\"])());\n\n case 8:\n _context9.next = 10;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"call\"])(setIsSavedStatus);\n\n case 10:\n case \"end\":\n return _context9.stop();\n }\n }\n }, _marked9);\n}\n\nfunction zoomSideEffects() {\n var zoomA, duration, _ref9, currentTime, viewportWidth, zoom, x, sliderWidth, percentThrough, maxMiddle, pixelThrough, from, to, isVisible, shouldZoomToRange, targetPan;\n\n return _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default.a.wrap(function zoomSideEffects$(_context10) {\n while (1) {\n switch (_context10.prev = _context10.next) {\n case 0:\n _context10.next = 2;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"select\"])(function (state) {\n return state.viewState.zoom;\n });\n\n case 2:\n zoomA = _context10.sent;\n _context10.next = 5;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"select\"])(getDuration);\n\n case 5:\n duration = _context10.sent;\n\n if (!(zoomA === 1)) {\n _context10.next = 8;\n break;\n }\n\n return _context10.abrupt(\"return\");\n\n case 8:\n if (false) {}\n\n _context10.next = 11;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"take\"])(_constants_viewState__WEBPACK_IMPORTED_MODULE_12__[\"SET_CURRENT_TIME\"]);\n\n case 11:\n _ref9 = _context10.sent;\n currentTime = _ref9.payload.currentTime;\n _context10.next = 15;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"select\"])(getViewerWidth);\n\n case 15:\n viewportWidth = _context10.sent;\n _context10.next = 18;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"select\"])(function (state) {\n return state.viewState.zoom;\n });\n\n case 18:\n zoom = _context10.sent;\n _context10.next = 21;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"select\"])(function (state) {\n return state.viewState.x;\n });\n\n case 21:\n x = _context10.sent;\n\n if (!(zoom === 1)) {\n _context10.next = 24;\n break;\n }\n\n return _context10.abrupt(\"return\");\n\n case 24:\n sliderWidth = viewportWidth * zoom;\n percentThrough = currentTime / duration;\n maxMiddle = sliderWidth - viewportWidth;\n pixelThrough = percentThrough * sliderWidth;\n from = Math.floor(x) - 20;\n to = Math.ceil(x + viewportWidth) + 20;\n isVisible = pixelThrough >= from && pixelThrough <= to; // If its not visible, pan to the middle.\n\n if (!(isVisible === false)) {\n _context10.next = 51;\n break;\n }\n\n _context10.next = 34;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"call\"])(timeWithinSelection, currentTime);\n\n case 34:\n shouldZoomToRange = _context10.sent;\n\n if (!shouldZoomToRange) {\n _context10.next = 39;\n break;\n }\n\n _context10.next = 38;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"call\"])(zoomToSelection);\n\n case 38:\n return _context10.abrupt(\"return\");\n\n case 39:\n targetPan = pixelThrough - viewportWidth / 2;\n\n if (!(targetPan <= 0)) {\n _context10.next = 44;\n break;\n }\n\n _context10.next = 43;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"put\"])(Object(_actions_viewState__WEBPACK_IMPORTED_MODULE_11__[\"panToPosition\"])(0));\n\n case 43:\n return _context10.abrupt(\"return\");\n\n case 44:\n if (!(targetPan >= maxMiddle)) {\n _context10.next = 48;\n break;\n }\n\n _context10.next = 47;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"put\"])(Object(_actions_viewState__WEBPACK_IMPORTED_MODULE_11__[\"panToPosition\"])(maxMiddle));\n\n case 47:\n return _context10.abrupt(\"return\");\n\n case 48:\n _context10.next = 50;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"put\"])(Object(_actions_viewState__WEBPACK_IMPORTED_MODULE_11__[\"panToPosition\"])(targetPan));\n\n case 50:\n return _context10.abrupt(\"return\");\n\n case 51:\n _context10.next = 8;\n break;\n\n case 53:\n case \"end\":\n return _context10.stop();\n }\n }\n }, _marked10);\n}\n\nvar getViewerWidth = function getViewerWidth(state) {\n return state.viewState.viewerWidth;\n};\n\nfunction timeWithinSelection(time) {\n var selectedRangeIds, selectedRanges, startTime, endTime;\n return _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default.a.wrap(function timeWithinSelection$(_context11) {\n while (1) {\n switch (_context11.prev = _context11.next) {\n case 0:\n _context11.next = 2;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"select\"])(_reducers_range__WEBPACK_IMPORTED_MODULE_15__[\"getSelectedRanges\"]);\n\n case 2:\n selectedRangeIds = _context11.sent;\n\n if (!(selectedRangeIds.length <= 1)) {\n _context11.next = 5;\n break;\n }\n\n return _context11.abrupt(\"return\", false);\n\n case 5:\n _context11.next = 7;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"select\"])(Object(_reducers_range__WEBPACK_IMPORTED_MODULE_15__[\"getRangesByIds\"])(selectedRangeIds));\n\n case 7:\n selectedRanges = _context11.sent;\n startTime = Math.min.apply(Math, Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_toConsumableArray__WEBPACK_IMPORTED_MODULE_0__[\"default\"])(selectedRanges.map(function (range) {\n return range.startTime;\n })));\n endTime = Math.max.apply(Math, Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_toConsumableArray__WEBPACK_IMPORTED_MODULE_0__[\"default\"])(selectedRanges.map(function (range) {\n return range.endTime;\n })));\n return _context11.abrupt(\"return\", time >= startTime && time <= endTime);\n\n case 11:\n case \"end\":\n return _context11.stop();\n }\n }\n }, _marked11);\n}\n\nfunction zoomToSelection(action) {\n var selectedRangeIds, selectedRanges, duration, viewerWidth, startTime, endTime, percentVisible, percentStart, targetZoom, targetPixelStart;\n return _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default.a.wrap(function zoomToSelection$(_context12) {\n while (1) {\n switch (_context12.prev = _context12.next) {\n case 0:\n _context12.next = 2;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"select\"])(_reducers_range__WEBPACK_IMPORTED_MODULE_15__[\"getSelectedRanges\"]);\n\n case 2:\n selectedRangeIds = _context12.sent;\n\n if (!(selectedRangeIds.length <= 1 || action.payload.deselectOthers)) {\n _context12.next = 5;\n break;\n }\n\n return _context12.abrupt(\"return\", false);\n\n case 5:\n _context12.next = 7;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"select\"])(Object(_reducers_range__WEBPACK_IMPORTED_MODULE_15__[\"getRangesByIds\"])(selectedRangeIds));\n\n case 7:\n selectedRanges = _context12.sent;\n _context12.next = 10;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"select\"])(getDuration);\n\n case 10:\n duration = _context12.sent;\n _context12.next = 13;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"select\"])(getViewerWidth);\n\n case 13:\n viewerWidth = _context12.sent;\n startTime = Math.min.apply(Math, Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_toConsumableArray__WEBPACK_IMPORTED_MODULE_0__[\"default\"])(selectedRanges.map(function (range) {\n return range.startTime;\n })));\n endTime = Math.max.apply(Math, Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_toConsumableArray__WEBPACK_IMPORTED_MODULE_0__[\"default\"])(selectedRanges.map(function (range) {\n return range.endTime;\n })));\n percentVisible = (endTime - startTime) / duration;\n percentStart = startTime / duration;\n targetZoom = 1 / percentVisible;\n targetPixelStart = percentStart * (viewerWidth * targetZoom);\n _context12.next = 22;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"put\"])(Object(_actions_viewState__WEBPACK_IMPORTED_MODULE_11__[\"zoomTo\"])(targetZoom));\n\n case 22:\n _context12.next = 24;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"put\"])(Object(_actions_viewState__WEBPACK_IMPORTED_MODULE_11__[\"panToPosition\"])(targetPixelStart));\n\n case 24:\n case \"end\":\n return _context12.stop();\n }\n }\n }, _marked12);\n}\n\nfunction zoomTowards(targetZoom) {\n var selectedRangeIds, selectedRanges, zoomIncr, duration, viewerWidth, startTime, endTime, percentStart, percentVisible, maxZoom, zoom, targetPixelStart;\n return _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default.a.wrap(function zoomTowards$(_context13) {\n while (1) {\n switch (_context13.prev = _context13.next) {\n case 0:\n _context13.next = 2;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"select\"])(_reducers_range__WEBPACK_IMPORTED_MODULE_15__[\"getSelectedRanges\"]);\n\n case 2:\n selectedRangeIds = _context13.sent;\n\n if (!(selectedRangeIds.length <= 1)) {\n _context13.next = 5;\n break;\n }\n\n return _context13.abrupt(\"return\", false);\n\n case 5:\n _context13.next = 7;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"select\"])(Object(_reducers_range__WEBPACK_IMPORTED_MODULE_15__[\"getRangesByIds\"])(selectedRangeIds));\n\n case 7:\n selectedRanges = _context13.sent;\n _context13.next = 10;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"select\"])(function (state) {\n return state.project[_constants_project__WEBPACK_IMPORTED_MODULE_6__[\"PROJECT\"].ZOOM_TO_SECTION_INCREMENTALLY];\n });\n\n case 10:\n zoomIncr = _context13.sent;\n _context13.next = 13;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"select\"])(getDuration);\n\n case 13:\n duration = _context13.sent;\n _context13.next = 16;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"select\"])(getViewerWidth);\n\n case 16:\n viewerWidth = _context13.sent;\n startTime = Math.min.apply(Math, Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_toConsumableArray__WEBPACK_IMPORTED_MODULE_0__[\"default\"])(selectedRanges.map(function (range) {\n return range.startTime;\n })));\n endTime = Math.max.apply(Math, Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_toConsumableArray__WEBPACK_IMPORTED_MODULE_0__[\"default\"])(selectedRanges.map(function (range) {\n return range.endTime;\n })));\n percentStart = startTime / duration;\n percentVisible = (endTime - startTime) / duration;\n maxZoom = 1 / percentVisible;\n zoom = zoomIncr ? targetZoom < maxZoom ? targetZoom : maxZoom : maxZoom;\n targetPixelStart = percentStart * (viewerWidth * zoom);\n _context13.next = 26;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"put\"])(Object(_actions_viewState__WEBPACK_IMPORTED_MODULE_11__[\"zoomTo\"])(maxZoom));\n\n case 26:\n _context13.next = 28;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"put\"])(Object(_actions_viewState__WEBPACK_IMPORTED_MODULE_11__[\"panToPosition\"])(targetPixelStart));\n\n case 28:\n return _context13.abrupt(\"return\", true);\n\n case 29:\n case \"end\":\n return _context13.stop();\n }\n }\n }, _marked13);\n}\n\nvar getZoom = function getZoom(state) {\n return state.viewState.zoom;\n};\n\nfunction zoomInOut(action) {\n var ZOOM_AMOUNT, zoom, duration, currentTime, ZOOM_ORIGIN, viewerWidth, targetViewerWidth, viewerOffsetLeft, didZoom;\n return _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default.a.wrap(function zoomInOut$(_context14) {\n while (1) {\n switch (_context14.prev = _context14.next) {\n case 0:\n ZOOM_AMOUNT = action.type === _constants_viewState__WEBPACK_IMPORTED_MODULE_12__[\"ZOOM_IN\"] ? 1.2 : 1 / 1.2;\n _context14.next = 3;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"select\"])(getZoom);\n\n case 3:\n zoom = _context14.sent;\n _context14.next = 6;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"select\"])(getDuration);\n\n case 6:\n duration = _context14.sent;\n _context14.next = 9;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"select\"])(getCurrentTime);\n\n case 9:\n currentTime = _context14.sent;\n ZOOM_ORIGIN = currentTime / duration;\n _context14.next = 13;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"select\"])(getViewerWidth);\n\n case 13:\n viewerWidth = _context14.sent;\n targetViewerWidth = viewerWidth * zoom * ZOOM_AMOUNT;\n viewerOffsetLeft = (targetViewerWidth - viewerWidth) * ZOOM_ORIGIN;\n\n if (!(action.type === _constants_viewState__WEBPACK_IMPORTED_MODULE_12__[\"ZOOM_IN\"])) {\n _context14.next = 22;\n break;\n }\n\n _context14.next = 19;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"call\"])(zoomTowards, zoom * ZOOM_AMOUNT);\n\n case 19:\n _context14.t0 = _context14.sent;\n _context14.next = 23;\n break;\n\n case 22:\n _context14.t0 = false;\n\n case 23:\n didZoom = _context14.t0;\n\n if (didZoom) {\n _context14.next = 29;\n break;\n }\n\n _context14.next = 27;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"put\"])(Object(_actions_viewState__WEBPACK_IMPORTED_MODULE_11__[\"zoomTo\"])(zoom * ZOOM_AMOUNT));\n\n case 27:\n _context14.next = 29;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"put\"])(Object(_actions_viewState__WEBPACK_IMPORTED_MODULE_11__[\"panToPosition\"])(viewerOffsetLeft));\n\n case 29:\n case \"end\":\n return _context14.stop();\n }\n }\n }, _marked14);\n}\n\nfunction saveResource(url, content) {\n return new Promise(function (resolve, reject) {\n var http = new XMLHttpRequest();\n http.open('POST', url);\n http.setRequestHeader('Content-Type', 'application/json;charset=UTF-8');\n\n http.onreadystatechange = function () {\n if (http.readyState === http.DONE) {\n if (200 <= http.status && http.status <= 299) {\n // reload parent widow to location of newly created timeline\n if (document.referrer !== http.getResponseHeader('location')) {\n reject({\n redirect_location: http.getResponseHeader('location')\n });\n return;\n }\n\n resolve();\n } else {\n reject(new Error('Save Failed: ' + http.status + ', ' + http.statusText));\n }\n }\n };\n\n http.send(JSON.stringify(content));\n });\n}\n\nfunction saveProject() {\n var state, callback, resource, yes, outputJSON;\n return _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default.a.wrap(function saveProject$(_context15) {\n while (1) {\n switch (_context15.prev = _context15.next) {\n case 0:\n _context15.next = 2;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"select\"])();\n\n case 2:\n state = _context15.sent;\n _context15.next = 5;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"select\"])(function (s) {\n return s.viewState.callback;\n });\n\n case 5:\n callback = _context15.sent;\n _context15.next = 8;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"select\"])(function (s) {\n return s.viewState.resource;\n });\n\n case 8:\n resource = _context15.sent;\n\n if (!(resource !== callback)) {\n _context15.next = 15;\n break;\n }\n\n _context15.next = 12;\n return showConfirmation('This timeline isn’t yours. Saving will create a personal copy of this timeline that includes any changes you’ve made. Proceed?');\n\n case 12:\n yes = _context15.sent;\n\n if (yes) {\n _context15.next = 15;\n break;\n }\n\n return _context15.abrupt(\"return\");\n\n case 15:\n outputJSON = Object(_utils_iiifSaver__WEBPACK_IMPORTED_MODULE_7__[\"default\"])(state);\n _context15.prev = 16;\n _context15.next = 19;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"call\"])(saveResource, callback, outputJSON);\n\n case 19:\n _context15.next = 21;\n return showConfirmation('Saved Successfully.', false);\n\n case 21:\n _context15.next = 30;\n break;\n\n case 23:\n _context15.prev = 23;\n _context15.t0 = _context15[\"catch\"](16);\n\n if (!_context15.t0.hasOwnProperty('redirect_location')) {\n _context15.next = 28;\n break;\n }\n\n top.window.location = _context15.t0.redirect_location;\n return _context15.abrupt(\"return\");\n\n case 28:\n _context15.next = 30;\n return showConfirmation(_context15.t0.message, false);\n\n case 30:\n case \"end\":\n return _context15.stop();\n }\n }\n }, _marked15, null, [[16, 23]]);\n}\n\nfunction undoAll() {\n var undoStack, yes, i;\n return _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default.a.wrap(function undoAll$(_context16) {\n while (1) {\n switch (_context16.prev = _context16.next) {\n case 0:\n _context16.next = 2;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"select\"])(function (s) {\n return s.undoHistory.undoQueue;\n });\n\n case 2:\n undoStack = _context16.sent;\n _context16.next = 5;\n return showConfirmation('Are you sure you want to revert all your changes?');\n\n case 5:\n yes = _context16.sent;\n\n if (yes) {\n _context16.next = 8;\n break;\n }\n\n return _context16.abrupt(\"return\");\n\n case 8:\n i = 0;\n\n case 9:\n if (!(i < undoStack.length)) {\n _context16.next = 15;\n break;\n }\n\n _context16.next = 12;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"put\"])(redux_undo_redo__WEBPACK_IMPORTED_MODULE_5__[\"actions\"].undo());\n\n case 12:\n i++;\n _context16.next = 9;\n break;\n\n case 15:\n _context16.next = 17;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"call\"])(setIsSavedStatus);\n\n case 17:\n case \"end\":\n return _context16.stop();\n }\n }\n }, _marked16);\n}\n\nfunction root() {\n return _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default.a.wrap(function root$(_context17) {\n while (1) {\n switch (_context17.prev = _context17.next) {\n case 0:\n _context17.next = 2;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"all\"])([Object(_range_saga__WEBPACK_IMPORTED_MODULE_16__[\"default\"])(), Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"takeEvery\"])(_constants_project__WEBPACK_IMPORTED_MODULE_6__[\"IMPORT_DOCUMENT\"], importDocument), Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"takeEvery\"])(_constants_project__WEBPACK_IMPORTED_MODULE_6__[\"RESET_DOCUMENT\"], resetDocument), Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"takeEvery\"])(_constants_project__WEBPACK_IMPORTED_MODULE_6__[\"EXPORT_DOCUMENT\"], exportDocument), Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"takeEvery\"])(_constants_viewState__WEBPACK_IMPORTED_MODULE_12__[\"SAVE_PROJECT_METADATA\"], saveProjectMetadata), Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"takeEvery\"])(_constants_markers__WEBPACK_IMPORTED_MODULE_17__[\"SELECT_MARKER\"], selectMarker), Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"takeEvery\"])(_constants_markers__WEBPACK_IMPORTED_MODULE_17__[\"UPDATE_MARKER\"], updateMarkerTime), Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"takeEvery\"])(_constants_project__WEBPACK_IMPORTED_MODULE_6__[\"UPDATE_SETTINGS\"], updateSettings), Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"takeLatest\"])([_constants_viewState__WEBPACK_IMPORTED_MODULE_12__[\"ZOOM_IN\"], _constants_viewState__WEBPACK_IMPORTED_MODULE_12__[\"ZOOM_OUT\"], _constants_viewState__WEBPACK_IMPORTED_MODULE_12__[\"PAN_TO_POSITION\"], _constants_viewState__WEBPACK_IMPORTED_MODULE_12__[\"PLAY_AUDIO\"]], zoomSideEffects), Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"takeEvery\"])([_constants_viewState__WEBPACK_IMPORTED_MODULE_12__[\"ZOOM_IN\"], _constants_viewState__WEBPACK_IMPORTED_MODULE_12__[\"ZOOM_OUT\"]], zoomInOut), Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"takeEvery\"])(_constants_project__WEBPACK_IMPORTED_MODULE_6__[\"SAVE_PROJECT\"], saveProject), Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"takeEvery\"])(_constants_viewState__WEBPACK_IMPORTED_MODULE_12__[\"UNDO_ALL\"], undoAll)]);\n\n case 2:\n case \"end\":\n return _context17.stop();\n }\n }\n }, _marked17);\n}\n\n//# sourceURL=webpack:///./src/sagas/index.js?"); + eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"showConfirmation\", function() { return showConfirmation; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"default\", function() { return root; });\n/* harmony import */ var _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_toConsumableArray__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./node_modules/babel-preset-react-app/node_modules/@babel/runtime/helpers/esm/toConsumableArray */ \"./node_modules/babel-preset-react-app/node_modules/@babel/runtime/helpers/esm/toConsumableArray.js\");\n/* harmony import */ var _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./node_modules/babel-preset-react-app/node_modules/@babel/runtime/regenerator */ \"./node_modules/babel-preset-react-app/node_modules/@babel/runtime/regenerator/index.js\");\n/* harmony import */ var _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1__);\n/* harmony import */ var redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! redux-saga/effects */ \"./node_modules/redux-saga/es/effects.js\");\n/* harmony import */ var _constants_range__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ../constants/range */ \"./src/constants/range.js\");\n/* harmony import */ var _utils_iiifLoader__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ../utils/iiifLoader */ \"./src/utils/iiifLoader.js\");\n/* harmony import */ var redux_undo_redo__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! redux-undo-redo */ \"./node_modules/redux-undo-redo/lib/index.js\");\n/* harmony import */ var redux_undo_redo__WEBPACK_IMPORTED_MODULE_5___default = /*#__PURE__*/__webpack_require__.n(redux_undo_redo__WEBPACK_IMPORTED_MODULE_5__);\n/* harmony import */ var _constants_project__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ../constants/project */ \"./src/constants/project.js\");\n/* harmony import */ var _utils_iiifSaver__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ../utils/iiifSaver */ \"./src/utils/iiifSaver.js\");\n/* harmony import */ var _actions_project__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(/*! ../actions/project */ \"./src/actions/project.js\");\n/* harmony import */ var _actions_canvas__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(/*! ../actions/canvas */ \"./src/actions/canvas.js\");\n/* harmony import */ var _actions_range__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(/*! ../actions/range */ \"./src/actions/range.js\");\n/* harmony import */ var _actions_viewState__WEBPACK_IMPORTED_MODULE_11__ = __webpack_require__(/*! ../actions/viewState */ \"./src/actions/viewState.js\");\n/* harmony import */ var _constants_viewState__WEBPACK_IMPORTED_MODULE_12__ = __webpack_require__(/*! ../constants/viewState */ \"./src/constants/viewState.js\");\n/* harmony import */ var _utils_iiifSerializer__WEBPACK_IMPORTED_MODULE_13__ = __webpack_require__(/*! ../utils/iiifSerializer */ \"./src/utils/iiifSerializer.js\");\n/* harmony import */ var _utils_fileDownload__WEBPACK_IMPORTED_MODULE_14__ = __webpack_require__(/*! ../utils/fileDownload */ \"./src/utils/fileDownload.js\");\n/* harmony import */ var _reducers_range__WEBPACK_IMPORTED_MODULE_15__ = __webpack_require__(/*! ../reducers/range */ \"./src/reducers/range.js\");\n/* harmony import */ var _range_saga__WEBPACK_IMPORTED_MODULE_16__ = __webpack_require__(/*! ./range-saga */ \"./src/sagas/range-saga.js\");\n/* harmony import */ var _constants_markers__WEBPACK_IMPORTED_MODULE_17__ = __webpack_require__(/*! ../constants/markers */ \"./src/constants/markers.js\");\n/* harmony import */ var _actions_markers__WEBPACK_IMPORTED_MODULE_18__ = __webpack_require__(/*! ../actions/markers */ \"./src/actions/markers.js\");\n\n\n\nvar _marked =\n/*#__PURE__*/\n_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default.a.mark(setIsSavedStatus),\n _marked2 =\n/*#__PURE__*/\n_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default.a.mark(importDocument),\n _marked3 =\n/*#__PURE__*/\n_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default.a.mark(showConfirmation),\n _marked4 =\n/*#__PURE__*/\n_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default.a.mark(resetDocument),\n _marked5 =\n/*#__PURE__*/\n_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default.a.mark(exportDocument),\n _marked6 =\n/*#__PURE__*/\n_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default.a.mark(saveProjectMetadata),\n _marked7 =\n/*#__PURE__*/\n_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default.a.mark(selectMarker),\n _marked8 =\n/*#__PURE__*/\n_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default.a.mark(updateMarkerTime),\n _marked9 =\n/*#__PURE__*/\n_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default.a.mark(updateSettings),\n _marked10 =\n/*#__PURE__*/\n_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default.a.mark(zoomSideEffects),\n _marked11 =\n/*#__PURE__*/\n_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default.a.mark(timeWithinSelection),\n _marked12 =\n/*#__PURE__*/\n_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default.a.mark(zoomToSelection),\n _marked13 =\n/*#__PURE__*/\n_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default.a.mark(zoomTowards),\n _marked14 =\n/*#__PURE__*/\n_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default.a.mark(zoomInOut),\n _marked15 =\n/*#__PURE__*/\n_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default.a.mark(saveProject),\n _marked16 =\n/*#__PURE__*/\n_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default.a.mark(undoAll),\n _marked17 =\n/*#__PURE__*/\n_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default.a.mark(root);\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nvar getDuration = function getDuration(state) {\n return state.viewState.runTime;\n};\n\nvar getCurrentTime = function getCurrentTime(state) {\n return state.viewState.currentTime;\n};\n\nfunction setIsSavedStatus() {\n return _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default.a.wrap(function setIsSavedStatus$(_context) {\n while (1) {\n switch (_context.prev = _context.next) {\n case 0:\n _context.next = 2;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"put\"])(Object(_actions_project__WEBPACK_IMPORTED_MODULE_8__[\"setProjectChanged\"])(false));\n\n case 2:\n case \"end\":\n return _context.stop();\n }\n }\n }, _marked);\n}\n\nfunction importDocument(_ref) {\n var manifest, source, _ref2, viewState, loadedState;\n\n return _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default.a.wrap(function importDocument$(_context2) {\n while (1) {\n switch (_context2.prev = _context2.next) {\n case 0:\n manifest = _ref.manifest, source = _ref.source;\n _context2.next = 3;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"select\"])();\n\n case 3:\n _ref2 = _context2.sent;\n viewState = _ref2.viewState;\n\n if (!(viewState.source === source)) {\n _context2.next = 7;\n break;\n }\n\n return _context2.abrupt(\"return\");\n\n case 7:\n _context2.next = 9;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"put\"])(redux_undo_redo__WEBPACK_IMPORTED_MODULE_5__[\"actions\"].clear());\n\n case 9:\n _context2.prev = 9;\n loadedState = Object(_utils_iiifLoader__WEBPACK_IMPORTED_MODULE_4__[\"loadProjectState\"])(manifest);\n _context2.next = 13;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"put\"])(Object(_actions_canvas__WEBPACK_IMPORTED_MODULE_9__[\"unloadAudio\"])());\n\n case 13:\n _context2.next = 15;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"put\"])(Object(_actions_project__WEBPACK_IMPORTED_MODULE_8__[\"loadProject\"])(loadedState.project));\n\n case 15:\n _context2.next = 17;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"put\"])(Object(_actions_viewState__WEBPACK_IMPORTED_MODULE_11__[\"loadViewState\"])(loadedState.viewState));\n\n case 17:\n _context2.next = 19;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"put\"])(Object(_actions_canvas__WEBPACK_IMPORTED_MODULE_9__[\"loadCanvas\"])(loadedState.canvas));\n\n case 19:\n _context2.next = 21;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"put\"])(Object(_actions_range__WEBPACK_IMPORTED_MODULE_10__[\"importRanges\"])(loadedState.range));\n\n case 21:\n _context2.next = 23;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"put\"])(Object(_actions_markers__WEBPACK_IMPORTED_MODULE_18__[\"clearMarkers\"])());\n\n case 23:\n _context2.next = 25;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"put\"])(Object(_actions_markers__WEBPACK_IMPORTED_MODULE_18__[\"importMarkers\"])(Object(_utils_iiifLoader__WEBPACK_IMPORTED_MODULE_4__[\"parseMarkers\"])(manifest)));\n\n case 25:\n _context2.next = 32;\n break;\n\n case 27:\n _context2.prev = 27;\n _context2.t0 = _context2[\"catch\"](9);\n console.error(_context2.t0);\n _context2.next = 32;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"put\"])(Object(_actions_project__WEBPACK_IMPORTED_MODULE_8__[\"importError\"])(_context2.t0));\n\n case 32:\n case \"end\":\n return _context2.stop();\n }\n }\n }, _marked2, null, [[9, 27]]);\n}\n\nfunction showConfirmation(message) {\n var doCancel,\n _ref3,\n yes,\n _args3 = arguments;\n\n return _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default.a.wrap(function showConfirmation$(_context3) {\n while (1) {\n switch (_context3.prev = _context3.next) {\n case 0:\n doCancel = _args3.length > 1 && _args3[1] !== undefined ? _args3[1] : true;\n _context3.next = 3;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"put\"])(Object(_actions_viewState__WEBPACK_IMPORTED_MODULE_11__[\"openVerifyDialog\"])(message, doCancel));\n\n case 3:\n _context3.next = 5;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"race\"])({\n yes: Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"take\"])(_constants_viewState__WEBPACK_IMPORTED_MODULE_12__[\"CONFIRM_YES\"]),\n no: Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"take\"])(_constants_viewState__WEBPACK_IMPORTED_MODULE_12__[\"CONFIRM_NO\"])\n });\n\n case 5:\n _ref3 = _context3.sent;\n yes = _ref3.yes;\n _context3.next = 9;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"put\"])(Object(_actions_viewState__WEBPACK_IMPORTED_MODULE_11__[\"closeVerifyDialog\"])());\n\n case 9:\n return _context3.abrupt(\"return\", !!yes);\n\n case 10:\n case \"end\":\n return _context3.stop();\n }\n }\n }, _marked3);\n}\n\nfunction resetDocument() {\n var confirmed, rangeIds, duration;\n return _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default.a.wrap(function resetDocument$(_context4) {\n while (1) {\n switch (_context4.prev = _context4.next) {\n case 0:\n _context4.next = 2;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"call\"])(showConfirmation, 'Are you sure you want to delete all sections?');\n\n case 2:\n confirmed = _context4.sent;\n\n if (!confirmed) {\n _context4.next = 14;\n break;\n }\n\n _context4.next = 6;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"select\"])(function (s) {\n return Object.keys(Object(_reducers_range__WEBPACK_IMPORTED_MODULE_15__[\"getRangeList\"])(s));\n });\n\n case 6:\n rangeIds = _context4.sent;\n _context4.next = 9;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"put\"])(redux_undo_redo__WEBPACK_IMPORTED_MODULE_5__[\"actions\"].clear());\n\n case 9:\n _context4.next = 11;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"select\"])(getDuration);\n\n case 11:\n duration = _context4.sent;\n _context4.next = 14;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"put\"])(Object(_actions_range__WEBPACK_IMPORTED_MODULE_10__[\"rangeMutations\"])([].concat(Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_toConsumableArray__WEBPACK_IMPORTED_MODULE_0__[\"default\"])(rangeIds.map(function (range) {\n return {\n type: 'DELETE_RANGE',\n payload: {\n id: range\n }\n };\n })), [Object(_actions_range__WEBPACK_IMPORTED_MODULE_10__[\"createRange\"])({\n startTime: 0,\n endTime: duration\n })])));\n\n case 14:\n case \"end\":\n return _context4.stop();\n }\n }\n }, _marked4);\n}\n\nfunction exportDocument() {\n var state, label, outputJSON;\n return _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default.a.wrap(function exportDocument$(_context5) {\n while (1) {\n switch (_context5.prev = _context5.next) {\n case 0:\n _context5.next = 2;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"select\"])();\n\n case 2:\n state = _context5.sent;\n _context5.next = 5;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"select\"])(function (s) {\n return \"\".concat(s.project[_constants_project__WEBPACK_IMPORTED_MODULE_6__[\"PROJECT\"].TITLE].replace(/[ ,.'\"]/g, '_') || 'manifest', \".json\");\n });\n\n case 5:\n label = _context5.sent;\n outputJSON = Object(_utils_iiifSaver__WEBPACK_IMPORTED_MODULE_7__[\"default\"])(state);\n Object(_utils_fileDownload__WEBPACK_IMPORTED_MODULE_14__[\"immediateDownload\"])(label, Object(_utils_iiifSerializer__WEBPACK_IMPORTED_MODULE_13__[\"serialize\"])(outputJSON));\n\n case 8:\n case \"end\":\n return _context5.stop();\n }\n }\n }, _marked5);\n}\n\nfunction saveProjectMetadata(_ref4) {\n var metadata, _ref5, title, description, manifestLabel, manifestSummary;\n\n return _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default.a.wrap(function saveProjectMetadata$(_context6) {\n while (1) {\n switch (_context6.prev = _context6.next) {\n case 0:\n metadata = _ref4.metadata;\n _context6.next = 3;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"select\"])(function (state) {\n return state.project;\n });\n\n case 3:\n _ref5 = _context6.sent;\n title = _ref5.title;\n description = _ref5.description;\n manifestLabel = metadata.manifestLabel, manifestSummary = metadata.manifestSummary;\n\n if (!(title !== manifestLabel)) {\n _context6.next = 10;\n break;\n }\n\n _context6.next = 10;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"put\"])(Object(_actions_project__WEBPACK_IMPORTED_MODULE_8__[\"setTitle\"])(manifestLabel));\n\n case 10:\n if (!(description !== manifestSummary)) {\n _context6.next = 13;\n break;\n }\n\n _context6.next = 13;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"put\"])(Object(_actions_project__WEBPACK_IMPORTED_MODULE_8__[\"setDescription\"])(manifestSummary));\n\n case 13:\n _context6.next = 15;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"put\"])(Object(_actions_viewState__WEBPACK_IMPORTED_MODULE_11__[\"cancelProjectMetadataEdits\"])());\n\n case 15:\n _context6.next = 17;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"call\"])(setIsSavedStatus);\n\n case 17:\n case \"end\":\n return _context6.stop();\n }\n }\n }, _marked6);\n}\n\nfunction selectMarker(_ref6) {\n var id, marker, startPlaying;\n return _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default.a.wrap(function selectMarker$(_context7) {\n while (1) {\n switch (_context7.prev = _context7.next) {\n case 0:\n id = _ref6.payload.id;\n _context7.next = 3;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"select\"])(function (state) {\n return state.markers.list[id];\n });\n\n case 3:\n marker = _context7.sent;\n _context7.next = 6;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"select\"])(function (state) {\n return state.project[_constants_project__WEBPACK_IMPORTED_MODULE_6__[\"PROJECT\"].START_PLAYING_WHEN_BUBBLES_CLICKED];\n });\n\n case 6:\n startPlaying = _context7.sent;\n _context7.next = 9;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"put\"])(Object(_actions_viewState__WEBPACK_IMPORTED_MODULE_11__[\"setCurrentTime\"])(marker.time));\n\n case 9:\n if (!startPlaying) {\n _context7.next = 12;\n break;\n }\n\n _context7.next = 12;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"put\"])(Object(_actions_viewState__WEBPACK_IMPORTED_MODULE_11__[\"play\"])());\n\n case 12:\n case \"end\":\n return _context7.stop();\n }\n }\n }, _marked7);\n}\n\nfunction updateMarkerTime(_ref7) {\n var _ref7$payload, id, time;\n\n return _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default.a.wrap(function updateMarkerTime$(_context8) {\n while (1) {\n switch (_context8.prev = _context8.next) {\n case 0:\n _ref7$payload = _ref7.payload, id = _ref7$payload.id, time = _ref7$payload.time;\n\n if (!time) {\n _context8.next = 4;\n break;\n }\n\n _context8.next = 4;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"put\"])(Object(_actions_viewState__WEBPACK_IMPORTED_MODULE_11__[\"setCurrentTime\"])(time));\n\n case 4:\n _context8.next = 6;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"call\"])(setIsSavedStatus);\n\n case 6:\n case \"end\":\n return _context8.stop();\n }\n }\n }, _marked8);\n}\n\nfunction updateSettings(_ref8) {\n var payload;\n return _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default.a.wrap(function updateSettings$(_context9) {\n while (1) {\n switch (_context9.prev = _context9.next) {\n case 0:\n payload = _ref8.payload;\n\n if (!payload.showMarkers) {\n _context9.next = 6;\n break;\n }\n\n _context9.next = 4;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"put\"])(Object(_actions_markers__WEBPACK_IMPORTED_MODULE_18__[\"showMarkers\"])());\n\n case 4:\n _context9.next = 8;\n break;\n\n case 6:\n _context9.next = 8;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"put\"])(Object(_actions_markers__WEBPACK_IMPORTED_MODULE_18__[\"hideMarkers\"])());\n\n case 8:\n _context9.next = 10;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"call\"])(setIsSavedStatus);\n\n case 10:\n case \"end\":\n return _context9.stop();\n }\n }\n }, _marked9);\n}\n\nfunction zoomSideEffects() {\n var zoomA, duration, _ref9, currentTime, viewportWidth, zoom, x, sliderWidth, percentThrough, maxMiddle, pixelThrough, from, to, isVisible, shouldZoomToRange, targetPan;\n\n return _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default.a.wrap(function zoomSideEffects$(_context10) {\n while (1) {\n switch (_context10.prev = _context10.next) {\n case 0:\n _context10.next = 2;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"select\"])(function (state) {\n return state.viewState.zoom;\n });\n\n case 2:\n zoomA = _context10.sent;\n _context10.next = 5;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"select\"])(getDuration);\n\n case 5:\n duration = _context10.sent;\n\n if (!(zoomA === 1)) {\n _context10.next = 8;\n break;\n }\n\n return _context10.abrupt(\"return\");\n\n case 8:\n if (false) {}\n\n _context10.next = 11;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"take\"])(_constants_viewState__WEBPACK_IMPORTED_MODULE_12__[\"SET_CURRENT_TIME\"]);\n\n case 11:\n _ref9 = _context10.sent;\n currentTime = _ref9.payload.currentTime;\n _context10.next = 15;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"select\"])(getViewerWidth);\n\n case 15:\n viewportWidth = _context10.sent;\n _context10.next = 18;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"select\"])(function (state) {\n return state.viewState.zoom;\n });\n\n case 18:\n zoom = _context10.sent;\n _context10.next = 21;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"select\"])(function (state) {\n return state.viewState.x;\n });\n\n case 21:\n x = _context10.sent;\n\n if (!(zoom === 1)) {\n _context10.next = 24;\n break;\n }\n\n return _context10.abrupt(\"return\");\n\n case 24:\n sliderWidth = viewportWidth * zoom;\n percentThrough = currentTime / duration;\n maxMiddle = sliderWidth - viewportWidth;\n pixelThrough = percentThrough * sliderWidth;\n from = Math.floor(x) - 20;\n to = Math.ceil(x + viewportWidth) + 20;\n isVisible = pixelThrough >= from && pixelThrough <= to; // If its not visible, pan to the middle.\n\n if (!(isVisible === false)) {\n _context10.next = 51;\n break;\n }\n\n _context10.next = 34;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"call\"])(timeWithinSelection, currentTime);\n\n case 34:\n shouldZoomToRange = _context10.sent;\n\n if (!shouldZoomToRange) {\n _context10.next = 39;\n break;\n }\n\n _context10.next = 38;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"call\"])(zoomToSelection);\n\n case 38:\n return _context10.abrupt(\"return\");\n\n case 39:\n targetPan = pixelThrough - viewportWidth / 2;\n\n if (!(targetPan <= 0)) {\n _context10.next = 44;\n break;\n }\n\n _context10.next = 43;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"put\"])(Object(_actions_viewState__WEBPACK_IMPORTED_MODULE_11__[\"panToPosition\"])(0));\n\n case 43:\n return _context10.abrupt(\"return\");\n\n case 44:\n if (!(targetPan >= maxMiddle)) {\n _context10.next = 48;\n break;\n }\n\n _context10.next = 47;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"put\"])(Object(_actions_viewState__WEBPACK_IMPORTED_MODULE_11__[\"panToPosition\"])(maxMiddle));\n\n case 47:\n return _context10.abrupt(\"return\");\n\n case 48:\n _context10.next = 50;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"put\"])(Object(_actions_viewState__WEBPACK_IMPORTED_MODULE_11__[\"panToPosition\"])(targetPan));\n\n case 50:\n return _context10.abrupt(\"return\");\n\n case 51:\n _context10.next = 8;\n break;\n\n case 53:\n case \"end\":\n return _context10.stop();\n }\n }\n }, _marked10);\n}\n\nvar getViewerWidth = function getViewerWidth(state) {\n return state.viewState.viewerWidth;\n};\n\nfunction timeWithinSelection(time) {\n var selectedRangeIds, selectedRanges, startTime, endTime;\n return _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default.a.wrap(function timeWithinSelection$(_context11) {\n while (1) {\n switch (_context11.prev = _context11.next) {\n case 0:\n _context11.next = 2;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"select\"])(_reducers_range__WEBPACK_IMPORTED_MODULE_15__[\"getSelectedRanges\"]);\n\n case 2:\n selectedRangeIds = _context11.sent;\n\n if (!(selectedRangeIds.length <= 1)) {\n _context11.next = 5;\n break;\n }\n\n return _context11.abrupt(\"return\", false);\n\n case 5:\n _context11.next = 7;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"select\"])(Object(_reducers_range__WEBPACK_IMPORTED_MODULE_15__[\"getRangesByIds\"])(selectedRangeIds));\n\n case 7:\n selectedRanges = _context11.sent;\n startTime = Math.min.apply(Math, Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_toConsumableArray__WEBPACK_IMPORTED_MODULE_0__[\"default\"])(selectedRanges.map(function (range) {\n return range.startTime;\n })));\n endTime = Math.max.apply(Math, Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_toConsumableArray__WEBPACK_IMPORTED_MODULE_0__[\"default\"])(selectedRanges.map(function (range) {\n return range.endTime;\n })));\n return _context11.abrupt(\"return\", time >= startTime && time <= endTime);\n\n case 11:\n case \"end\":\n return _context11.stop();\n }\n }\n }, _marked11);\n}\n\nfunction zoomToSelection(action) {\n var selectedRangeIds, selectedRanges, duration, viewerWidth, startTime, endTime, percentVisible, percentStart, targetZoom, targetPixelStart;\n return _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default.a.wrap(function zoomToSelection$(_context12) {\n while (1) {\n switch (_context12.prev = _context12.next) {\n case 0:\n _context12.next = 2;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"select\"])(_reducers_range__WEBPACK_IMPORTED_MODULE_15__[\"getSelectedRanges\"]);\n\n case 2:\n selectedRangeIds = _context12.sent;\n\n if (!(selectedRangeIds.length <= 1 || action.payload.deselectOthers)) {\n _context12.next = 5;\n break;\n }\n\n return _context12.abrupt(\"return\", false);\n\n case 5:\n _context12.next = 7;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"select\"])(Object(_reducers_range__WEBPACK_IMPORTED_MODULE_15__[\"getRangesByIds\"])(selectedRangeIds));\n\n case 7:\n selectedRanges = _context12.sent;\n _context12.next = 10;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"select\"])(getDuration);\n\n case 10:\n duration = _context12.sent;\n _context12.next = 13;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"select\"])(getViewerWidth);\n\n case 13:\n viewerWidth = _context12.sent;\n startTime = Math.min.apply(Math, Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_toConsumableArray__WEBPACK_IMPORTED_MODULE_0__[\"default\"])(selectedRanges.map(function (range) {\n return range.startTime;\n })));\n endTime = Math.max.apply(Math, Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_toConsumableArray__WEBPACK_IMPORTED_MODULE_0__[\"default\"])(selectedRanges.map(function (range) {\n return range.endTime;\n })));\n percentVisible = (endTime - startTime) / duration;\n percentStart = startTime / duration;\n targetZoom = 1 / percentVisible;\n targetPixelStart = percentStart * (viewerWidth * targetZoom);\n _context12.next = 22;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"put\"])(Object(_actions_viewState__WEBPACK_IMPORTED_MODULE_11__[\"zoomTo\"])(targetZoom));\n\n case 22:\n _context12.next = 24;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"put\"])(Object(_actions_viewState__WEBPACK_IMPORTED_MODULE_11__[\"panToPosition\"])(targetPixelStart));\n\n case 24:\n case \"end\":\n return _context12.stop();\n }\n }\n }, _marked12);\n}\n\nfunction zoomTowards(targetZoom) {\n var selectedRangeIds, selectedRanges, zoomIncr, duration, viewerWidth, startTime, endTime, percentStart, percentVisible, maxZoom, zoom, targetPixelStart;\n return _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default.a.wrap(function zoomTowards$(_context13) {\n while (1) {\n switch (_context13.prev = _context13.next) {\n case 0:\n _context13.next = 2;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"select\"])(_reducers_range__WEBPACK_IMPORTED_MODULE_15__[\"getSelectedRanges\"]);\n\n case 2:\n selectedRangeIds = _context13.sent;\n\n if (!(selectedRangeIds.length <= 1)) {\n _context13.next = 5;\n break;\n }\n\n return _context13.abrupt(\"return\", false);\n\n case 5:\n _context13.next = 7;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"select\"])(Object(_reducers_range__WEBPACK_IMPORTED_MODULE_15__[\"getRangesByIds\"])(selectedRangeIds));\n\n case 7:\n selectedRanges = _context13.sent;\n _context13.next = 10;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"select\"])(function (state) {\n return state.project[_constants_project__WEBPACK_IMPORTED_MODULE_6__[\"PROJECT\"].ZOOM_TO_SECTION_INCREMENTALLY];\n });\n\n case 10:\n zoomIncr = _context13.sent;\n _context13.next = 13;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"select\"])(getDuration);\n\n case 13:\n duration = _context13.sent;\n _context13.next = 16;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"select\"])(getViewerWidth);\n\n case 16:\n viewerWidth = _context13.sent;\n startTime = Math.min.apply(Math, Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_toConsumableArray__WEBPACK_IMPORTED_MODULE_0__[\"default\"])(selectedRanges.map(function (range) {\n return range.startTime;\n })));\n endTime = Math.max.apply(Math, Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_toConsumableArray__WEBPACK_IMPORTED_MODULE_0__[\"default\"])(selectedRanges.map(function (range) {\n return range.endTime;\n })));\n percentStart = startTime / duration;\n percentVisible = (endTime - startTime) / duration;\n maxZoom = 1 / percentVisible;\n zoom = zoomIncr ? targetZoom < maxZoom ? targetZoom : maxZoom : maxZoom;\n targetPixelStart = percentStart * (viewerWidth * zoom);\n _context13.next = 26;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"put\"])(Object(_actions_viewState__WEBPACK_IMPORTED_MODULE_11__[\"zoomTo\"])(maxZoom));\n\n case 26:\n _context13.next = 28;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"put\"])(Object(_actions_viewState__WEBPACK_IMPORTED_MODULE_11__[\"panToPosition\"])(targetPixelStart));\n\n case 28:\n return _context13.abrupt(\"return\", true);\n\n case 29:\n case \"end\":\n return _context13.stop();\n }\n }\n }, _marked13);\n}\n\nvar getZoom = function getZoom(state) {\n return state.viewState.zoom;\n};\n\nfunction zoomInOut(action) {\n var ZOOM_AMOUNT, zoom, duration, currentTime, ZOOM_ORIGIN, viewerWidth, targetViewerWidth, viewerOffsetLeft, didZoom;\n return _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default.a.wrap(function zoomInOut$(_context14) {\n while (1) {\n switch (_context14.prev = _context14.next) {\n case 0:\n ZOOM_AMOUNT = action.type === _constants_viewState__WEBPACK_IMPORTED_MODULE_12__[\"ZOOM_IN\"] ? 1.2 : 1 / 1.2;\n _context14.next = 3;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"select\"])(getZoom);\n\n case 3:\n zoom = _context14.sent;\n _context14.next = 6;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"select\"])(getDuration);\n\n case 6:\n duration = _context14.sent;\n _context14.next = 9;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"select\"])(getCurrentTime);\n\n case 9:\n currentTime = _context14.sent;\n ZOOM_ORIGIN = currentTime / duration;\n _context14.next = 13;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"select\"])(getViewerWidth);\n\n case 13:\n viewerWidth = _context14.sent;\n targetViewerWidth = viewerWidth * zoom * ZOOM_AMOUNT;\n viewerOffsetLeft = (targetViewerWidth - viewerWidth) * ZOOM_ORIGIN;\n\n if (!(action.type === _constants_viewState__WEBPACK_IMPORTED_MODULE_12__[\"ZOOM_IN\"])) {\n _context14.next = 22;\n break;\n }\n\n _context14.next = 19;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"call\"])(zoomTowards, zoom * ZOOM_AMOUNT);\n\n case 19:\n _context14.t0 = _context14.sent;\n _context14.next = 23;\n break;\n\n case 22:\n _context14.t0 = false;\n\n case 23:\n didZoom = _context14.t0;\n\n if (didZoom) {\n _context14.next = 29;\n break;\n }\n\n _context14.next = 27;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"put\"])(Object(_actions_viewState__WEBPACK_IMPORTED_MODULE_11__[\"zoomTo\"])(zoom * ZOOM_AMOUNT));\n\n case 27:\n _context14.next = 29;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"put\"])(Object(_actions_viewState__WEBPACK_IMPORTED_MODULE_11__[\"panToPosition\"])(viewerOffsetLeft));\n\n case 29:\n case \"end\":\n return _context14.stop();\n }\n }\n }, _marked14);\n}\n\nfunction saveResource(url, content) {\n return new Promise(function (resolve, reject) {\n var http = new XMLHttpRequest();\n http.open('POST', url);\n http.setRequestHeader('Content-Type', 'application/json;charset=UTF-8');\n\n http.onreadystatechange = function () {\n if (http.readyState === http.DONE) {\n if (200 <= http.status && http.status <= 299) {\n // reload parent widow to location of newly created timeline\n if (document.referrer !== http.getResponseHeader('location')) {\n reject({\n redirect_location: http.getResponseHeader('location')\n });\n return;\n }\n\n resolve();\n } else {\n reject(new Error('Save Failed: ' + http.status + ', ' + http.statusText));\n }\n }\n };\n\n http.send(JSON.stringify(content));\n });\n}\n\nfunction saveProject() {\n var state, callback, resource, yes, outputJSON;\n return _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default.a.wrap(function saveProject$(_context15) {\n while (1) {\n switch (_context15.prev = _context15.next) {\n case 0:\n _context15.next = 2;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"select\"])();\n\n case 2:\n state = _context15.sent;\n _context15.next = 5;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"select\"])(function (s) {\n return s.viewState.callback;\n });\n\n case 5:\n callback = _context15.sent;\n _context15.next = 8;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"select\"])(function (s) {\n return s.viewState.resource;\n });\n\n case 8:\n resource = _context15.sent;\n\n if (!(resource !== callback)) {\n _context15.next = 15;\n break;\n }\n\n _context15.next = 12;\n return showConfirmation('This timeline isn’t yours. Saving will create a personal copy of this timeline that includes any changes you’ve made. Proceed?');\n\n case 12:\n yes = _context15.sent;\n\n if (yes) {\n _context15.next = 15;\n break;\n }\n\n return _context15.abrupt(\"return\");\n\n case 15:\n outputJSON = Object(_utils_iiifSaver__WEBPACK_IMPORTED_MODULE_7__[\"default\"])(state);\n _context15.prev = 16;\n _context15.next = 19;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"call\"])(saveResource, callback, outputJSON);\n\n case 19:\n _context15.next = 21;\n return showConfirmation('Saved Successfully.', false);\n\n case 21:\n _context15.next = 30;\n break;\n\n case 23:\n _context15.prev = 23;\n _context15.t0 = _context15[\"catch\"](16);\n\n if (!_context15.t0.hasOwnProperty('redirect_location')) {\n _context15.next = 28;\n break;\n }\n\n top.window.location = _context15.t0.redirect_location;\n return _context15.abrupt(\"return\");\n\n case 28:\n _context15.next = 30;\n return showConfirmation(_context15.t0.message, false);\n\n case 30:\n case \"end\":\n return _context15.stop();\n }\n }\n }, _marked15, null, [[16, 23]]);\n}\n\nfunction undoAll() {\n var undoStack, yes, i;\n return _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default.a.wrap(function undoAll$(_context16) {\n while (1) {\n switch (_context16.prev = _context16.next) {\n case 0:\n _context16.next = 2;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"select\"])(function (s) {\n return s.undoHistory.undoQueue;\n });\n\n case 2:\n undoStack = _context16.sent;\n _context16.next = 5;\n return showConfirmation('Are you sure you want to revert all your changes?');\n\n case 5:\n yes = _context16.sent;\n\n if (yes) {\n _context16.next = 8;\n break;\n }\n\n return _context16.abrupt(\"return\");\n\n case 8:\n i = 0;\n\n case 9:\n if (!(i < undoStack.length)) {\n _context16.next = 15;\n break;\n }\n\n _context16.next = 12;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"put\"])(redux_undo_redo__WEBPACK_IMPORTED_MODULE_5__[\"actions\"].undo());\n\n case 12:\n i++;\n _context16.next = 9;\n break;\n\n case 15:\n _context16.next = 17;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"call\"])(setIsSavedStatus);\n\n case 17:\n case \"end\":\n return _context16.stop();\n }\n }\n }, _marked16);\n}\n\nfunction root() {\n return _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_1___default.a.wrap(function root$(_context17) {\n while (1) {\n switch (_context17.prev = _context17.next) {\n case 0:\n _context17.next = 2;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"all\"])([Object(_range_saga__WEBPACK_IMPORTED_MODULE_16__[\"default\"])(), Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"takeEvery\"])(_constants_project__WEBPACK_IMPORTED_MODULE_6__[\"IMPORT_DOCUMENT\"], importDocument), Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"takeEvery\"])(_constants_project__WEBPACK_IMPORTED_MODULE_6__[\"RESET_DOCUMENT\"], resetDocument), Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"takeEvery\"])(_constants_project__WEBPACK_IMPORTED_MODULE_6__[\"EXPORT_DOCUMENT\"], exportDocument), Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"takeEvery\"])(_constants_viewState__WEBPACK_IMPORTED_MODULE_12__[\"SAVE_PROJECT_METADATA\"], saveProjectMetadata), Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"takeEvery\"])(_constants_markers__WEBPACK_IMPORTED_MODULE_17__[\"SELECT_MARKER\"], selectMarker), Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"takeEvery\"])(_constants_markers__WEBPACK_IMPORTED_MODULE_17__[\"UPDATE_MARKER\"], updateMarkerTime), Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"takeEvery\"])(_constants_project__WEBPACK_IMPORTED_MODULE_6__[\"UPDATE_SETTINGS\"], updateSettings), Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"takeLatest\"])([_constants_viewState__WEBPACK_IMPORTED_MODULE_12__[\"ZOOM_IN\"], _constants_viewState__WEBPACK_IMPORTED_MODULE_12__[\"ZOOM_OUT\"], _constants_viewState__WEBPACK_IMPORTED_MODULE_12__[\"PAN_TO_POSITION\"], _constants_viewState__WEBPACK_IMPORTED_MODULE_12__[\"PLAY_AUDIO\"]], zoomSideEffects), Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"takeEvery\"])([_constants_viewState__WEBPACK_IMPORTED_MODULE_12__[\"ZOOM_IN\"], _constants_viewState__WEBPACK_IMPORTED_MODULE_12__[\"ZOOM_OUT\"]], zoomInOut), Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"takeEvery\"])(_constants_project__WEBPACK_IMPORTED_MODULE_6__[\"SAVE_PROJECT\"], saveProject), Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_2__[\"takeEvery\"])(_constants_viewState__WEBPACK_IMPORTED_MODULE_12__[\"UNDO_ALL\"], undoAll)]);\n\n case 2:\n case \"end\":\n return _context17.stop();\n }\n }\n }, _marked17);\n}\n\n//# sourceURL=webpack:///./src/sagas/index.js?"); /***/ }), @@ -11155,7 +11155,7 @@ import "../iiif-timeliner-styles.css" /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; - eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"STICKY_BUBBLE_MS\", function() { return STICKY_BUBBLE_MS; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"getStickyPointDelta\", function() { return getStickyPointDelta; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"currentTimeSaga\", function() { return currentTimeSaga; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"createRangeAction\", function() { return createRangeAction; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"default\", function() { return rangeSaga; });\n/* harmony import */ var _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_objectSpread__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./node_modules/babel-preset-react-app/node_modules/@babel/runtime/helpers/esm/objectSpread */ \"./node_modules/babel-preset-react-app/node_modules/@babel/runtime/helpers/esm/objectSpread.js\");\n/* harmony import */ var _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_objectWithoutProperties__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./node_modules/babel-preset-react-app/node_modules/@babel/runtime/helpers/esm/objectWithoutProperties */ \"./node_modules/babel-preset-react-app/node_modules/@babel/runtime/helpers/esm/objectWithoutProperties.js\");\n/* harmony import */ var _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_toConsumableArray__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./node_modules/babel-preset-react-app/node_modules/@babel/runtime/helpers/esm/toConsumableArray */ \"./node_modules/babel-preset-react-app/node_modules/@babel/runtime/helpers/esm/toConsumableArray.js\");\n/* harmony import */ var _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./node_modules/babel-preset-react-app/node_modules/@babel/runtime/regenerator */ \"./node_modules/babel-preset-react-app/node_modules/@babel/runtime/regenerator/index.js\");\n/* harmony import */ var _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3__);\n/* harmony import */ var redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! redux-saga/effects */ \"./node_modules/redux-saga/es/effects.js\");\n/* harmony import */ var _actions_viewState__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ../actions/viewState */ \"./src/actions/viewState.js\");\n/* harmony import */ var _reducers_range__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ../reducers/range */ \"./src/reducers/range.js\");\n/* harmony import */ var _constants_viewState__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ../constants/viewState */ \"./src/constants/viewState.js\");\n/* harmony import */ var _constants_range__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(/*! ../constants/range */ \"./src/constants/range.js\");\n/* harmony import */ var _actions_range__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(/*! ../actions/range */ \"./src/actions/range.js\");\n/* harmony import */ var _actions_project__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(/*! ../actions/project */ \"./src/actions/project.js\");\n/* harmony import */ var _constants_project__WEBPACK_IMPORTED_MODULE_11__ = __webpack_require__(/*! ../constants/project */ \"./src/constants/project.js\");\n/* harmony import */ var _utils_invariant__WEBPACK_IMPORTED_MODULE_12__ = __webpack_require__(/*! ../utils/invariant */ \"./src/utils/invariant.js\");\n/* harmony import */ var _index__WEBPACK_IMPORTED_MODULE_13__ = __webpack_require__(/*! ./index */ \"./src/sagas/index.js\");\n\n\n\n\n\nvar _marked =\n/*#__PURE__*/\n_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.mark(setIsSavedStatus),\n _marked2 =\n/*#__PURE__*/\n_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.mark(previousBubble),\n _marked3 =\n/*#__PURE__*/\n_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.mark(nextBubble),\n _marked4 =\n/*#__PURE__*/\n_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.mark(calculateRedundantSizes),\n _marked5 =\n/*#__PURE__*/\n_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.mark(getDirectParentRange),\n _marked6 =\n/*#__PURE__*/\n_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.mark(getSiblingRanges),\n _marked7 =\n/*#__PURE__*/\n_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.mark(getMovePointMutations),\n _marked8 =\n/*#__PURE__*/\n_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.mark(saveRangeSaga),\n _marked9 =\n/*#__PURE__*/\n_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.mark(movePointSaga),\n _marked10 =\n/*#__PURE__*/\n_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.mark(playWhenBubbleIsClicked),\n _marked11 =\n/*#__PURE__*/\n_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.mark(currentTimeSaga),\n _marked12 =\n/*#__PURE__*/\n_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.mark(deselectOtherRanges),\n _marked13 =\n/*#__PURE__*/\n_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.mark(deselectAllRangesSaga),\n _marked14 =\n/*#__PURE__*/\n_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.mark(selectRangeSaga),\n _marked15 =\n/*#__PURE__*/\n_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.mark(deselectRangeSaga),\n _marked16 =\n/*#__PURE__*/\n_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.mark(createRangeAction),\n _marked17 =\n/*#__PURE__*/\n_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.mark(resolveParentDepths),\n _marked18 =\n/*#__PURE__*/\n_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.mark(groupRangeSaga),\n _marked19 =\n/*#__PURE__*/\n_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.mark(splitRangeSaga),\n _marked20 =\n/*#__PURE__*/\n_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.mark(deleteRangeRequest),\n _marked21 =\n/*#__PURE__*/\n_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.mark(singleDelete),\n _marked22 =\n/*#__PURE__*/\n_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.mark(multiDelete),\n _marked23 =\n/*#__PURE__*/\n_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.mark(clearCustomColorsSaga),\n _marked24 =\n/*#__PURE__*/\n_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.mark(rangeSaga);\n\n\n\n\n\n\n\n\n\n\n\nvar STICKY_BUBBLE_MS = 500;\n\nfunction setIsSavedStatus() {\n return _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.wrap(function setIsSavedStatus$(_context) {\n while (1) {\n switch (_context.prev = _context.next) {\n case 0:\n _context.next = 2;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"put\"])(Object(_actions_project__WEBPACK_IMPORTED_MODULE_10__[\"setProjectStatus\"])(false));\n\n case 2:\n case \"end\":\n return _context.stop();\n }\n }\n }, _marked);\n}\n\nfunction previousBubble() {\n var previousBubbleTime;\n return _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.wrap(function previousBubble$(_context2) {\n while (1) {\n switch (_context2.prev = _context2.next) {\n case 0:\n _context2.next = 2;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"select\"])(_reducers_range__WEBPACK_IMPORTED_MODULE_6__[\"getPreviousBubbleStartTime\"]);\n\n case 2:\n previousBubbleTime = _context2.sent;\n\n if (!Number.isFinite(previousBubbleTime)) {\n _context2.next = 8;\n break;\n }\n\n _context2.next = 6;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"put\"])(Object(_actions_viewState__WEBPACK_IMPORTED_MODULE_5__[\"setCurrentTime\"])(previousBubbleTime));\n\n case 6:\n _context2.next = 10;\n break;\n\n case 8:\n _context2.next = 10;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"put\"])(Object(_actions_viewState__WEBPACK_IMPORTED_MODULE_5__[\"setCurrentTime\"])(0));\n\n case 10:\n case \"end\":\n return _context2.stop();\n }\n }\n }, _marked2);\n}\n\nfunction nextBubble() {\n var nextBubbleTime;\n return _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.wrap(function nextBubble$(_context3) {\n while (1) {\n switch (_context3.prev = _context3.next) {\n case 0:\n _context3.next = 2;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"select\"])(_reducers_range__WEBPACK_IMPORTED_MODULE_6__[\"getNextBubbleStartTime\"]);\n\n case 2:\n nextBubbleTime = _context3.sent;\n _context3.next = 5;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"put\"])(Object(_actions_viewState__WEBPACK_IMPORTED_MODULE_5__[\"setCurrentTime\"])(nextBubbleTime.time));\n\n case 5:\n case \"end\":\n return _context3.stop();\n }\n }\n }, _marked3);\n}\n\nvar getCurrentTime = function getCurrentTime(state) {\n return state.viewState.currentTime;\n};\n/**\n * Can points merge\n *\n * Given a list of points and a start and end time this will\n * return true if the bubbles can be grouped.\n *\n * This is to avoid intersecting bubbles, which is not allowed.\n *\n * @param points\n * @param startTime\n * @param endTime\n * @returns {boolean}\n */\n\n\nfunction canMerge(points, _ref) {\n var startTime = _ref.startTime,\n endTime = _ref.endTime;\n return Object.values(points).filter(function (bubble) {\n return bubble.depth > 1;\n }).filter(function (bubble) {\n return bubble.startTime < startTime && startTime < bubble.endTime && bubble.endTime < endTime || startTime < bubble.startTime && bubble.startTime < endTime && endTime < bubble.endTime || bubble.startTime === startTime && bubble.endTime === endTime;\n }).length === 0;\n}\n/**\n * Get sticky point delta\n *\n * From a list of ranges and a single point in time this will return\n * the largest delta within `sticky` (default: 50ms) so that you can\n * use it to auto-correct inaccuracy of user pointers.\n *\n * @param ranges\n * @param x\n * @param sticky\n * @returns {T | *}\n */\n\n\nfunction getStickyPointDelta(ranges, x, sticky) {\n return ranges.reduce(function (stickyCandidates, range) {\n if (Math.abs(range.startTime - x) <= sticky) {\n stickyCandidates.push(range.startTime);\n }\n\n if (Math.abs(range.endTime - x) <= sticky) {\n stickyCandidates.push(range.endTime);\n }\n\n return stickyCandidates;\n }, []).filter(function (r) {\n return r > 0;\n }).sort(function (a, b) {\n return Math.abs(x - a) - Math.abs(x - b);\n }).pop() || x;\n}\n/**\n * Sort ranges by depth then time.\n *\n * Sort them first by depth, and then by endTime.\n *\n * This will give an order where parents always come before their\n * children AND they are still ordered by time.\n *\n * @param a\n * @param b\n * @returns {number}\n */\n\nfunction sortRangesByDepthThenTime(a, b) {\n // First by depth, if they are different from each other.\n if (b.depth !== a.depth) {\n return b.depth - a.depth;\n } // Then by end time.\n\n\n if (a.endTime < b.endTime) {\n return -1;\n }\n\n if (a.endTime > b.endTime) {\n return 1;\n } // This shouldn't happen, if they share an end time AND depth, its\n // not a valid bubble.\n\n\n return 0;\n}\n/**\n * Get direct children\n *\n * Given a list of all the children of a grouping bubble, this will\n * return only the DIRECT children.\n *\n * @param childrenRanges\n * @returns {*}\n */\n\n\nfunction getDirectChildren(childrenRanges) {\n return childrenRanges.sort(sortRangesByDepthThenTime).reduce(function (ac, n) {\n // Does it start first in range.\n var isMin = n.startTime < ac.min; // Is furthest reaching in range.\n\n var isMax = n.endTime > ac.max; // Record the min and max.\n\n if (isMin) {\n ac.min = n.startTime;\n }\n\n if (isMax) {\n ac.max = n.endTime;\n } // If we've just established a new min or max, push it on our list.\n\n\n if (isMin || isMax) {\n ac.list.push(n);\n } // Return the state.\n\n\n return ac;\n }, {\n min: Infinity,\n max: 0,\n list: []\n }).list;\n}\n/**\n * Calculate redundant sizes\n *\n * Asks the question, in the current state of the application, if I\n * removed X bubbles, which bubbles would have to be removed.\n *\n * @param toRemoveIds\n * @returns {IterableIterator<*>}\n */\n\n\nfunction calculateRedundantSizes(toRemoveIds) {\n var allRanges;\n return _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.wrap(function calculateRedundantSizes$(_context4) {\n while (1) {\n switch (_context4.prev = _context4.next) {\n case 0:\n _context4.t0 = Object;\n _context4.next = 3;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"select\"])(_reducers_range__WEBPACK_IMPORTED_MODULE_6__[\"getRangeList\"]);\n\n case 3:\n _context4.t1 = _context4.sent;\n allRanges = _context4.t0.values.call(_context4.t0, _context4.t1);\n return _context4.abrupt(\"return\", allRanges.filter(function (range) {\n return toRemoveIds.indexOf(range.id) === -1;\n }).filter(filterOnlyGroupingRanges).sort(function (a, b) {\n return a.depth === b.depth ? 0 : a.depth < b.depth ? -1 : 1;\n }).reduce(function (acc, next) {\n // Filter to only include children inside the current range.\n var filteredChildren = allRanges.filter(function (range) {\n return (// Not already removed\n acc.indexOf(range.id) === -1 && // Is within the parent bubble.\n bubbleIsWithin(next, range)\n );\n }); // From the filtered children, filter out only the DIRECT children.\n\n var directChildren = getDirectChildren(filteredChildren); // If there is only one child, we need to remove it.\n\n if (directChildren.length === 1) {\n acc.push(next.id);\n }\n\n return acc;\n }, toRemoveIds));\n\n case 6:\n case \"end\":\n return _context4.stop();\n }\n }\n }, _marked4);\n}\n/**\n * Filter only grouping ranges\n *\n * This has been split out, it may be expanded later on.\n *\n * @param range\n * @returns {boolean}\n */\n\n\nfunction filterOnlyGroupingRanges(range) {\n return range.depth > 1;\n}\n/**\n * Bubble is withing\n *\n * Filter for two ranges, returns true if `childCandidate` is\n * inside the first range at any level.\n *\n * @param parent\n * @param childCandidate\n * @returns {boolean}\n */\n\n\nfunction bubbleIsWithin(parent, childCandidate) {\n return childCandidate.id !== parent.id && childCandidate.startTime >= parent.startTime && childCandidate.endTime <= parent.endTime;\n}\n/**\n * Fuzzy equal.\n *\n * Compares `a` and `b` and returns if they are within\n * STICK_BUBBLE_MS configuration value (default: 50)\n *\n * @param a\n * @param b\n * @returns {boolean}\n */\n\n\nfunction fuzzyEqual(a, b) {\n return Math.abs(a - b) <= STICKY_BUBBLE_MS;\n}\n/**\n * Extract times from range list\n *\n * This will take a list of ranges and simply return the smallest start\n * time and the largest end time to get a range.\n *\n * @param ranges\n * @returns {{startTime: *, endTime: *}}\n */\n\n\nfunction extractTimesFromRangeList(ranges) {\n var startTime = ranges.reduce(function (cur, next) {\n return next.startTime <= cur ? next.startTime : cur;\n }, Infinity);\n var endTime = ranges.reduce(function (cur, next) {\n return next.endTime >= cur ? next.endTime : cur;\n }, 0);\n return {\n startTime: startTime,\n endTime: endTime\n };\n}\n\nfunction getDirectParentRange(range) {\n var parentBubbles;\n return _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.wrap(function getDirectParentRange$(_context5) {\n while (1) {\n switch (_context5.prev = _context5.next) {\n case 0:\n _context5.next = 2;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"select\"])(getParentBubbles(range));\n\n case 2:\n parentBubbles = _context5.sent;\n return _context5.abrupt(\"return\", parentBubbles.find(function (parentRange) {\n return parentRange.depth === range.depth + 1 && range.startTime >= parentRange.startTime && range.endTime <= parentRange.endTime;\n }));\n\n case 4:\n case \"end\":\n return _context5.stop();\n }\n }\n }, _marked5);\n}\n\nfunction getSiblingRanges(parentRange, childRange) {\n var rangesBetweenParent;\n return _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.wrap(function getSiblingRanges$(_context6) {\n while (1) {\n switch (_context6.prev = _context6.next) {\n case 0:\n _context6.next = 2;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"select\"])(Object(_reducers_range__WEBPACK_IMPORTED_MODULE_6__[\"getRangesBetweenTimes\"])(parentRange.startTime, parentRange.endTime));\n\n case 2:\n rangesBetweenParent = _context6.sent;\n return _context6.abrupt(\"return\", rangesBetweenParent.filter(function (range) {\n return range.id !== childRange.id && range.id !== parentRange.id;\n }));\n\n case 4:\n case \"end\":\n return _context6.stop();\n }\n }\n }, _marked6);\n}\n\nfunction getRangeToTheLeft(siblingRanges, childRange) {\n return siblingRanges.find(function (range) {\n return range.id !== childRange.id && range.depth === childRange.depth && range.endTime === childRange.startTime;\n });\n}\n\nfunction getRangeToTheRight(siblingRanges, childRange) {\n return siblingRanges.find(function (range) {\n return range.id !== childRange.id && range.depth === childRange.depth && range.startTime === childRange.endTime;\n });\n}\n\nfunction overlapBubbleRight(toCover, toGrow) {\n return toGrow.startTime === toCover.endTime ? Object(_actions_range__WEBPACK_IMPORTED_MODULE_9__[\"updateRangeTime\"])(toGrow.id, {\n startTime: toCover.startTime\n }) : Object(_actions_range__WEBPACK_IMPORTED_MODULE_9__[\"updateRangeTime\"])(toGrow.id, {\n endTime: toCover.startTime\n });\n}\n\nfunction overlapBubbleLeft(toCover, toGrow) {\n return toGrow.endTime === toCover.startTime ? Object(_actions_range__WEBPACK_IMPORTED_MODULE_9__[\"updateRangeTime\"])(toGrow.id, {\n endTime: toCover.endTime\n }) : Object(_actions_range__WEBPACK_IMPORTED_MODULE_9__[\"updateRangeTime\"])(toGrow.id, {\n startTime: toCover.endTime\n });\n}\n\nvar getParentBubbles = function getParentBubbles(child) {\n return function (state) {\n return Object.values(Object(_reducers_range__WEBPACK_IMPORTED_MODULE_6__[\"getRangeList\"])(state)).filter(function (range) {\n return bubbleIsWithin(range, child);\n }).sort(function (range) {\n return -(range.startTime - range.endTime);\n });\n };\n};\n\nfunction canMoveBubbles(bubbleList, toRemove) {\n return bubbleList.filter(function (range) {\n return range.depth === toRemove.depth;\n }).length > 0;\n}\n\nfunction getMovePointMutations(fromTime, toTime) {\n var ranges, x, toRemoveIds, redundantSizes, toRemove, updateRangeTimes;\n return _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.wrap(function getMovePointMutations$(_context7) {\n while (1) {\n switch (_context7.prev = _context7.next) {\n case 0:\n _context7.next = 2;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"select\"])(Object(_reducers_range__WEBPACK_IMPORTED_MODULE_6__[\"getRangesAtPoint\"])(fromTime, STICKY_BUBBLE_MS));\n\n case 2:\n ranges = _context7.sent;\n x = getStickyPointDelta(ranges, toTime, STICKY_BUBBLE_MS);\n toRemoveIds = ranges.filter(function (range) {\n if (fuzzyEqual(range.startTime, fromTime)) {\n return fuzzyEqual(range.endTime, x);\n }\n\n return fuzzyEqual(range.startTime, x);\n }).map(function (range) {\n return range.id;\n }); // This should be able to be split out.\n\n _context7.next = 7;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"call\"])(calculateRedundantSizes, toRemoveIds);\n\n case 7:\n redundantSizes = _context7.sent;\n toRemove = redundantSizes.map(_actions_range__WEBPACK_IMPORTED_MODULE_9__[\"deleteRange\"]); // When updating range times, need to consider the STICKY_BUBBLE_MS of stickiness.\n\n updateRangeTimes = ranges.filter(function (range) {\n return redundantSizes.indexOf(range.id) === -1;\n }).map(function (range) {\n return fuzzyEqual(range.startTime, fromTime) ? Object(_actions_range__WEBPACK_IMPORTED_MODULE_9__[\"updateRangeTime\"])(range.id, {\n startTime: x\n }) : Object(_actions_range__WEBPACK_IMPORTED_MODULE_9__[\"updateRangeTime\"])(range.id, {\n endTime: x\n });\n }); // Finish the mutation.\n\n return _context7.abrupt(\"return\", [].concat(Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_toConsumableArray__WEBPACK_IMPORTED_MODULE_2__[\"default\"])(updateRangeTimes), Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_toConsumableArray__WEBPACK_IMPORTED_MODULE_2__[\"default\"])(toRemove)));\n\n case 11:\n case \"end\":\n return _context7.stop();\n }\n }\n }, _marked7);\n}\n/**\n * Get range by id\n *\n * Returns a selector for easily getting a single range by id\n * @param id\n * @returns {function(*): *}\n */\n\n\nfunction getRangeById(id) {\n return function (state) {\n return state.range.list[id];\n };\n}\n\nfunction saveRangeSaga(_ref2) {\n var payload, startTime, endTime, restPayload, range, mutations, startMutations, endMutations;\n return _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.wrap(function saveRangeSaga$(_context8) {\n while (1) {\n switch (_context8.prev = _context8.next) {\n case 0:\n payload = _ref2.payload;\n startTime = payload.startTime, endTime = payload.endTime, restPayload = Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_objectWithoutProperties__WEBPACK_IMPORTED_MODULE_1__[\"default\"])(payload, [\"startTime\", \"endTime\"]);\n _context8.next = 4;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"select\"])(getRangeById(restPayload.id));\n\n case 4:\n range = _context8.sent;\n mutations = [{\n type: _constants_range__WEBPACK_IMPORTED_MODULE_8__[\"UPDATE_RANGE\"],\n payload: restPayload\n }];\n\n if (!(typeof startTime !== 'undefined' && startTime !== null)) {\n _context8.next = 11;\n break;\n }\n\n _context8.next = 9;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"call\"])(getMovePointMutations, range.startTime, startTime);\n\n case 9:\n startMutations = _context8.sent;\n startMutations.forEach(function (mutation) {\n return mutations.push(mutation);\n });\n\n case 11:\n if (!(typeof endTime !== 'undefined' && endTime !== null)) {\n _context8.next = 16;\n break;\n }\n\n _context8.next = 14;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"call\"])(getMovePointMutations, range.endTime, endTime);\n\n case 14:\n endMutations = _context8.sent;\n endMutations.forEach(function (mutation) {\n return mutations.push(mutation);\n });\n\n case 16:\n _context8.next = 18;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"put\"])(Object(_actions_range__WEBPACK_IMPORTED_MODULE_9__[\"rangeMutations\"])(mutations));\n\n case 18:\n _context8.next = 20;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"put\"])(Object(_actions_viewState__WEBPACK_IMPORTED_MODULE_5__[\"editMetadata\"])(null));\n\n case 20:\n _context8.next = 22;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"call\"])(setIsSavedStatus);\n\n case 22:\n case \"end\":\n return _context8.stop();\n }\n }\n }, _marked8);\n}\n\nfunction movePointSaga(_ref3) {\n var _ref3$payload, originalX, newX, mutations;\n\n return _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.wrap(function movePointSaga$(_context9) {\n while (1) {\n switch (_context9.prev = _context9.next) {\n case 0:\n _ref3$payload = _ref3.payload, originalX = _ref3$payload.originalX, newX = _ref3$payload.x;\n _context9.next = 3;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"call\"])(getMovePointMutations, originalX, newX);\n\n case 3:\n mutations = _context9.sent;\n _context9.next = 6;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"put\"])(Object(_actions_range__WEBPACK_IMPORTED_MODULE_9__[\"rangeMutations\"])(mutations));\n\n case 6:\n case \"end\":\n return _context9.stop();\n }\n }\n }, _marked9);\n}\n\nfunction playWhenBubbleIsClicked(id, isSelected) {\n var state, ranges, selectedRangeIds, selectedRanges;\n return _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.wrap(function playWhenBubbleIsClicked$(_context10) {\n while (1) {\n switch (_context10.prev = _context10.next) {\n case 0:\n _context10.next = 2;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"select\"])();\n\n case 2:\n state = _context10.sent;\n _context10.next = 5;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"select\"])(_reducers_range__WEBPACK_IMPORTED_MODULE_6__[\"getRangeList\"]);\n\n case 5:\n ranges = _context10.sent;\n _context10.next = 8;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"select\"])(_reducers_range__WEBPACK_IMPORTED_MODULE_6__[\"getSelectedRanges\"]);\n\n case 8:\n selectedRangeIds = _context10.sent;\n _context10.next = 11;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"select\"])(Object(_reducers_range__WEBPACK_IMPORTED_MODULE_6__[\"getRangesByIds\"])(selectedRangeIds));\n\n case 11:\n selectedRanges = _context10.sent;\n\n if (!isSelected) {\n _context10.next = 21;\n break;\n }\n\n if (!(selectedRanges.length && selectedRanges[0] && selectedRanges[0].startTime === ranges[id].startTime)) {\n _context10.next = 19;\n break;\n }\n\n if (!state.project[_constants_project__WEBPACK_IMPORTED_MODULE_11__[\"PROJECT\"].START_PLAYING_WHEN_BUBBLES_CLICKED]) {\n _context10.next = 17;\n break;\n }\n\n _context10.next = 17;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"put\"])(Object(_actions_viewState__WEBPACK_IMPORTED_MODULE_5__[\"play\"])());\n\n case 17:\n _context10.next = 19;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"put\"])(Object(_actions_viewState__WEBPACK_IMPORTED_MODULE_5__[\"setCurrentTime\"])(ranges[id].startTime));\n\n case 19:\n _context10.next = 23;\n break;\n\n case 21:\n _context10.next = 23;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"put\"])(Object(_actions_viewState__WEBPACK_IMPORTED_MODULE_5__[\"pause\"])());\n\n case 23:\n case \"end\":\n return _context10.stop();\n }\n }\n }, _marked10);\n}\n\nfunction currentTimeSaga() {\n var _ref4,\n type,\n state,\n startPlayingAtEnd,\n stopPlayingAtEnd,\n selectedRangeIds,\n selectedRanges,\n startTime,\n endTime,\n time,\n lastTime,\n clicked,\n _ref5,\n currentTime,\n _args11 = arguments;\n\n return _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.wrap(function currentTimeSaga$(_context11) {\n while (1) {\n switch (_context11.prev = _context11.next) {\n case 0:\n _ref4 = _args11.length > 0 && _args11[0] !== undefined ? _args11[0] : {}, type = _ref4.type;\n _context11.next = 3;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"select\"])(function (s) {\n return s.project;\n });\n\n case 3:\n state = _context11.sent;\n startPlayingAtEnd = state.startPlayingAtEndOfSection;\n stopPlayingAtEnd = state.stopPlayingAtTheEndOfSection;\n _context11.next = 8;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"select\"])(_reducers_range__WEBPACK_IMPORTED_MODULE_6__[\"getSelectedRanges\"]);\n\n case 8:\n selectedRangeIds = _context11.sent;\n\n if (selectedRangeIds.length) {\n _context11.next = 11;\n break;\n }\n\n return _context11.abrupt(\"return\");\n\n case 11:\n _context11.next = 13;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"select\"])(Object(_reducers_range__WEBPACK_IMPORTED_MODULE_6__[\"getRangesByIds\"])(selectedRangeIds));\n\n case 13:\n selectedRanges = _context11.sent;\n startTime = selectedRanges[0].startTime;\n endTime = selectedRanges[selectedRanges.length - 1].endTime;\n _context11.next = 18;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"select\"])(getCurrentTime);\n\n case 18:\n time = _context11.sent;\n\n if (!(type === _constants_viewState__WEBPACK_IMPORTED_MODULE_7__[\"PLAY_AUDIO\"] && time <= startTime || time >= endTime)) {\n _context11.next = 21;\n break;\n }\n\n return _context11.abrupt(\"return\");\n\n case 21:\n // Last time stores previous tick time so that we can compare the gap.\n lastTime = 0; // Click stores if we have made a jump in this code, so we can skip it\n // in our checks when trying to identify user clicks.\n // @todo improvement if we can identify different sources of `SET_CURRENT_TIME` then\n // we can more easily know if the jumps in the time are from human clicks or automated.\n\n clicked = false; // We want to listen for every tick of the time.\n\n case 23:\n if (false) {}\n\n _context11.next = 26;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"take\"])(_constants_viewState__WEBPACK_IMPORTED_MODULE_7__[\"SET_CURRENT_TIME\"]);\n\n case 26:\n _ref5 = _context11.sent;\n currentTime = _ref5.payload.currentTime;\n\n if (!(clicked === false && lastTime && Math.abs(currentTime - lastTime) >= 1000)) {\n _context11.next = 30;\n break;\n }\n\n return _context11.abrupt(\"break\", 44);\n\n case 30:\n // Reset the clicked value if set.\n if (clicked) {\n clicked = false;\n } // Set a new last time, since we're done using it.\n\n\n lastTime = currentTime; // When the time goes past the end of a section\n\n if (!(currentTime >= endTime)) {\n _context11.next = 42;\n break;\n }\n\n if (!startPlayingAtEnd) {\n _context11.next = 39;\n break;\n }\n\n // Set a clicked value so we know to skip the user click check.\n clicked = true; // Either we start playing from the beginning of the section.\n\n _context11.next = 37;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"put\"])(Object(_actions_viewState__WEBPACK_IMPORTED_MODULE_5__[\"setCurrentTime\"])(startTime));\n\n case 37:\n _context11.next = 42;\n break;\n\n case 39:\n if (!stopPlayingAtEnd) {\n _context11.next = 42;\n break;\n }\n\n _context11.next = 42;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"put\"])(Object(_actions_viewState__WEBPACK_IMPORTED_MODULE_5__[\"pause\"])());\n\n case 42:\n _context11.next = 23;\n break;\n\n case 44:\n case \"end\":\n return _context11.stop();\n }\n }\n }, _marked11);\n}\n\nfunction deselectOtherRanges(id) {\n var selected;\n return _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.wrap(function deselectOtherRanges$(_context12) {\n while (1) {\n switch (_context12.prev = _context12.next) {\n case 0:\n _context12.next = 2;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"select\"])(_reducers_range__WEBPACK_IMPORTED_MODULE_6__[\"getSelectedRanges\"]);\n\n case 2:\n selected = _context12.sent;\n return _context12.delegateYield((selected || []).filter(function (range) {\n return range !== id;\n }).map(function (range) {\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"put\"])(Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_objectSpread__WEBPACK_IMPORTED_MODULE_0__[\"default\"])({}, Object(_actions_range__WEBPACK_IMPORTED_MODULE_9__[\"deselectRange\"])(range), {\n meta: {\n fromSaga: true\n }\n }));\n }), \"t0\", 4);\n\n case 4:\n case \"end\":\n return _context12.stop();\n }\n }\n }, _marked12);\n}\n\nfunction deselectAllRangesSaga(_ref6) {\n var payload, selectedRangeIds, selectedRanges, startTime, state;\n return _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.wrap(function deselectAllRangesSaga$(_context13) {\n while (1) {\n switch (_context13.prev = _context13.next) {\n case 0:\n payload = _ref6.payload;\n _context13.next = 3;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"select\"])(_reducers_range__WEBPACK_IMPORTED_MODULE_6__[\"getSelectedRanges\"]);\n\n case 3:\n selectedRangeIds = _context13.sent;\n\n if (selectedRangeIds.length) {\n _context13.next = 6;\n break;\n }\n\n return _context13.abrupt(\"return\");\n\n case 6:\n _context13.next = 8;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"select\"])(Object(_reducers_range__WEBPACK_IMPORTED_MODULE_6__[\"getRangesByIds\"])(selectedRangeIds));\n\n case 8:\n selectedRanges = _context13.sent;\n startTime = selectedRanges[0].startTime;\n _context13.next = 12;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"select\"])(function (s) {\n return s;\n });\n\n case 12:\n state = _context13.sent;\n\n if (!(payload.currentTime === startTime && !state.viewState.isPlaying)) {\n _context13.next = 15;\n break;\n }\n\n return _context13.abrupt(\"return\");\n\n case 15:\n _context13.next = 17;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"call\"])(deselectOtherRanges, null);\n\n case 17:\n case \"end\":\n return _context13.stop();\n }\n }\n }, _marked13);\n}\n\nfunction selectRangeSaga(_ref7) {\n var payload;\n return _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.wrap(function selectRangeSaga$(_context14) {\n while (1) {\n switch (_context14.prev = _context14.next) {\n case 0:\n payload = _ref7.payload;\n\n if (!payload.deselectOthers) {\n _context14.next = 4;\n break;\n }\n\n _context14.next = 4;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"call\"])(deselectOtherRanges, payload.id);\n\n case 4:\n _context14.next = 6;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"call\"])(playWhenBubbleIsClicked, payload.id, true);\n\n case 6:\n case \"end\":\n return _context14.stop();\n }\n }\n }, _marked14);\n}\n\nfunction deselectRangeSaga(_ref8) {\n var payload, meta;\n return _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.wrap(function deselectRangeSaga$(_context15) {\n while (1) {\n switch (_context15.prev = _context15.next) {\n case 0:\n payload = _ref8.payload, meta = _ref8.meta;\n\n if (!(meta && meta.fromSaga)) {\n _context15.next = 3;\n break;\n }\n\n return _context15.abrupt(\"return\");\n\n case 3:\n _context15.next = 5;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"call\"])(playWhenBubbleIsClicked, payload.id, false);\n\n case 5:\n case \"end\":\n return _context15.stop();\n }\n }\n }, _marked15);\n}\n\nfunction createRangeAction(data) {\n return _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.wrap(function createRangeAction$(_context16) {\n while (1) {\n switch (_context16.prev = _context16.next) {\n case 0:\n return _context16.abrupt(\"return\", Object(_actions_range__WEBPACK_IMPORTED_MODULE_9__[\"createRange\"])(data));\n\n case 1:\n case \"end\":\n return _context16.stop();\n }\n }\n }, _marked16);\n}\n/**\n * Resolve parents depths\n *\n * Given an initial target depth of a \"child\" of a set of parents, this will\n * change the parents depth if required, and then recursively to the same\n * to that bubble. It walks all the way up parents parents bumping the size\n * if the parent bubble can no longer fit the child bubble.\n *\n * Return value is an array of depth changes.\n *\n * This is a co-routine that needs the state so it is wrapped in this generator.\n * @param initialDepth\n * @param initialParents\n * @returns {IterableIterator<*>}\n */\n\nfunction resolveParentDepths(initialDepth, initialParents) {\n var state, depthChangeParents;\n return _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.wrap(function resolveParentDepths$(_context17) {\n while (1) {\n switch (_context17.prev = _context17.next) {\n case 0:\n _context17.next = 2;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"select\"])();\n\n case 2:\n state = _context17.sent;\n\n // Create a recursive function.\n depthChangeParents = function depthChangeParents(childDepth, parents) {\n var mutations = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : [];\n // Loop through the parents\n return parents.reduce(function (acc, range) {\n // If we \"bumped\" the child depth by one, would this particular parent still be\n // tall enough to contain this new height.\n if (childDepth + 1 === range.depth) {\n // If not, increase the depth of it\n acc.push(Object(_actions_range__WEBPACK_IMPORTED_MODULE_9__[\"increaseRangeDepth\"])(range.id)); // Now get its direct parents.\n\n var parentsParents = getParentBubbles(range)(state); // And do the same process, using this ranges depth.\n\n return depthChangeParents(range.depth, parentsParents, acc);\n } // Accumulate all of the mutations.\n\n\n return acc;\n }, mutations);\n };\n\n return _context17.abrupt(\"return\", depthChangeParents(initialDepth, initialParents));\n\n case 5:\n case \"end\":\n return _context17.stop();\n }\n }\n }, _marked17);\n}\n\nfunction groupRangeSaga() {\n var selectedRangeIds, selectedRanges, _extractTimesFromRang, startTime, endTime, rangeBubbles, parentBubbles, depth, depthMutations, deselectMutations, newRange;\n\n return _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.wrap(function groupRangeSaga$(_context18) {\n while (1) {\n switch (_context18.prev = _context18.next) {\n case 0:\n _context18.next = 2;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"select\"])(_reducers_range__WEBPACK_IMPORTED_MODULE_6__[\"getSelectedRanges\"]);\n\n case 2:\n selectedRangeIds = _context18.sent;\n\n if (!(selectedRangeIds.length < 2)) {\n _context18.next = 5;\n break;\n }\n\n return _context18.abrupt(\"return\");\n\n case 5:\n _context18.next = 7;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"select\"])(Object(_reducers_range__WEBPACK_IMPORTED_MODULE_6__[\"getRangesByIds\"])(selectedRangeIds));\n\n case 7:\n selectedRanges = _context18.sent;\n _extractTimesFromRang = extractTimesFromRangeList(selectedRanges), startTime = _extractTimesFromRang.startTime, endTime = _extractTimesFromRang.endTime;\n _context18.t0 = canMerge;\n _context18.next = 12;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"select\"])(_reducers_range__WEBPACK_IMPORTED_MODULE_6__[\"getRangeList\"]);\n\n case 12:\n _context18.t1 = _context18.sent;\n _context18.t2 = {\n startTime: startTime,\n endTime: endTime\n };\n\n if ((0, _context18.t0)(_context18.t1, _context18.t2)) {\n _context18.next = 17;\n break;\n }\n\n console.log(\"We can't merge these bubbles.\");\n return _context18.abrupt(\"return\");\n\n case 17:\n _context18.next = 19;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"select\"])(Object(_reducers_range__WEBPACK_IMPORTED_MODULE_6__[\"getRangesBetweenTimes\"])(startTime, endTime));\n\n case 19:\n rangeBubbles = _context18.sent;\n _context18.next = 22;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"select\"])(getParentBubbles({\n startTime: startTime,\n endTime: endTime\n }));\n\n case 22:\n parentBubbles = _context18.sent;\n // Calculate the tallest bubble in the selection\n depth = Math.max.apply(Math, Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_toConsumableArray__WEBPACK_IMPORTED_MODULE_2__[\"default\"])(rangeBubbles.map(function (range) {\n return range.depth;\n }))) || 1; // Calculate any changes to parent bubble depths.\n\n _context18.next = 26;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"call\"])(resolveParentDepths, depth, parentBubbles);\n\n case 26:\n depthMutations = _context18.sent;\n // Deselect currently selected.\n deselectMutations = selectedRangeIds.map(_actions_range__WEBPACK_IMPORTED_MODULE_9__[\"deselectRange\"]); // Create the new range.\n\n _context18.next = 30;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"call\"])(createRangeAction, {\n startTime: startTime,\n endTime: endTime,\n depth: depth + 1\n });\n\n case 30:\n newRange = _context18.sent;\n _context18.next = 33;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"put\"])(Object(_actions_range__WEBPACK_IMPORTED_MODULE_9__[\"rangeMutations\"])([].concat(Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_toConsumableArray__WEBPACK_IMPORTED_MODULE_2__[\"default\"])(deselectMutations), Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_toConsumableArray__WEBPACK_IMPORTED_MODULE_2__[\"default\"])(depthMutations), [newRange, Object(_actions_range__WEBPACK_IMPORTED_MODULE_9__[\"selectRange\"])(newRange.payload.id)])));\n\n case 33:\n case \"end\":\n return _context18.stop();\n }\n }\n }, _marked18);\n}\n\nfunction splitRangeSaga(_ref9) {\n var time, ranges, itemToSplit;\n return _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.wrap(function splitRangeSaga$(_context19) {\n while (1) {\n switch (_context19.prev = _context19.next) {\n case 0:\n time = _ref9.payload.time;\n _context19.next = 3;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"select\"])(_reducers_range__WEBPACK_IMPORTED_MODULE_6__[\"getRangeList\"]);\n\n case 3:\n ranges = _context19.sent;\n itemToSplit = Object.values(ranges).filter(function (bubble) {\n return time <= bubble.endTime && time >= bubble.startTime;\n }).sort(function (b1, b2) {\n return b1.depth - b2.depth;\n })[0];\n Object(_utils_invariant__WEBPACK_IMPORTED_MODULE_12__[\"default\"])(function () {\n return itemToSplit && itemToSplit.id && itemToSplit.endTime;\n }, 'Unstable state: There should be a bubble at every point'); // If we wanted to implement split left, so that a new bubble would be created on the left side\n // yield put(updateRangeTime(itemToSplit.id, { startTime }));\n // yield put(createRange({ startTime, endTime: time }));\n\n _context19.next = 8;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"put\"])(Object(_actions_range__WEBPACK_IMPORTED_MODULE_9__[\"rangeMutations\"])([Object(_actions_range__WEBPACK_IMPORTED_MODULE_9__[\"createRange\"])({\n startTime: time,\n endTime: itemToSplit.endTime\n }), Object(_actions_range__WEBPACK_IMPORTED_MODULE_9__[\"updateRangeTime\"])(itemToSplit.id, {\n endTime: time\n })]));\n\n case 8:\n _context19.next = 10;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"call\"])(setIsSavedStatus);\n\n case 10:\n case \"end\":\n return _context19.stop();\n }\n }\n }, _marked19);\n}\n\nfunction deleteRangeRequest(toRemoveId) {\n var toRemoveIds, redundantSizes, toRemove, rangeList, rangeArray, remainingRanges, depthMutations, newDepthMap, _iteratorNormalCompletion, _didIteratorError, _iteratorError, _loop, _iterator, _step, _ret, sizeMutations, _iteratorNormalCompletion2, _didIteratorError2, _iteratorError2, _loop2, _iterator2, _step2, _ret2;\n\n return _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.wrap(function deleteRangeRequest$(_context22) {\n while (1) {\n switch (_context22.prev = _context22.next) {\n case 0:\n toRemoveIds = [toRemoveId]; // Removing redundant sizes first\n\n _context22.next = 3;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"call\"])(calculateRedundantSizes, toRemoveIds);\n\n case 3:\n redundantSizes = _context22.sent;\n // Map these Ids to a delete range action.\n toRemove = redundantSizes.map(_actions_range__WEBPACK_IMPORTED_MODULE_9__[\"deleteRange\"]); // Some useful variables.\n\n _context22.next = 7;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"select\"])(_reducers_range__WEBPACK_IMPORTED_MODULE_6__[\"getRangeList\"]);\n\n case 7:\n rangeList = _context22.sent;\n rangeArray = Object.values(rangeList); // Create new list of bubbles\n // Run through a validation step to ensure they are of the correct level.\n\n remainingRanges = rangeArray.filter(function (range) {\n return redundantSizes.indexOf(range.id) === -1 && range.depth > 1;\n }).sort(function (a, b) {\n return a.depth === b.depth ? 0 : a.depth < b.depth ? -1 : 1;\n });\n depthMutations = [];\n newDepthMap = {}; // Loop through the remaining ranges to calculate depth changes.\n\n _iteratorNormalCompletion = true;\n _didIteratorError = false;\n _iteratorError = undefined;\n _context22.prev = 15;\n _loop =\n /*#__PURE__*/\n _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.mark(function _loop() {\n var parent, rangeChildren, maxDepthOfChildren;\n return _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.wrap(function _loop$(_context20) {\n while (1) {\n switch (_context20.prev = _context20.next) {\n case 0:\n parent = _step.value;\n _context20.next = 3;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"select\"])(Object(_reducers_range__WEBPACK_IMPORTED_MODULE_6__[\"getRangesBetweenTimes\"])(parent.startTime, parent.endTime));\n\n case 3:\n _context20.t0 = function (range) {\n return range.id !== parent.id && redundantSizes.indexOf(range.id) === -1;\n };\n\n rangeChildren = _context20.sent.filter(_context20.t0);\n\n if (rangeChildren.length) {\n _context20.next = 7;\n break;\n }\n\n return _context20.abrupt(\"return\", \"continue\");\n\n case 7:\n // Calculate the maximum depth, taking into account any changes made in this loop.\n maxDepthOfChildren = Math.max.apply(Math, Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_toConsumableArray__WEBPACK_IMPORTED_MODULE_2__[\"default\"])(rangeChildren.map(function (range) {\n return newDepthMap[range.id] || range.depth;\n }))); // Continue if there are no changes.\n\n if (!(maxDepthOfChildren + 1 === parent.depth)) {\n _context20.next = 10;\n break;\n }\n\n return _context20.abrupt(\"return\", \"continue\");\n\n case 10:\n // This indicates we have an incorrect depth,\n if (maxDepthOfChildren + 1 < parent.depth) {\n // Decrease the range if it is too much higher than children.\n newDepthMap[parent.id] = parent.depth - 1;\n depthMutations.push(Object(_actions_range__WEBPACK_IMPORTED_MODULE_9__[\"decreaseRangeDepth\"])(parent.id));\n } else {\n // Increase the range if it can't fit the children anymore.\n newDepthMap[parent.id] = parent.depth + 1;\n depthMutations.push(Object(_actions_range__WEBPACK_IMPORTED_MODULE_9__[\"increaseRangeDepth\"])(parent.id));\n }\n\n case 11:\n case \"end\":\n return _context20.stop();\n }\n }\n }, _loop);\n });\n _iterator = remainingRanges[Symbol.iterator]();\n\n case 18:\n if (_iteratorNormalCompletion = (_step = _iterator.next()).done) {\n _context22.next = 26;\n break;\n }\n\n return _context22.delegateYield(_loop(), \"t0\", 20);\n\n case 20:\n _ret = _context22.t0;\n\n if (!(_ret === \"continue\")) {\n _context22.next = 23;\n break;\n }\n\n return _context22.abrupt(\"continue\", 23);\n\n case 23:\n _iteratorNormalCompletion = true;\n _context22.next = 18;\n break;\n\n case 26:\n _context22.next = 32;\n break;\n\n case 28:\n _context22.prev = 28;\n _context22.t1 = _context22[\"catch\"](15);\n _didIteratorError = true;\n _iteratorError = _context22.t1;\n\n case 32:\n _context22.prev = 32;\n _context22.prev = 33;\n\n if (!_iteratorNormalCompletion && _iterator.return != null) {\n _iterator.return();\n }\n\n case 35:\n _context22.prev = 35;\n\n if (!_didIteratorError) {\n _context22.next = 38;\n break;\n }\n\n throw _iteratorError;\n\n case 38:\n return _context22.finish(35);\n\n case 39:\n return _context22.finish(32);\n\n case 40:\n // Make remaining ranges grow right (or left if that's not possible)\n sizeMutations = []; // Loop through all of the deleted ranges.\n\n _iteratorNormalCompletion2 = true;\n _didIteratorError2 = false;\n _iteratorError2 = undefined;\n _context22.prev = 44;\n _loop2 =\n /*#__PURE__*/\n _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.mark(function _loop2() {\n var deletedId, rangeToRemove, rangeChildren, parentRange, siblingRanges, lookLeftSibling, lookRightSibling, leftBubbles, _iteratorNormalCompletion3, _didIteratorError3, _iteratorError3, _iterator3, _step3, leftBubble, rightBubbles, _iteratorNormalCompletion4, _didIteratorError4, _iteratorError4, _iterator4, _step4, rightBubble;\n\n return _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.wrap(function _loop2$(_context21) {\n while (1) {\n switch (_context21.prev = _context21.next) {\n case 0:\n deletedId = _step2.value;\n // Get the full range object.\n rangeToRemove = rangeList[deletedId]; // Need to check if this range has children first, children grow inside so\n // no sibling bubbles will change size in this case.\n\n _context21.next = 4;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"select\"])(Object(_reducers_range__WEBPACK_IMPORTED_MODULE_6__[\"getRangesBetweenTimes\"])(rangeToRemove.startTime, rangeToRemove.endTime));\n\n case 4:\n _context21.t0 = function (range) {\n return range.id !== rangeToRemove.id;\n };\n\n rangeChildren = _context21.sent.filter(_context21.t0);\n\n if (!rangeChildren.length) {\n _context21.next = 8;\n break;\n }\n\n return _context21.abrupt(\"return\", \"continue\");\n\n case 8:\n _context21.next = 10;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"call\"])(getDirectParentRange, rangeToRemove);\n\n case 10:\n parentRange = _context21.sent;\n\n if (!parentRange) {\n _context21.next = 23;\n break;\n }\n\n _context21.next = 14;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"call\"])(getSiblingRanges, parentRange, rangeToRemove);\n\n case 14:\n siblingRanges = _context21.sent;\n lookLeftSibling = getRangeToTheLeft(siblingRanges, rangeToRemove);\n\n if (!lookLeftSibling) {\n _context21.next = 19;\n break;\n }\n\n sizeMutations.push(Object(_actions_range__WEBPACK_IMPORTED_MODULE_9__[\"updateRangeTime\"])(lookLeftSibling.id, {\n endTime: rangeToRemove.endTime\n }));\n return _context21.abrupt(\"return\", \"continue\");\n\n case 19:\n lookRightSibling = getRangeToTheRight(siblingRanges, rangeToRemove);\n\n if (!lookRightSibling) {\n _context21.next = 23;\n break;\n }\n\n sizeMutations.push(Object(_actions_range__WEBPACK_IMPORTED_MODULE_9__[\"updateRangeTime\"])(lookRightSibling.id, {\n startTime: rangeToRemove.startTime\n }));\n return _context21.abrupt(\"return\", \"continue\");\n\n case 23:\n // At this point we know that bubbles inside the parent won't shift to fill the space.\n // First look left to find all the bubbles that share the startTime.\n leftBubbles = rangeArray.filter(function (range) {\n return range.id !== rangeToRemove.id && (range.endTime === rangeToRemove.startTime || range.startTime === rangeToRemove.startTime);\n }); // And queue them up to be moved.\n\n if (!(canMoveBubbles(leftBubbles, rangeToRemove) && leftBubbles.length)) {\n _context21.next = 45;\n break;\n }\n\n _iteratorNormalCompletion3 = true;\n _didIteratorError3 = false;\n _iteratorError3 = undefined;\n _context21.prev = 28;\n\n for (_iterator3 = leftBubbles[Symbol.iterator](); !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) {\n leftBubble = _step3.value;\n // Overlap bubble growing from the left.\n sizeMutations.push(overlapBubbleLeft(rangeToRemove, leftBubble));\n }\n\n _context21.next = 36;\n break;\n\n case 32:\n _context21.prev = 32;\n _context21.t1 = _context21[\"catch\"](28);\n _didIteratorError3 = true;\n _iteratorError3 = _context21.t1;\n\n case 36:\n _context21.prev = 36;\n _context21.prev = 37;\n\n if (!_iteratorNormalCompletion3 && _iterator3.return != null) {\n _iterator3.return();\n }\n\n case 39:\n _context21.prev = 39;\n\n if (!_didIteratorError3) {\n _context21.next = 42;\n break;\n }\n\n throw _iteratorError3;\n\n case 42:\n return _context21.finish(39);\n\n case 43:\n return _context21.finish(36);\n\n case 44:\n return _context21.abrupt(\"return\", \"continue\");\n\n case 45:\n // Now we look to the right of the bubble to find bubbles that share\n // the endTime. This is only applicable when there are no bubbles\n // to the left (i.e. a bubble with startTime = 0)\n rightBubbles = rangeArray.filter(function (range) {\n return range.id !== rangeToRemove.id && (range.startTime === rangeToRemove.endTime || range.endTime === rangeToRemove.startTime);\n }); // And queue these up to be moved.\n\n if (!(canMoveBubbles(rightBubbles, rangeToRemove) && rightBubbles.length)) {\n _context21.next = 66;\n break;\n }\n\n _iteratorNormalCompletion4 = true;\n _didIteratorError4 = false;\n _iteratorError4 = undefined;\n _context21.prev = 50;\n\n for (_iterator4 = rightBubbles[Symbol.iterator](); !(_iteratorNormalCompletion4 = (_step4 = _iterator4.next()).done); _iteratorNormalCompletion4 = true) {\n rightBubble = _step4.value;\n // Overlap bubble growing from the right.\n sizeMutations.push(overlapBubbleRight(rangeToRemove, rightBubble));\n } // continue;\n\n\n _context21.next = 58;\n break;\n\n case 54:\n _context21.prev = 54;\n _context21.t2 = _context21[\"catch\"](50);\n _didIteratorError4 = true;\n _iteratorError4 = _context21.t2;\n\n case 58:\n _context21.prev = 58;\n _context21.prev = 59;\n\n if (!_iteratorNormalCompletion4 && _iterator4.return != null) {\n _iterator4.return();\n }\n\n case 61:\n _context21.prev = 61;\n\n if (!_didIteratorError4) {\n _context21.next = 64;\n break;\n }\n\n throw _iteratorError4;\n\n case 64:\n return _context21.finish(61);\n\n case 65:\n return _context21.finish(58);\n\n case 66:\n case \"end\":\n return _context21.stop();\n }\n }\n }, _loop2, null, [[28, 32, 36, 44], [37,, 39, 43], [50, 54, 58, 66], [59,, 61, 65]]);\n });\n _iterator2 = redundantSizes[Symbol.iterator]();\n\n case 47:\n if (_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done) {\n _context22.next = 55;\n break;\n }\n\n return _context22.delegateYield(_loop2(), \"t2\", 49);\n\n case 49:\n _ret2 = _context22.t2;\n\n if (!(_ret2 === \"continue\")) {\n _context22.next = 52;\n break;\n }\n\n return _context22.abrupt(\"continue\", 52);\n\n case 52:\n _iteratorNormalCompletion2 = true;\n _context22.next = 47;\n break;\n\n case 55:\n _context22.next = 61;\n break;\n\n case 57:\n _context22.prev = 57;\n _context22.t3 = _context22[\"catch\"](44);\n _didIteratorError2 = true;\n _iteratorError2 = _context22.t3;\n\n case 61:\n _context22.prev = 61;\n _context22.prev = 62;\n\n if (!_iteratorNormalCompletion2 && _iterator2.return != null) {\n _iterator2.return();\n }\n\n case 64:\n _context22.prev = 64;\n\n if (!_didIteratorError2) {\n _context22.next = 67;\n break;\n }\n\n throw _iteratorError2;\n\n case 67:\n return _context22.finish(64);\n\n case 68:\n return _context22.finish(61);\n\n case 69:\n _context22.next = 71;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"put\"])(Object(_actions_range__WEBPACK_IMPORTED_MODULE_9__[\"rangeMutations\"])([].concat(Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_toConsumableArray__WEBPACK_IMPORTED_MODULE_2__[\"default\"])(toRemove), depthMutations, sizeMutations)));\n\n case 71:\n case \"end\":\n return _context22.stop();\n }\n }\n }, _marked20, null, [[15, 28, 32, 40], [33,, 35, 39], [44, 57, 61, 69], [62,, 64, 68]]);\n}\n\nfunction singleDelete(_ref10) {\n var id;\n return _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.wrap(function singleDelete$(_context23) {\n while (1) {\n switch (_context23.prev = _context23.next) {\n case 0:\n id = _ref10.payload.id;\n _context23.next = 3;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"call\"])(deleteRangeRequest, id);\n\n case 3:\n _context23.next = 5;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"call\"])(setIsSavedStatus);\n\n case 5:\n case \"end\":\n return _context23.stop();\n }\n }\n }, _marked21);\n}\n\nfunction multiDelete(_ref11) {\n var ranges, confirmed, _iteratorNormalCompletion5, _didIteratorError5, _iteratorError5, _iterator5, _step5, range;\n\n return _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.wrap(function multiDelete$(_context24) {\n while (1) {\n switch (_context24.prev = _context24.next) {\n case 0:\n ranges = _ref11.payload.ranges;\n\n if (!(ranges.length > 1)) {\n _context24.next = 7;\n break;\n }\n\n _context24.next = 4;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"call\"])(_index__WEBPACK_IMPORTED_MODULE_13__[\"showConfirmation\"], 'Multiple sections will be deleted. Redundant sections will be removed. Do you wish to continue?');\n\n case 4:\n confirmed = _context24.sent;\n\n if (!(confirmed === false)) {\n _context24.next = 7;\n break;\n }\n\n return _context24.abrupt(\"return\");\n\n case 7:\n _iteratorNormalCompletion5 = true;\n _didIteratorError5 = false;\n _iteratorError5 = undefined;\n _context24.prev = 10;\n _iterator5 = ranges[Symbol.iterator]();\n\n case 12:\n if (_iteratorNormalCompletion5 = (_step5 = _iterator5.next()).done) {\n _context24.next = 19;\n break;\n }\n\n range = _step5.value;\n _context24.next = 16;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"call\"])(deleteRangeRequest, range);\n\n case 16:\n _iteratorNormalCompletion5 = true;\n _context24.next = 12;\n break;\n\n case 19:\n _context24.next = 25;\n break;\n\n case 21:\n _context24.prev = 21;\n _context24.t0 = _context24[\"catch\"](10);\n _didIteratorError5 = true;\n _iteratorError5 = _context24.t0;\n\n case 25:\n _context24.prev = 25;\n _context24.prev = 26;\n\n if (!_iteratorNormalCompletion5 && _iterator5.return != null) {\n _iterator5.return();\n }\n\n case 28:\n _context24.prev = 28;\n\n if (!_didIteratorError5) {\n _context24.next = 31;\n break;\n }\n\n throw _iteratorError5;\n\n case 31:\n return _context24.finish(28);\n\n case 32:\n return _context24.finish(25);\n\n case 33:\n _context24.next = 35;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"call\"])(setIsSavedStatus);\n\n case 35:\n case \"end\":\n return _context24.stop();\n }\n }\n }, _marked22, null, [[10, 21, 25, 33], [26,, 28, 32]]);\n}\n\nfunction clearCustomColorsSaga() {\n var rangeList, rangeIds, _i, rangeId;\n\n return _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.wrap(function clearCustomColorsSaga$(_context25) {\n while (1) {\n switch (_context25.prev = _context25.next) {\n case 0:\n _context25.next = 2;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"select\"])(_reducers_range__WEBPACK_IMPORTED_MODULE_6__[\"getRangeList\"]);\n\n case 2:\n rangeList = _context25.sent;\n rangeIds = Object.keys(rangeList);\n _i = 0;\n\n case 5:\n if (!(_i < rangeIds.length)) {\n _context25.next = 12;\n break;\n }\n\n rangeId = rangeIds[_i];\n _context25.next = 9;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"put\"])(Object(_actions_range__WEBPACK_IMPORTED_MODULE_9__[\"unsetRangeColor\"])(rangeId));\n\n case 9:\n _i++;\n _context25.next = 5;\n break;\n\n case 12:\n _context25.next = 14;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"call\"])(setIsSavedStatus);\n\n case 14:\n case \"end\":\n return _context25.stop();\n }\n }\n }, _marked23);\n}\n\nfunction rangeSaga() {\n return _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.wrap(function rangeSaga$(_context26) {\n while (1) {\n switch (_context26.prev = _context26.next) {\n case 0:\n _context26.next = 2;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"all\"])([Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"takeEvery\"])(_constants_viewState__WEBPACK_IMPORTED_MODULE_7__[\"PREVIOUS_BUBBLE\"], previousBubble), Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"takeEvery\"])(_constants_viewState__WEBPACK_IMPORTED_MODULE_7__[\"NEXT_BUBBLE\"], nextBubble), Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"takeEvery\"])(_constants_range__WEBPACK_IMPORTED_MODULE_8__[\"MOVE_POINT\"], movePointSaga), Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"takeEvery\"])(_constants_range__WEBPACK_IMPORTED_MODULE_8__[\"SCHEDULE_UPDATE_RANGE\"], saveRangeSaga), Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"takeEvery\"])(_constants_range__WEBPACK_IMPORTED_MODULE_8__[\"SELECT_RANGE\"], selectRangeSaga), Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"takeLatest\"])([_constants_range__WEBPACK_IMPORTED_MODULE_8__[\"SELECT_RANGE\"], _constants_range__WEBPACK_IMPORTED_MODULE_8__[\"DESELECT_RANGE\"]], currentTimeSaga), Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"takeEvery\"])(_constants_range__WEBPACK_IMPORTED_MODULE_8__[\"DESELECT_RANGE\"], deselectRangeSaga), Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"takeEvery\"])(_constants_range__WEBPACK_IMPORTED_MODULE_8__[\"GROUP_RANGES\"], groupRangeSaga), Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"takeEvery\"])(_constants_range__WEBPACK_IMPORTED_MODULE_8__[\"SPLIT_RANGE_AT\"], splitRangeSaga), Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"takeEvery\"])(_constants_range__WEBPACK_IMPORTED_MODULE_8__[\"SCHEDULE_DELETE_RANGE\"], singleDelete), Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"takeEvery\"])(_constants_range__WEBPACK_IMPORTED_MODULE_8__[\"SCHEDULE_DELETE_RANGES\"], multiDelete), Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"takeEvery\"])(_constants_viewState__WEBPACK_IMPORTED_MODULE_7__[\"SET_CURRENT_TIME\"], deselectAllRangesSaga), Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"takeEvery\"])(_constants_project__WEBPACK_IMPORTED_MODULE_11__[\"CLEAR_CUSTOM_COLORS\"], clearCustomColorsSaga)]);\n\n case 2:\n case \"end\":\n return _context26.stop();\n }\n }\n }, _marked24);\n}\n\n//# sourceURL=webpack:///./src/sagas/range-saga.js?"); + eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"STICKY_BUBBLE_MS\", function() { return STICKY_BUBBLE_MS; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"getStickyPointDelta\", function() { return getStickyPointDelta; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"currentTimeSaga\", function() { return currentTimeSaga; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"createRangeAction\", function() { return createRangeAction; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"default\", function() { return rangeSaga; });\n/* harmony import */ var _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_objectSpread__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./node_modules/babel-preset-react-app/node_modules/@babel/runtime/helpers/esm/objectSpread */ \"./node_modules/babel-preset-react-app/node_modules/@babel/runtime/helpers/esm/objectSpread.js\");\n/* harmony import */ var _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_objectWithoutProperties__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./node_modules/babel-preset-react-app/node_modules/@babel/runtime/helpers/esm/objectWithoutProperties */ \"./node_modules/babel-preset-react-app/node_modules/@babel/runtime/helpers/esm/objectWithoutProperties.js\");\n/* harmony import */ var _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_toConsumableArray__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./node_modules/babel-preset-react-app/node_modules/@babel/runtime/helpers/esm/toConsumableArray */ \"./node_modules/babel-preset-react-app/node_modules/@babel/runtime/helpers/esm/toConsumableArray.js\");\n/* harmony import */ var _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./node_modules/babel-preset-react-app/node_modules/@babel/runtime/regenerator */ \"./node_modules/babel-preset-react-app/node_modules/@babel/runtime/regenerator/index.js\");\n/* harmony import */ var _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3__);\n/* harmony import */ var redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! redux-saga/effects */ \"./node_modules/redux-saga/es/effects.js\");\n/* harmony import */ var _actions_viewState__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ../actions/viewState */ \"./src/actions/viewState.js\");\n/* harmony import */ var _reducers_range__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ../reducers/range */ \"./src/reducers/range.js\");\n/* harmony import */ var _constants_viewState__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ../constants/viewState */ \"./src/constants/viewState.js\");\n/* harmony import */ var _constants_range__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(/*! ../constants/range */ \"./src/constants/range.js\");\n/* harmony import */ var _actions_range__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(/*! ../actions/range */ \"./src/actions/range.js\");\n/* harmony import */ var _actions_project__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(/*! ../actions/project */ \"./src/actions/project.js\");\n/* harmony import */ var _constants_project__WEBPACK_IMPORTED_MODULE_11__ = __webpack_require__(/*! ../constants/project */ \"./src/constants/project.js\");\n/* harmony import */ var _utils_invariant__WEBPACK_IMPORTED_MODULE_12__ = __webpack_require__(/*! ../utils/invariant */ \"./src/utils/invariant.js\");\n/* harmony import */ var _index__WEBPACK_IMPORTED_MODULE_13__ = __webpack_require__(/*! ./index */ \"./src/sagas/index.js\");\n\n\n\n\n\nvar _marked =\n/*#__PURE__*/\n_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.mark(setIsSavedStatus),\n _marked2 =\n/*#__PURE__*/\n_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.mark(previousBubble),\n _marked3 =\n/*#__PURE__*/\n_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.mark(nextBubble),\n _marked4 =\n/*#__PURE__*/\n_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.mark(calculateRedundantSizes),\n _marked5 =\n/*#__PURE__*/\n_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.mark(getDirectParentRange),\n _marked6 =\n/*#__PURE__*/\n_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.mark(getSiblingRanges),\n _marked7 =\n/*#__PURE__*/\n_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.mark(getMovePointMutations),\n _marked8 =\n/*#__PURE__*/\n_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.mark(saveRangeSaga),\n _marked9 =\n/*#__PURE__*/\n_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.mark(movePointSaga),\n _marked10 =\n/*#__PURE__*/\n_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.mark(playWhenBubbleIsClicked),\n _marked11 =\n/*#__PURE__*/\n_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.mark(currentTimeSaga),\n _marked12 =\n/*#__PURE__*/\n_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.mark(deselectOtherRanges),\n _marked13 =\n/*#__PURE__*/\n_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.mark(deselectAllRangesSaga),\n _marked14 =\n/*#__PURE__*/\n_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.mark(selectRangeSaga),\n _marked15 =\n/*#__PURE__*/\n_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.mark(deselectRangeSaga),\n _marked16 =\n/*#__PURE__*/\n_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.mark(createRangeAction),\n _marked17 =\n/*#__PURE__*/\n_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.mark(resolveParentDepths),\n _marked18 =\n/*#__PURE__*/\n_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.mark(groupRangeSaga),\n _marked19 =\n/*#__PURE__*/\n_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.mark(splitRangeSaga),\n _marked20 =\n/*#__PURE__*/\n_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.mark(deleteRangeRequest),\n _marked21 =\n/*#__PURE__*/\n_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.mark(singleDelete),\n _marked22 =\n/*#__PURE__*/\n_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.mark(multiDelete),\n _marked23 =\n/*#__PURE__*/\n_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.mark(clearCustomColorsSaga),\n _marked24 =\n/*#__PURE__*/\n_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.mark(rangeSaga);\n\n\n\n\n\n\n\n\n\n\n\nvar STICKY_BUBBLE_MS = 500;\n\nfunction setIsSavedStatus() {\n return _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.wrap(function setIsSavedStatus$(_context) {\n while (1) {\n switch (_context.prev = _context.next) {\n case 0:\n _context.next = 2;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"put\"])(Object(_actions_project__WEBPACK_IMPORTED_MODULE_10__[\"setProjectChanged\"])(false));\n\n case 2:\n case \"end\":\n return _context.stop();\n }\n }\n }, _marked);\n}\n\nfunction previousBubble() {\n var previousBubbleTime;\n return _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.wrap(function previousBubble$(_context2) {\n while (1) {\n switch (_context2.prev = _context2.next) {\n case 0:\n _context2.next = 2;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"select\"])(_reducers_range__WEBPACK_IMPORTED_MODULE_6__[\"getPreviousBubbleStartTime\"]);\n\n case 2:\n previousBubbleTime = _context2.sent;\n\n if (!Number.isFinite(previousBubbleTime)) {\n _context2.next = 8;\n break;\n }\n\n _context2.next = 6;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"put\"])(Object(_actions_viewState__WEBPACK_IMPORTED_MODULE_5__[\"setCurrentTime\"])(previousBubbleTime));\n\n case 6:\n _context2.next = 10;\n break;\n\n case 8:\n _context2.next = 10;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"put\"])(Object(_actions_viewState__WEBPACK_IMPORTED_MODULE_5__[\"setCurrentTime\"])(0));\n\n case 10:\n case \"end\":\n return _context2.stop();\n }\n }\n }, _marked2);\n}\n\nfunction nextBubble() {\n var nextBubbleTime;\n return _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.wrap(function nextBubble$(_context3) {\n while (1) {\n switch (_context3.prev = _context3.next) {\n case 0:\n _context3.next = 2;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"select\"])(_reducers_range__WEBPACK_IMPORTED_MODULE_6__[\"getNextBubbleStartTime\"]);\n\n case 2:\n nextBubbleTime = _context3.sent;\n _context3.next = 5;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"put\"])(Object(_actions_viewState__WEBPACK_IMPORTED_MODULE_5__[\"setCurrentTime\"])(nextBubbleTime.time));\n\n case 5:\n case \"end\":\n return _context3.stop();\n }\n }\n }, _marked3);\n}\n\nvar getCurrentTime = function getCurrentTime(state) {\n return state.viewState.currentTime;\n};\n/**\n * Can points merge\n *\n * Given a list of points and a start and end time this will\n * return true if the bubbles can be grouped.\n *\n * This is to avoid intersecting bubbles, which is not allowed.\n *\n * @param points\n * @param startTime\n * @param endTime\n * @returns {boolean}\n */\n\n\nfunction canMerge(points, _ref) {\n var startTime = _ref.startTime,\n endTime = _ref.endTime;\n return Object.values(points).filter(function (bubble) {\n return bubble.depth > 1;\n }).filter(function (bubble) {\n return bubble.startTime < startTime && startTime < bubble.endTime && bubble.endTime < endTime || startTime < bubble.startTime && bubble.startTime < endTime && endTime < bubble.endTime || bubble.startTime === startTime && bubble.endTime === endTime;\n }).length === 0;\n}\n/**\n * Get sticky point delta\n *\n * From a list of ranges and a single point in time this will return\n * the largest delta within `sticky` (default: 50ms) so that you can\n * use it to auto-correct inaccuracy of user pointers.\n *\n * @param ranges\n * @param x\n * @param sticky\n * @returns {T | *}\n */\n\n\nfunction getStickyPointDelta(ranges, x, sticky) {\n return ranges.reduce(function (stickyCandidates, range) {\n if (Math.abs(range.startTime - x) <= sticky) {\n stickyCandidates.push(range.startTime);\n }\n\n if (Math.abs(range.endTime - x) <= sticky) {\n stickyCandidates.push(range.endTime);\n }\n\n return stickyCandidates;\n }, []).filter(function (r) {\n return r > 0;\n }).sort(function (a, b) {\n return Math.abs(x - a) - Math.abs(x - b);\n }).pop() || x;\n}\n/**\n * Sort ranges by depth then time.\n *\n * Sort them first by depth, and then by endTime.\n *\n * This will give an order where parents always come before their\n * children AND they are still ordered by time.\n *\n * @param a\n * @param b\n * @returns {number}\n */\n\nfunction sortRangesByDepthThenTime(a, b) {\n // First by depth, if they are different from each other.\n if (b.depth !== a.depth) {\n return b.depth - a.depth;\n } // Then by end time.\n\n\n if (a.endTime < b.endTime) {\n return -1;\n }\n\n if (a.endTime > b.endTime) {\n return 1;\n } // This shouldn't happen, if they share an end time AND depth, its\n // not a valid bubble.\n\n\n return 0;\n}\n/**\n * Get direct children\n *\n * Given a list of all the children of a grouping bubble, this will\n * return only the DIRECT children.\n *\n * @param childrenRanges\n * @returns {*}\n */\n\n\nfunction getDirectChildren(childrenRanges) {\n return childrenRanges.sort(sortRangesByDepthThenTime).reduce(function (ac, n) {\n // Does it start first in range.\n var isMin = n.startTime < ac.min; // Is furthest reaching in range.\n\n var isMax = n.endTime > ac.max; // Record the min and max.\n\n if (isMin) {\n ac.min = n.startTime;\n }\n\n if (isMax) {\n ac.max = n.endTime;\n } // If we've just established a new min or max, push it on our list.\n\n\n if (isMin || isMax) {\n ac.list.push(n);\n } // Return the state.\n\n\n return ac;\n }, {\n min: Infinity,\n max: 0,\n list: []\n }).list;\n}\n/**\n * Calculate redundant sizes\n *\n * Asks the question, in the current state of the application, if I\n * removed X bubbles, which bubbles would have to be removed.\n *\n * @param toRemoveIds\n * @returns {IterableIterator<*>}\n */\n\n\nfunction calculateRedundantSizes(toRemoveIds) {\n var allRanges;\n return _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.wrap(function calculateRedundantSizes$(_context4) {\n while (1) {\n switch (_context4.prev = _context4.next) {\n case 0:\n _context4.t0 = Object;\n _context4.next = 3;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"select\"])(_reducers_range__WEBPACK_IMPORTED_MODULE_6__[\"getRangeList\"]);\n\n case 3:\n _context4.t1 = _context4.sent;\n allRanges = _context4.t0.values.call(_context4.t0, _context4.t1);\n return _context4.abrupt(\"return\", allRanges.filter(function (range) {\n return toRemoveIds.indexOf(range.id) === -1;\n }).filter(filterOnlyGroupingRanges).sort(function (a, b) {\n return a.depth === b.depth ? 0 : a.depth < b.depth ? -1 : 1;\n }).reduce(function (acc, next) {\n // Filter to only include children inside the current range.\n var filteredChildren = allRanges.filter(function (range) {\n return (// Not already removed\n acc.indexOf(range.id) === -1 && // Is within the parent bubble.\n bubbleIsWithin(next, range)\n );\n }); // From the filtered children, filter out only the DIRECT children.\n\n var directChildren = getDirectChildren(filteredChildren); // If there is only one child, we need to remove it.\n\n if (directChildren.length === 1) {\n acc.push(next.id);\n }\n\n return acc;\n }, toRemoveIds));\n\n case 6:\n case \"end\":\n return _context4.stop();\n }\n }\n }, _marked4);\n}\n/**\n * Filter only grouping ranges\n *\n * This has been split out, it may be expanded later on.\n *\n * @param range\n * @returns {boolean}\n */\n\n\nfunction filterOnlyGroupingRanges(range) {\n return range.depth > 1;\n}\n/**\n * Bubble is withing\n *\n * Filter for two ranges, returns true if `childCandidate` is\n * inside the first range at any level.\n *\n * @param parent\n * @param childCandidate\n * @returns {boolean}\n */\n\n\nfunction bubbleIsWithin(parent, childCandidate) {\n return childCandidate.id !== parent.id && childCandidate.startTime >= parent.startTime && childCandidate.endTime <= parent.endTime;\n}\n/**\n * Fuzzy equal.\n *\n * Compares `a` and `b` and returns if they are within\n * STICK_BUBBLE_MS configuration value (default: 50)\n *\n * @param a\n * @param b\n * @returns {boolean}\n */\n\n\nfunction fuzzyEqual(a, b) {\n return Math.abs(a - b) <= STICKY_BUBBLE_MS;\n}\n/**\n * Extract times from range list\n *\n * This will take a list of ranges and simply return the smallest start\n * time and the largest end time to get a range.\n *\n * @param ranges\n * @returns {{startTime: *, endTime: *}}\n */\n\n\nfunction extractTimesFromRangeList(ranges) {\n var startTime = ranges.reduce(function (cur, next) {\n return next.startTime <= cur ? next.startTime : cur;\n }, Infinity);\n var endTime = ranges.reduce(function (cur, next) {\n return next.endTime >= cur ? next.endTime : cur;\n }, 0);\n return {\n startTime: startTime,\n endTime: endTime\n };\n}\n\nfunction getDirectParentRange(range) {\n var parentBubbles;\n return _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.wrap(function getDirectParentRange$(_context5) {\n while (1) {\n switch (_context5.prev = _context5.next) {\n case 0:\n _context5.next = 2;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"select\"])(getParentBubbles(range));\n\n case 2:\n parentBubbles = _context5.sent;\n return _context5.abrupt(\"return\", parentBubbles.find(function (parentRange) {\n return parentRange.depth === range.depth + 1 && range.startTime >= parentRange.startTime && range.endTime <= parentRange.endTime;\n }));\n\n case 4:\n case \"end\":\n return _context5.stop();\n }\n }\n }, _marked5);\n}\n\nfunction getSiblingRanges(parentRange, childRange) {\n var rangesBetweenParent;\n return _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.wrap(function getSiblingRanges$(_context6) {\n while (1) {\n switch (_context6.prev = _context6.next) {\n case 0:\n _context6.next = 2;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"select\"])(Object(_reducers_range__WEBPACK_IMPORTED_MODULE_6__[\"getRangesBetweenTimes\"])(parentRange.startTime, parentRange.endTime));\n\n case 2:\n rangesBetweenParent = _context6.sent;\n return _context6.abrupt(\"return\", rangesBetweenParent.filter(function (range) {\n return range.id !== childRange.id && range.id !== parentRange.id;\n }));\n\n case 4:\n case \"end\":\n return _context6.stop();\n }\n }\n }, _marked6);\n}\n\nfunction getRangeToTheLeft(siblingRanges, childRange) {\n return siblingRanges.find(function (range) {\n return range.id !== childRange.id && range.depth === childRange.depth && range.endTime === childRange.startTime;\n });\n}\n\nfunction getRangeToTheRight(siblingRanges, childRange) {\n return siblingRanges.find(function (range) {\n return range.id !== childRange.id && range.depth === childRange.depth && range.startTime === childRange.endTime;\n });\n}\n\nfunction overlapBubbleRight(toCover, toGrow) {\n return toGrow.startTime === toCover.endTime ? Object(_actions_range__WEBPACK_IMPORTED_MODULE_9__[\"updateRangeTime\"])(toGrow.id, {\n startTime: toCover.startTime\n }) : Object(_actions_range__WEBPACK_IMPORTED_MODULE_9__[\"updateRangeTime\"])(toGrow.id, {\n endTime: toCover.startTime\n });\n}\n\nfunction overlapBubbleLeft(toCover, toGrow) {\n return toGrow.endTime === toCover.startTime ? Object(_actions_range__WEBPACK_IMPORTED_MODULE_9__[\"updateRangeTime\"])(toGrow.id, {\n endTime: toCover.endTime\n }) : Object(_actions_range__WEBPACK_IMPORTED_MODULE_9__[\"updateRangeTime\"])(toGrow.id, {\n startTime: toCover.endTime\n });\n}\n\nvar getParentBubbles = function getParentBubbles(child) {\n return function (state) {\n return Object.values(Object(_reducers_range__WEBPACK_IMPORTED_MODULE_6__[\"getRangeList\"])(state)).filter(function (range) {\n return bubbleIsWithin(range, child);\n }).sort(function (range) {\n return -(range.startTime - range.endTime);\n });\n };\n};\n\nfunction canMoveBubbles(bubbleList, toRemove) {\n return bubbleList.filter(function (range) {\n return range.depth === toRemove.depth;\n }).length > 0;\n}\n\nfunction getMovePointMutations(fromTime, toTime) {\n var ranges, x, toRemoveIds, redundantSizes, toRemove, updateRangeTimes;\n return _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.wrap(function getMovePointMutations$(_context7) {\n while (1) {\n switch (_context7.prev = _context7.next) {\n case 0:\n _context7.next = 2;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"select\"])(Object(_reducers_range__WEBPACK_IMPORTED_MODULE_6__[\"getRangesAtPoint\"])(fromTime, STICKY_BUBBLE_MS));\n\n case 2:\n ranges = _context7.sent;\n x = getStickyPointDelta(ranges, toTime, STICKY_BUBBLE_MS);\n toRemoveIds = ranges.filter(function (range) {\n if (fuzzyEqual(range.startTime, fromTime)) {\n return fuzzyEqual(range.endTime, x);\n }\n\n return fuzzyEqual(range.startTime, x);\n }).map(function (range) {\n return range.id;\n }); // This should be able to be split out.\n\n _context7.next = 7;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"call\"])(calculateRedundantSizes, toRemoveIds);\n\n case 7:\n redundantSizes = _context7.sent;\n toRemove = redundantSizes.map(_actions_range__WEBPACK_IMPORTED_MODULE_9__[\"deleteRange\"]); // When updating range times, need to consider the STICKY_BUBBLE_MS of stickiness.\n\n updateRangeTimes = ranges.filter(function (range) {\n return redundantSizes.indexOf(range.id) === -1;\n }).map(function (range) {\n return fuzzyEqual(range.startTime, fromTime) ? Object(_actions_range__WEBPACK_IMPORTED_MODULE_9__[\"updateRangeTime\"])(range.id, {\n startTime: x\n }) : Object(_actions_range__WEBPACK_IMPORTED_MODULE_9__[\"updateRangeTime\"])(range.id, {\n endTime: x\n });\n }); // Finish the mutation.\n\n return _context7.abrupt(\"return\", [].concat(Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_toConsumableArray__WEBPACK_IMPORTED_MODULE_2__[\"default\"])(updateRangeTimes), Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_toConsumableArray__WEBPACK_IMPORTED_MODULE_2__[\"default\"])(toRemove)));\n\n case 11:\n case \"end\":\n return _context7.stop();\n }\n }\n }, _marked7);\n}\n/**\n * Get range by id\n *\n * Returns a selector for easily getting a single range by id\n * @param id\n * @returns {function(*): *}\n */\n\n\nfunction getRangeById(id) {\n return function (state) {\n return state.range.list[id];\n };\n}\n\nfunction saveRangeSaga(_ref2) {\n var payload, startTime, endTime, restPayload, range, mutations, startMutations, endMutations;\n return _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.wrap(function saveRangeSaga$(_context8) {\n while (1) {\n switch (_context8.prev = _context8.next) {\n case 0:\n payload = _ref2.payload;\n startTime = payload.startTime, endTime = payload.endTime, restPayload = Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_objectWithoutProperties__WEBPACK_IMPORTED_MODULE_1__[\"default\"])(payload, [\"startTime\", \"endTime\"]);\n _context8.next = 4;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"select\"])(getRangeById(restPayload.id));\n\n case 4:\n range = _context8.sent;\n mutations = [{\n type: _constants_range__WEBPACK_IMPORTED_MODULE_8__[\"UPDATE_RANGE\"],\n payload: restPayload\n }];\n\n if (!(typeof startTime !== 'undefined' && startTime !== null)) {\n _context8.next = 11;\n break;\n }\n\n _context8.next = 9;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"call\"])(getMovePointMutations, range.startTime, startTime);\n\n case 9:\n startMutations = _context8.sent;\n startMutations.forEach(function (mutation) {\n return mutations.push(mutation);\n });\n\n case 11:\n if (!(typeof endTime !== 'undefined' && endTime !== null)) {\n _context8.next = 16;\n break;\n }\n\n _context8.next = 14;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"call\"])(getMovePointMutations, range.endTime, endTime);\n\n case 14:\n endMutations = _context8.sent;\n endMutations.forEach(function (mutation) {\n return mutations.push(mutation);\n });\n\n case 16:\n _context8.next = 18;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"put\"])(Object(_actions_range__WEBPACK_IMPORTED_MODULE_9__[\"rangeMutations\"])(mutations));\n\n case 18:\n _context8.next = 20;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"put\"])(Object(_actions_viewState__WEBPACK_IMPORTED_MODULE_5__[\"editMetadata\"])(null));\n\n case 20:\n _context8.next = 22;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"call\"])(setIsSavedStatus);\n\n case 22:\n case \"end\":\n return _context8.stop();\n }\n }\n }, _marked8);\n}\n\nfunction movePointSaga(_ref3) {\n var _ref3$payload, originalX, newX, mutations;\n\n return _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.wrap(function movePointSaga$(_context9) {\n while (1) {\n switch (_context9.prev = _context9.next) {\n case 0:\n _ref3$payload = _ref3.payload, originalX = _ref3$payload.originalX, newX = _ref3$payload.x;\n _context9.next = 3;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"call\"])(getMovePointMutations, originalX, newX);\n\n case 3:\n mutations = _context9.sent;\n _context9.next = 6;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"put\"])(Object(_actions_range__WEBPACK_IMPORTED_MODULE_9__[\"rangeMutations\"])(mutations));\n\n case 6:\n case \"end\":\n return _context9.stop();\n }\n }\n }, _marked9);\n}\n\nfunction playWhenBubbleIsClicked(id, isSelected) {\n var state, ranges, selectedRangeIds, selectedRanges;\n return _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.wrap(function playWhenBubbleIsClicked$(_context10) {\n while (1) {\n switch (_context10.prev = _context10.next) {\n case 0:\n _context10.next = 2;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"select\"])();\n\n case 2:\n state = _context10.sent;\n _context10.next = 5;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"select\"])(_reducers_range__WEBPACK_IMPORTED_MODULE_6__[\"getRangeList\"]);\n\n case 5:\n ranges = _context10.sent;\n _context10.next = 8;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"select\"])(_reducers_range__WEBPACK_IMPORTED_MODULE_6__[\"getSelectedRanges\"]);\n\n case 8:\n selectedRangeIds = _context10.sent;\n _context10.next = 11;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"select\"])(Object(_reducers_range__WEBPACK_IMPORTED_MODULE_6__[\"getRangesByIds\"])(selectedRangeIds));\n\n case 11:\n selectedRanges = _context10.sent;\n\n if (!isSelected) {\n _context10.next = 21;\n break;\n }\n\n if (!(selectedRanges.length && selectedRanges[0] && selectedRanges[0].startTime === ranges[id].startTime)) {\n _context10.next = 19;\n break;\n }\n\n if (!state.project[_constants_project__WEBPACK_IMPORTED_MODULE_11__[\"PROJECT\"].START_PLAYING_WHEN_BUBBLES_CLICKED]) {\n _context10.next = 17;\n break;\n }\n\n _context10.next = 17;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"put\"])(Object(_actions_viewState__WEBPACK_IMPORTED_MODULE_5__[\"play\"])());\n\n case 17:\n _context10.next = 19;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"put\"])(Object(_actions_viewState__WEBPACK_IMPORTED_MODULE_5__[\"setCurrentTime\"])(ranges[id].startTime));\n\n case 19:\n _context10.next = 23;\n break;\n\n case 21:\n _context10.next = 23;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"put\"])(Object(_actions_viewState__WEBPACK_IMPORTED_MODULE_5__[\"pause\"])());\n\n case 23:\n case \"end\":\n return _context10.stop();\n }\n }\n }, _marked10);\n}\n\nfunction currentTimeSaga() {\n var _ref4,\n type,\n state,\n startPlayingAtEnd,\n stopPlayingAtEnd,\n selectedRangeIds,\n selectedRanges,\n startTime,\n endTime,\n time,\n lastTime,\n clicked,\n _ref5,\n currentTime,\n _args11 = arguments;\n\n return _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.wrap(function currentTimeSaga$(_context11) {\n while (1) {\n switch (_context11.prev = _context11.next) {\n case 0:\n _ref4 = _args11.length > 0 && _args11[0] !== undefined ? _args11[0] : {}, type = _ref4.type;\n _context11.next = 3;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"select\"])(function (s) {\n return s.project;\n });\n\n case 3:\n state = _context11.sent;\n startPlayingAtEnd = state.startPlayingAtEndOfSection;\n stopPlayingAtEnd = state.stopPlayingAtTheEndOfSection;\n _context11.next = 8;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"select\"])(_reducers_range__WEBPACK_IMPORTED_MODULE_6__[\"getSelectedRanges\"]);\n\n case 8:\n selectedRangeIds = _context11.sent;\n\n if (selectedRangeIds.length) {\n _context11.next = 11;\n break;\n }\n\n return _context11.abrupt(\"return\");\n\n case 11:\n _context11.next = 13;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"select\"])(Object(_reducers_range__WEBPACK_IMPORTED_MODULE_6__[\"getRangesByIds\"])(selectedRangeIds));\n\n case 13:\n selectedRanges = _context11.sent;\n startTime = selectedRanges[0].startTime;\n endTime = selectedRanges[selectedRanges.length - 1].endTime;\n _context11.next = 18;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"select\"])(getCurrentTime);\n\n case 18:\n time = _context11.sent;\n\n if (!(type === _constants_viewState__WEBPACK_IMPORTED_MODULE_7__[\"PLAY_AUDIO\"] && time <= startTime || time >= endTime)) {\n _context11.next = 21;\n break;\n }\n\n return _context11.abrupt(\"return\");\n\n case 21:\n // Last time stores previous tick time so that we can compare the gap.\n lastTime = 0; // Click stores if we have made a jump in this code, so we can skip it\n // in our checks when trying to identify user clicks.\n // @todo improvement if we can identify different sources of `SET_CURRENT_TIME` then\n // we can more easily know if the jumps in the time are from human clicks or automated.\n\n clicked = false; // We want to listen for every tick of the time.\n\n case 23:\n if (false) {}\n\n _context11.next = 26;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"take\"])(_constants_viewState__WEBPACK_IMPORTED_MODULE_7__[\"SET_CURRENT_TIME\"]);\n\n case 26:\n _ref5 = _context11.sent;\n currentTime = _ref5.payload.currentTime;\n\n if (!(clicked === false && lastTime && Math.abs(currentTime - lastTime) >= 1000)) {\n _context11.next = 30;\n break;\n }\n\n return _context11.abrupt(\"break\", 44);\n\n case 30:\n // Reset the clicked value if set.\n if (clicked) {\n clicked = false;\n } // Set a new last time, since we're done using it.\n\n\n lastTime = currentTime; // When the time goes past the end of a section\n\n if (!(currentTime >= endTime)) {\n _context11.next = 42;\n break;\n }\n\n if (!startPlayingAtEnd) {\n _context11.next = 39;\n break;\n }\n\n // Set a clicked value so we know to skip the user click check.\n clicked = true; // Either we start playing from the beginning of the section.\n\n _context11.next = 37;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"put\"])(Object(_actions_viewState__WEBPACK_IMPORTED_MODULE_5__[\"setCurrentTime\"])(startTime));\n\n case 37:\n _context11.next = 42;\n break;\n\n case 39:\n if (!stopPlayingAtEnd) {\n _context11.next = 42;\n break;\n }\n\n _context11.next = 42;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"put\"])(Object(_actions_viewState__WEBPACK_IMPORTED_MODULE_5__[\"pause\"])());\n\n case 42:\n _context11.next = 23;\n break;\n\n case 44:\n case \"end\":\n return _context11.stop();\n }\n }\n }, _marked11);\n}\n\nfunction deselectOtherRanges(id) {\n var selected;\n return _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.wrap(function deselectOtherRanges$(_context12) {\n while (1) {\n switch (_context12.prev = _context12.next) {\n case 0:\n _context12.next = 2;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"select\"])(_reducers_range__WEBPACK_IMPORTED_MODULE_6__[\"getSelectedRanges\"]);\n\n case 2:\n selected = _context12.sent;\n return _context12.delegateYield((selected || []).filter(function (range) {\n return range !== id;\n }).map(function (range) {\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"put\"])(Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_objectSpread__WEBPACK_IMPORTED_MODULE_0__[\"default\"])({}, Object(_actions_range__WEBPACK_IMPORTED_MODULE_9__[\"deselectRange\"])(range), {\n meta: {\n fromSaga: true\n }\n }));\n }), \"t0\", 4);\n\n case 4:\n case \"end\":\n return _context12.stop();\n }\n }\n }, _marked12);\n}\n\nfunction deselectAllRangesSaga(_ref6) {\n var payload, selectedRangeIds, selectedRanges, startTime, state;\n return _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.wrap(function deselectAllRangesSaga$(_context13) {\n while (1) {\n switch (_context13.prev = _context13.next) {\n case 0:\n payload = _ref6.payload;\n _context13.next = 3;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"select\"])(_reducers_range__WEBPACK_IMPORTED_MODULE_6__[\"getSelectedRanges\"]);\n\n case 3:\n selectedRangeIds = _context13.sent;\n\n if (selectedRangeIds.length) {\n _context13.next = 6;\n break;\n }\n\n return _context13.abrupt(\"return\");\n\n case 6:\n _context13.next = 8;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"select\"])(Object(_reducers_range__WEBPACK_IMPORTED_MODULE_6__[\"getRangesByIds\"])(selectedRangeIds));\n\n case 8:\n selectedRanges = _context13.sent;\n startTime = selectedRanges[0].startTime;\n _context13.next = 12;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"select\"])(function (s) {\n return s;\n });\n\n case 12:\n state = _context13.sent;\n\n if (!(payload.currentTime === startTime && !state.viewState.isPlaying)) {\n _context13.next = 15;\n break;\n }\n\n return _context13.abrupt(\"return\");\n\n case 15:\n _context13.next = 17;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"call\"])(deselectOtherRanges, null);\n\n case 17:\n case \"end\":\n return _context13.stop();\n }\n }\n }, _marked13);\n}\n\nfunction selectRangeSaga(_ref7) {\n var payload;\n return _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.wrap(function selectRangeSaga$(_context14) {\n while (1) {\n switch (_context14.prev = _context14.next) {\n case 0:\n payload = _ref7.payload;\n\n if (!payload.deselectOthers) {\n _context14.next = 4;\n break;\n }\n\n _context14.next = 4;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"call\"])(deselectOtherRanges, payload.id);\n\n case 4:\n _context14.next = 6;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"call\"])(playWhenBubbleIsClicked, payload.id, true);\n\n case 6:\n case \"end\":\n return _context14.stop();\n }\n }\n }, _marked14);\n}\n\nfunction deselectRangeSaga(_ref8) {\n var payload, meta;\n return _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.wrap(function deselectRangeSaga$(_context15) {\n while (1) {\n switch (_context15.prev = _context15.next) {\n case 0:\n payload = _ref8.payload, meta = _ref8.meta;\n\n if (!(meta && meta.fromSaga)) {\n _context15.next = 3;\n break;\n }\n\n return _context15.abrupt(\"return\");\n\n case 3:\n _context15.next = 5;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"call\"])(playWhenBubbleIsClicked, payload.id, false);\n\n case 5:\n case \"end\":\n return _context15.stop();\n }\n }\n }, _marked15);\n}\n\nfunction createRangeAction(data) {\n return _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.wrap(function createRangeAction$(_context16) {\n while (1) {\n switch (_context16.prev = _context16.next) {\n case 0:\n return _context16.abrupt(\"return\", Object(_actions_range__WEBPACK_IMPORTED_MODULE_9__[\"createRange\"])(data));\n\n case 1:\n case \"end\":\n return _context16.stop();\n }\n }\n }, _marked16);\n}\n/**\n * Resolve parents depths\n *\n * Given an initial target depth of a \"child\" of a set of parents, this will\n * change the parents depth if required, and then recursively to the same\n * to that bubble. It walks all the way up parents parents bumping the size\n * if the parent bubble can no longer fit the child bubble.\n *\n * Return value is an array of depth changes.\n *\n * This is a co-routine that needs the state so it is wrapped in this generator.\n * @param initialDepth\n * @param initialParents\n * @returns {IterableIterator<*>}\n */\n\nfunction resolveParentDepths(initialDepth, initialParents) {\n var state, depthChangeParents;\n return _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.wrap(function resolveParentDepths$(_context17) {\n while (1) {\n switch (_context17.prev = _context17.next) {\n case 0:\n _context17.next = 2;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"select\"])();\n\n case 2:\n state = _context17.sent;\n\n // Create a recursive function.\n depthChangeParents = function depthChangeParents(childDepth, parents) {\n var mutations = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : [];\n // Loop through the parents\n return parents.reduce(function (acc, range) {\n // If we \"bumped\" the child depth by one, would this particular parent still be\n // tall enough to contain this new height.\n if (childDepth + 1 === range.depth) {\n // If not, increase the depth of it\n acc.push(Object(_actions_range__WEBPACK_IMPORTED_MODULE_9__[\"increaseRangeDepth\"])(range.id)); // Now get its direct parents.\n\n var parentsParents = getParentBubbles(range)(state); // And do the same process, using this ranges depth.\n\n return depthChangeParents(range.depth, parentsParents, acc);\n } // Accumulate all of the mutations.\n\n\n return acc;\n }, mutations);\n };\n\n return _context17.abrupt(\"return\", depthChangeParents(initialDepth, initialParents));\n\n case 5:\n case \"end\":\n return _context17.stop();\n }\n }\n }, _marked17);\n}\n\nfunction groupRangeSaga() {\n var selectedRangeIds, selectedRanges, _extractTimesFromRang, startTime, endTime, rangeBubbles, parentBubbles, depth, depthMutations, deselectMutations, newRange;\n\n return _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.wrap(function groupRangeSaga$(_context18) {\n while (1) {\n switch (_context18.prev = _context18.next) {\n case 0:\n _context18.next = 2;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"select\"])(_reducers_range__WEBPACK_IMPORTED_MODULE_6__[\"getSelectedRanges\"]);\n\n case 2:\n selectedRangeIds = _context18.sent;\n\n if (!(selectedRangeIds.length < 2)) {\n _context18.next = 5;\n break;\n }\n\n return _context18.abrupt(\"return\");\n\n case 5:\n _context18.next = 7;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"select\"])(Object(_reducers_range__WEBPACK_IMPORTED_MODULE_6__[\"getRangesByIds\"])(selectedRangeIds));\n\n case 7:\n selectedRanges = _context18.sent;\n _extractTimesFromRang = extractTimesFromRangeList(selectedRanges), startTime = _extractTimesFromRang.startTime, endTime = _extractTimesFromRang.endTime;\n _context18.t0 = canMerge;\n _context18.next = 12;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"select\"])(_reducers_range__WEBPACK_IMPORTED_MODULE_6__[\"getRangeList\"]);\n\n case 12:\n _context18.t1 = _context18.sent;\n _context18.t2 = {\n startTime: startTime,\n endTime: endTime\n };\n\n if ((0, _context18.t0)(_context18.t1, _context18.t2)) {\n _context18.next = 17;\n break;\n }\n\n console.log(\"We can't merge these bubbles.\");\n return _context18.abrupt(\"return\");\n\n case 17:\n _context18.next = 19;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"select\"])(Object(_reducers_range__WEBPACK_IMPORTED_MODULE_6__[\"getRangesBetweenTimes\"])(startTime, endTime));\n\n case 19:\n rangeBubbles = _context18.sent;\n _context18.next = 22;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"select\"])(getParentBubbles({\n startTime: startTime,\n endTime: endTime\n }));\n\n case 22:\n parentBubbles = _context18.sent;\n // Calculate the tallest bubble in the selection\n depth = Math.max.apply(Math, Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_toConsumableArray__WEBPACK_IMPORTED_MODULE_2__[\"default\"])(rangeBubbles.map(function (range) {\n return range.depth;\n }))) || 1; // Calculate any changes to parent bubble depths.\n\n _context18.next = 26;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"call\"])(resolveParentDepths, depth, parentBubbles);\n\n case 26:\n depthMutations = _context18.sent;\n // Deselect currently selected.\n deselectMutations = selectedRangeIds.map(_actions_range__WEBPACK_IMPORTED_MODULE_9__[\"deselectRange\"]); // Create the new range.\n\n _context18.next = 30;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"call\"])(createRangeAction, {\n startTime: startTime,\n endTime: endTime,\n depth: depth + 1\n });\n\n case 30:\n newRange = _context18.sent;\n _context18.next = 33;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"put\"])(Object(_actions_range__WEBPACK_IMPORTED_MODULE_9__[\"rangeMutations\"])([].concat(Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_toConsumableArray__WEBPACK_IMPORTED_MODULE_2__[\"default\"])(deselectMutations), Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_toConsumableArray__WEBPACK_IMPORTED_MODULE_2__[\"default\"])(depthMutations), [newRange, Object(_actions_range__WEBPACK_IMPORTED_MODULE_9__[\"selectRange\"])(newRange.payload.id)])));\n\n case 33:\n case \"end\":\n return _context18.stop();\n }\n }\n }, _marked18);\n}\n\nfunction splitRangeSaga(_ref9) {\n var time, ranges, itemToSplit;\n return _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.wrap(function splitRangeSaga$(_context19) {\n while (1) {\n switch (_context19.prev = _context19.next) {\n case 0:\n time = _ref9.payload.time;\n _context19.next = 3;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"select\"])(_reducers_range__WEBPACK_IMPORTED_MODULE_6__[\"getRangeList\"]);\n\n case 3:\n ranges = _context19.sent;\n itemToSplit = Object.values(ranges).filter(function (bubble) {\n return time <= bubble.endTime && time >= bubble.startTime;\n }).sort(function (b1, b2) {\n return b1.depth - b2.depth;\n })[0];\n Object(_utils_invariant__WEBPACK_IMPORTED_MODULE_12__[\"default\"])(function () {\n return itemToSplit && itemToSplit.id && itemToSplit.endTime;\n }, 'Unstable state: There should be a bubble at every point'); // If we wanted to implement split left, so that a new bubble would be created on the left side\n // yield put(updateRangeTime(itemToSplit.id, { startTime }));\n // yield put(createRange({ startTime, endTime: time }));\n\n _context19.next = 8;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"put\"])(Object(_actions_range__WEBPACK_IMPORTED_MODULE_9__[\"rangeMutations\"])([Object(_actions_range__WEBPACK_IMPORTED_MODULE_9__[\"createRange\"])({\n startTime: time,\n endTime: itemToSplit.endTime\n }), Object(_actions_range__WEBPACK_IMPORTED_MODULE_9__[\"updateRangeTime\"])(itemToSplit.id, {\n endTime: time\n })]));\n\n case 8:\n _context19.next = 10;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"call\"])(setIsSavedStatus);\n\n case 10:\n case \"end\":\n return _context19.stop();\n }\n }\n }, _marked19);\n}\n\nfunction deleteRangeRequest(toRemoveId) {\n var toRemoveIds, redundantSizes, toRemove, rangeList, rangeArray, remainingRanges, depthMutations, newDepthMap, _iteratorNormalCompletion, _didIteratorError, _iteratorError, _loop, _iterator, _step, _ret, sizeMutations, _iteratorNormalCompletion2, _didIteratorError2, _iteratorError2, _loop2, _iterator2, _step2, _ret2;\n\n return _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.wrap(function deleteRangeRequest$(_context22) {\n while (1) {\n switch (_context22.prev = _context22.next) {\n case 0:\n toRemoveIds = [toRemoveId]; // Removing redundant sizes first\n\n _context22.next = 3;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"call\"])(calculateRedundantSizes, toRemoveIds);\n\n case 3:\n redundantSizes = _context22.sent;\n // Map these Ids to a delete range action.\n toRemove = redundantSizes.map(_actions_range__WEBPACK_IMPORTED_MODULE_9__[\"deleteRange\"]); // Some useful variables.\n\n _context22.next = 7;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"select\"])(_reducers_range__WEBPACK_IMPORTED_MODULE_6__[\"getRangeList\"]);\n\n case 7:\n rangeList = _context22.sent;\n rangeArray = Object.values(rangeList); // Create new list of bubbles\n // Run through a validation step to ensure they are of the correct level.\n\n remainingRanges = rangeArray.filter(function (range) {\n return redundantSizes.indexOf(range.id) === -1 && range.depth > 1;\n }).sort(function (a, b) {\n return a.depth === b.depth ? 0 : a.depth < b.depth ? -1 : 1;\n });\n depthMutations = [];\n newDepthMap = {}; // Loop through the remaining ranges to calculate depth changes.\n\n _iteratorNormalCompletion = true;\n _didIteratorError = false;\n _iteratorError = undefined;\n _context22.prev = 15;\n _loop =\n /*#__PURE__*/\n _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.mark(function _loop() {\n var parent, rangeChildren, maxDepthOfChildren;\n return _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.wrap(function _loop$(_context20) {\n while (1) {\n switch (_context20.prev = _context20.next) {\n case 0:\n parent = _step.value;\n _context20.next = 3;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"select\"])(Object(_reducers_range__WEBPACK_IMPORTED_MODULE_6__[\"getRangesBetweenTimes\"])(parent.startTime, parent.endTime));\n\n case 3:\n _context20.t0 = function (range) {\n return range.id !== parent.id && redundantSizes.indexOf(range.id) === -1;\n };\n\n rangeChildren = _context20.sent.filter(_context20.t0);\n\n if (rangeChildren.length) {\n _context20.next = 7;\n break;\n }\n\n return _context20.abrupt(\"return\", \"continue\");\n\n case 7:\n // Calculate the maximum depth, taking into account any changes made in this loop.\n maxDepthOfChildren = Math.max.apply(Math, Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_toConsumableArray__WEBPACK_IMPORTED_MODULE_2__[\"default\"])(rangeChildren.map(function (range) {\n return newDepthMap[range.id] || range.depth;\n }))); // Continue if there are no changes.\n\n if (!(maxDepthOfChildren + 1 === parent.depth)) {\n _context20.next = 10;\n break;\n }\n\n return _context20.abrupt(\"return\", \"continue\");\n\n case 10:\n // This indicates we have an incorrect depth,\n if (maxDepthOfChildren + 1 < parent.depth) {\n // Decrease the range if it is too much higher than children.\n newDepthMap[parent.id] = parent.depth - 1;\n depthMutations.push(Object(_actions_range__WEBPACK_IMPORTED_MODULE_9__[\"decreaseRangeDepth\"])(parent.id));\n } else {\n // Increase the range if it can't fit the children anymore.\n newDepthMap[parent.id] = parent.depth + 1;\n depthMutations.push(Object(_actions_range__WEBPACK_IMPORTED_MODULE_9__[\"increaseRangeDepth\"])(parent.id));\n }\n\n case 11:\n case \"end\":\n return _context20.stop();\n }\n }\n }, _loop);\n });\n _iterator = remainingRanges[Symbol.iterator]();\n\n case 18:\n if (_iteratorNormalCompletion = (_step = _iterator.next()).done) {\n _context22.next = 26;\n break;\n }\n\n return _context22.delegateYield(_loop(), \"t0\", 20);\n\n case 20:\n _ret = _context22.t0;\n\n if (!(_ret === \"continue\")) {\n _context22.next = 23;\n break;\n }\n\n return _context22.abrupt(\"continue\", 23);\n\n case 23:\n _iteratorNormalCompletion = true;\n _context22.next = 18;\n break;\n\n case 26:\n _context22.next = 32;\n break;\n\n case 28:\n _context22.prev = 28;\n _context22.t1 = _context22[\"catch\"](15);\n _didIteratorError = true;\n _iteratorError = _context22.t1;\n\n case 32:\n _context22.prev = 32;\n _context22.prev = 33;\n\n if (!_iteratorNormalCompletion && _iterator.return != null) {\n _iterator.return();\n }\n\n case 35:\n _context22.prev = 35;\n\n if (!_didIteratorError) {\n _context22.next = 38;\n break;\n }\n\n throw _iteratorError;\n\n case 38:\n return _context22.finish(35);\n\n case 39:\n return _context22.finish(32);\n\n case 40:\n // Make remaining ranges grow right (or left if that's not possible)\n sizeMutations = []; // Loop through all of the deleted ranges.\n\n _iteratorNormalCompletion2 = true;\n _didIteratorError2 = false;\n _iteratorError2 = undefined;\n _context22.prev = 44;\n _loop2 =\n /*#__PURE__*/\n _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.mark(function _loop2() {\n var deletedId, rangeToRemove, rangeChildren, parentRange, siblingRanges, lookLeftSibling, lookRightSibling, leftBubbles, _iteratorNormalCompletion3, _didIteratorError3, _iteratorError3, _iterator3, _step3, leftBubble, rightBubbles, _iteratorNormalCompletion4, _didIteratorError4, _iteratorError4, _iterator4, _step4, rightBubble;\n\n return _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.wrap(function _loop2$(_context21) {\n while (1) {\n switch (_context21.prev = _context21.next) {\n case 0:\n deletedId = _step2.value;\n // Get the full range object.\n rangeToRemove = rangeList[deletedId]; // Need to check if this range has children first, children grow inside so\n // no sibling bubbles will change size in this case.\n\n _context21.next = 4;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"select\"])(Object(_reducers_range__WEBPACK_IMPORTED_MODULE_6__[\"getRangesBetweenTimes\"])(rangeToRemove.startTime, rangeToRemove.endTime));\n\n case 4:\n _context21.t0 = function (range) {\n return range.id !== rangeToRemove.id;\n };\n\n rangeChildren = _context21.sent.filter(_context21.t0);\n\n if (!rangeChildren.length) {\n _context21.next = 8;\n break;\n }\n\n return _context21.abrupt(\"return\", \"continue\");\n\n case 8:\n _context21.next = 10;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"call\"])(getDirectParentRange, rangeToRemove);\n\n case 10:\n parentRange = _context21.sent;\n\n if (!parentRange) {\n _context21.next = 23;\n break;\n }\n\n _context21.next = 14;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"call\"])(getSiblingRanges, parentRange, rangeToRemove);\n\n case 14:\n siblingRanges = _context21.sent;\n lookLeftSibling = getRangeToTheLeft(siblingRanges, rangeToRemove);\n\n if (!lookLeftSibling) {\n _context21.next = 19;\n break;\n }\n\n sizeMutations.push(Object(_actions_range__WEBPACK_IMPORTED_MODULE_9__[\"updateRangeTime\"])(lookLeftSibling.id, {\n endTime: rangeToRemove.endTime\n }));\n return _context21.abrupt(\"return\", \"continue\");\n\n case 19:\n lookRightSibling = getRangeToTheRight(siblingRanges, rangeToRemove);\n\n if (!lookRightSibling) {\n _context21.next = 23;\n break;\n }\n\n sizeMutations.push(Object(_actions_range__WEBPACK_IMPORTED_MODULE_9__[\"updateRangeTime\"])(lookRightSibling.id, {\n startTime: rangeToRemove.startTime\n }));\n return _context21.abrupt(\"return\", \"continue\");\n\n case 23:\n // At this point we know that bubbles inside the parent won't shift to fill the space.\n // First look left to find all the bubbles that share the startTime.\n leftBubbles = rangeArray.filter(function (range) {\n return range.id !== rangeToRemove.id && (range.endTime === rangeToRemove.startTime || range.startTime === rangeToRemove.startTime);\n }); // And queue them up to be moved.\n\n if (!(canMoveBubbles(leftBubbles, rangeToRemove) && leftBubbles.length)) {\n _context21.next = 45;\n break;\n }\n\n _iteratorNormalCompletion3 = true;\n _didIteratorError3 = false;\n _iteratorError3 = undefined;\n _context21.prev = 28;\n\n for (_iterator3 = leftBubbles[Symbol.iterator](); !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) {\n leftBubble = _step3.value;\n // Overlap bubble growing from the left.\n sizeMutations.push(overlapBubbleLeft(rangeToRemove, leftBubble));\n }\n\n _context21.next = 36;\n break;\n\n case 32:\n _context21.prev = 32;\n _context21.t1 = _context21[\"catch\"](28);\n _didIteratorError3 = true;\n _iteratorError3 = _context21.t1;\n\n case 36:\n _context21.prev = 36;\n _context21.prev = 37;\n\n if (!_iteratorNormalCompletion3 && _iterator3.return != null) {\n _iterator3.return();\n }\n\n case 39:\n _context21.prev = 39;\n\n if (!_didIteratorError3) {\n _context21.next = 42;\n break;\n }\n\n throw _iteratorError3;\n\n case 42:\n return _context21.finish(39);\n\n case 43:\n return _context21.finish(36);\n\n case 44:\n return _context21.abrupt(\"return\", \"continue\");\n\n case 45:\n // Now we look to the right of the bubble to find bubbles that share\n // the endTime. This is only applicable when there are no bubbles\n // to the left (i.e. a bubble with startTime = 0)\n rightBubbles = rangeArray.filter(function (range) {\n return range.id !== rangeToRemove.id && (range.startTime === rangeToRemove.endTime || range.endTime === rangeToRemove.startTime);\n }); // And queue these up to be moved.\n\n if (!(canMoveBubbles(rightBubbles, rangeToRemove) && rightBubbles.length)) {\n _context21.next = 66;\n break;\n }\n\n _iteratorNormalCompletion4 = true;\n _didIteratorError4 = false;\n _iteratorError4 = undefined;\n _context21.prev = 50;\n\n for (_iterator4 = rightBubbles[Symbol.iterator](); !(_iteratorNormalCompletion4 = (_step4 = _iterator4.next()).done); _iteratorNormalCompletion4 = true) {\n rightBubble = _step4.value;\n // Overlap bubble growing from the right.\n sizeMutations.push(overlapBubbleRight(rangeToRemove, rightBubble));\n } // continue;\n\n\n _context21.next = 58;\n break;\n\n case 54:\n _context21.prev = 54;\n _context21.t2 = _context21[\"catch\"](50);\n _didIteratorError4 = true;\n _iteratorError4 = _context21.t2;\n\n case 58:\n _context21.prev = 58;\n _context21.prev = 59;\n\n if (!_iteratorNormalCompletion4 && _iterator4.return != null) {\n _iterator4.return();\n }\n\n case 61:\n _context21.prev = 61;\n\n if (!_didIteratorError4) {\n _context21.next = 64;\n break;\n }\n\n throw _iteratorError4;\n\n case 64:\n return _context21.finish(61);\n\n case 65:\n return _context21.finish(58);\n\n case 66:\n case \"end\":\n return _context21.stop();\n }\n }\n }, _loop2, null, [[28, 32, 36, 44], [37,, 39, 43], [50, 54, 58, 66], [59,, 61, 65]]);\n });\n _iterator2 = redundantSizes[Symbol.iterator]();\n\n case 47:\n if (_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done) {\n _context22.next = 55;\n break;\n }\n\n return _context22.delegateYield(_loop2(), \"t2\", 49);\n\n case 49:\n _ret2 = _context22.t2;\n\n if (!(_ret2 === \"continue\")) {\n _context22.next = 52;\n break;\n }\n\n return _context22.abrupt(\"continue\", 52);\n\n case 52:\n _iteratorNormalCompletion2 = true;\n _context22.next = 47;\n break;\n\n case 55:\n _context22.next = 61;\n break;\n\n case 57:\n _context22.prev = 57;\n _context22.t3 = _context22[\"catch\"](44);\n _didIteratorError2 = true;\n _iteratorError2 = _context22.t3;\n\n case 61:\n _context22.prev = 61;\n _context22.prev = 62;\n\n if (!_iteratorNormalCompletion2 && _iterator2.return != null) {\n _iterator2.return();\n }\n\n case 64:\n _context22.prev = 64;\n\n if (!_didIteratorError2) {\n _context22.next = 67;\n break;\n }\n\n throw _iteratorError2;\n\n case 67:\n return _context22.finish(64);\n\n case 68:\n return _context22.finish(61);\n\n case 69:\n _context22.next = 71;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"put\"])(Object(_actions_range__WEBPACK_IMPORTED_MODULE_9__[\"rangeMutations\"])([].concat(Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_toConsumableArray__WEBPACK_IMPORTED_MODULE_2__[\"default\"])(toRemove), depthMutations, sizeMutations)));\n\n case 71:\n case \"end\":\n return _context22.stop();\n }\n }\n }, _marked20, null, [[15, 28, 32, 40], [33,, 35, 39], [44, 57, 61, 69], [62,, 64, 68]]);\n}\n\nfunction singleDelete(_ref10) {\n var id;\n return _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.wrap(function singleDelete$(_context23) {\n while (1) {\n switch (_context23.prev = _context23.next) {\n case 0:\n id = _ref10.payload.id;\n _context23.next = 3;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"call\"])(deleteRangeRequest, id);\n\n case 3:\n _context23.next = 5;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"call\"])(setIsSavedStatus);\n\n case 5:\n case \"end\":\n return _context23.stop();\n }\n }\n }, _marked21);\n}\n\nfunction multiDelete(_ref11) {\n var ranges, confirmed, _iteratorNormalCompletion5, _didIteratorError5, _iteratorError5, _iterator5, _step5, range;\n\n return _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.wrap(function multiDelete$(_context24) {\n while (1) {\n switch (_context24.prev = _context24.next) {\n case 0:\n ranges = _ref11.payload.ranges;\n\n if (!(ranges.length > 1)) {\n _context24.next = 7;\n break;\n }\n\n _context24.next = 4;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"call\"])(_index__WEBPACK_IMPORTED_MODULE_13__[\"showConfirmation\"], 'Multiple sections will be deleted. Redundant sections will be removed. Do you wish to continue?');\n\n case 4:\n confirmed = _context24.sent;\n\n if (!(confirmed === false)) {\n _context24.next = 7;\n break;\n }\n\n return _context24.abrupt(\"return\");\n\n case 7:\n _iteratorNormalCompletion5 = true;\n _didIteratorError5 = false;\n _iteratorError5 = undefined;\n _context24.prev = 10;\n _iterator5 = ranges[Symbol.iterator]();\n\n case 12:\n if (_iteratorNormalCompletion5 = (_step5 = _iterator5.next()).done) {\n _context24.next = 19;\n break;\n }\n\n range = _step5.value;\n _context24.next = 16;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"call\"])(deleteRangeRequest, range);\n\n case 16:\n _iteratorNormalCompletion5 = true;\n _context24.next = 12;\n break;\n\n case 19:\n _context24.next = 25;\n break;\n\n case 21:\n _context24.prev = 21;\n _context24.t0 = _context24[\"catch\"](10);\n _didIteratorError5 = true;\n _iteratorError5 = _context24.t0;\n\n case 25:\n _context24.prev = 25;\n _context24.prev = 26;\n\n if (!_iteratorNormalCompletion5 && _iterator5.return != null) {\n _iterator5.return();\n }\n\n case 28:\n _context24.prev = 28;\n\n if (!_didIteratorError5) {\n _context24.next = 31;\n break;\n }\n\n throw _iteratorError5;\n\n case 31:\n return _context24.finish(28);\n\n case 32:\n return _context24.finish(25);\n\n case 33:\n _context24.next = 35;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"call\"])(setIsSavedStatus);\n\n case 35:\n case \"end\":\n return _context24.stop();\n }\n }\n }, _marked22, null, [[10, 21, 25, 33], [26,, 28, 32]]);\n}\n\nfunction clearCustomColorsSaga() {\n var rangeList, rangeIds, _i, rangeId;\n\n return _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.wrap(function clearCustomColorsSaga$(_context25) {\n while (1) {\n switch (_context25.prev = _context25.next) {\n case 0:\n _context25.next = 2;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"select\"])(_reducers_range__WEBPACK_IMPORTED_MODULE_6__[\"getRangeList\"]);\n\n case 2:\n rangeList = _context25.sent;\n rangeIds = Object.keys(rangeList);\n _i = 0;\n\n case 5:\n if (!(_i < rangeIds.length)) {\n _context25.next = 12;\n break;\n }\n\n rangeId = rangeIds[_i];\n _context25.next = 9;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"put\"])(Object(_actions_range__WEBPACK_IMPORTED_MODULE_9__[\"unsetRangeColor\"])(rangeId));\n\n case 9:\n _i++;\n _context25.next = 5;\n break;\n\n case 12:\n _context25.next = 14;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"call\"])(setIsSavedStatus);\n\n case 14:\n case \"end\":\n return _context25.stop();\n }\n }\n }, _marked23);\n}\n\nfunction rangeSaga() {\n return _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_regenerator__WEBPACK_IMPORTED_MODULE_3___default.a.wrap(function rangeSaga$(_context26) {\n while (1) {\n switch (_context26.prev = _context26.next) {\n case 0:\n _context26.next = 2;\n return Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"all\"])([Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"takeEvery\"])(_constants_viewState__WEBPACK_IMPORTED_MODULE_7__[\"PREVIOUS_BUBBLE\"], previousBubble), Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"takeEvery\"])(_constants_viewState__WEBPACK_IMPORTED_MODULE_7__[\"NEXT_BUBBLE\"], nextBubble), Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"takeEvery\"])(_constants_range__WEBPACK_IMPORTED_MODULE_8__[\"MOVE_POINT\"], movePointSaga), Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"takeEvery\"])(_constants_range__WEBPACK_IMPORTED_MODULE_8__[\"SCHEDULE_UPDATE_RANGE\"], saveRangeSaga), Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"takeEvery\"])(_constants_range__WEBPACK_IMPORTED_MODULE_8__[\"SELECT_RANGE\"], selectRangeSaga), Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"takeLatest\"])([_constants_range__WEBPACK_IMPORTED_MODULE_8__[\"SELECT_RANGE\"], _constants_range__WEBPACK_IMPORTED_MODULE_8__[\"DESELECT_RANGE\"]], currentTimeSaga), Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"takeEvery\"])(_constants_range__WEBPACK_IMPORTED_MODULE_8__[\"DESELECT_RANGE\"], deselectRangeSaga), Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"takeEvery\"])(_constants_range__WEBPACK_IMPORTED_MODULE_8__[\"GROUP_RANGES\"], groupRangeSaga), Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"takeEvery\"])(_constants_range__WEBPACK_IMPORTED_MODULE_8__[\"SPLIT_RANGE_AT\"], splitRangeSaga), Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"takeEvery\"])(_constants_range__WEBPACK_IMPORTED_MODULE_8__[\"SCHEDULE_DELETE_RANGE\"], singleDelete), Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"takeEvery\"])(_constants_range__WEBPACK_IMPORTED_MODULE_8__[\"SCHEDULE_DELETE_RANGES\"], multiDelete), Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"takeEvery\"])(_constants_viewState__WEBPACK_IMPORTED_MODULE_7__[\"SET_CURRENT_TIME\"], deselectAllRangesSaga), Object(redux_saga_effects__WEBPACK_IMPORTED_MODULE_4__[\"takeEvery\"])(_constants_project__WEBPACK_IMPORTED_MODULE_11__[\"CLEAR_CUSTOM_COLORS\"], clearCustomColorsSaga)]);\n\n case 2:\n case \"end\":\n return _context26.stop();\n }\n }\n }, _marked24);\n}\n\n//# sourceURL=webpack:///./src/sagas/range-saga.js?"); /***/ }), @@ -11227,7 +11227,7 @@ import "../iiif-timeliner-styles.css" /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; - eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"parseMarkers\", function() { return parseMarkers; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"loadProjectState\", function() { return loadProjectState; });\n/* harmony import */ var _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_toConsumableArray__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./node_modules/babel-preset-react-app/node_modules/@babel/runtime/helpers/esm/toConsumableArray */ \"./node_modules/babel-preset-react-app/node_modules/@babel/runtime/helpers/esm/toConsumableArray.js\");\n/* harmony import */ var _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_objectSpread__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./node_modules/babel-preset-react-app/node_modules/@babel/runtime/helpers/esm/objectSpread */ \"./node_modules/babel-preset-react-app/node_modules/@babel/runtime/helpers/esm/objectSpread.js\");\n/* harmony import */ var _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./node_modules/babel-preset-react-app/node_modules/@babel/runtime/helpers/esm/defineProperty */ \"./node_modules/babel-preset-react-app/node_modules/@babel/runtime/helpers/esm/defineProperty.js\");\n/* harmony import */ var _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_slicedToArray__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./node_modules/babel-preset-react-app/node_modules/@babel/runtime/helpers/esm/slicedToArray */ \"./node_modules/babel-preset-react-app/node_modules/@babel/runtime/helpers/esm/slicedToArray.js\");\n/* harmony import */ var _constants_project__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ../constants/project */ \"./src/constants/project.js\");\n/* harmony import */ var _constants_canvas__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ../constants/canvas */ \"./src/constants/canvas.js\");\n/* harmony import */ var _constants_range__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ../constants/range */ \"./src/constants/range.js\");\n/* harmony import */ var _constants_viewState__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ../constants/viewState */ \"./src/constants/viewState.js\");\n/* harmony import */ var _containers_AuthResource_AuthResource__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(/*! ../containers/AuthResource/AuthResource */ \"./src/containers/AuthResource/AuthResource.js\");\n/* harmony import */ var _generateId__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(/*! ./generateId */ \"./src/utils/generateId.js\");\n/* harmony import */ var query_string__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(/*! query-string */ \"./node_modules/query-string/index.js\");\n/* harmony import */ var query_string__WEBPACK_IMPORTED_MODULE_10___default = /*#__PURE__*/__webpack_require__.n(query_string__WEBPACK_IMPORTED_MODULE_10__);\n\n\n\n\n\n\n\n\n\n\n // Constants\n\nvar BACKGROUND_COLOUR_PROPERTY = 'tl:backgroundColour';\nvar TEXT_COLOUR_PROPERTY = 'tl:textColour'; // Helpers\n\n/**\n * seconds to microseconds\n * @param {String|Number} s seconds to convert\n * @return {Number} milliseconds\n */\n\nvar sToMs = function sToMs(s) {\n return parseFloat(s) * 1000;\n};\n/**\n * returns localised property\n * @param {Object} resource\n * @param {String} locale default en\n */\n\n\nvar getLocalisedResource = function getLocalisedResource(resource) {\n var locale = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'en';\n\n if (!resource) {\n return '';\n } else {\n return (resource[locale] || resource['@none'] || ['']).join('');\n }\n};\n/**\n * get custom bubble colour\n * @param {Object} range the range may has the background property\n */\n\n\nvar getColour = function getColour(range) {\n return range ? range[_constants_range__WEBPACK_IMPORTED_MODULE_6__[\"RANGE\"].COLOUR] || '' : '';\n};\n/**\n * converts url hash parameters to an object\n * @param {String} url input url\n * @returns {Object} the key-value pairs converted to object property-values.\n */\n\n\nvar hashParamsToObj = function hashParamsToObj(url) {\n return (url.split('#')[1] || '').split('&').reduce(function (params, item) {\n var _item$split = item.split('='),\n _item$split2 = Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_slicedToArray__WEBPACK_IMPORTED_MODULE_3__[\"default\"])(_item$split, 2),\n k = _item$split2[0],\n v = _item$split2[1];\n\n if (v !== undefined) {\n params[decodeURIComponent(k)] = decodeURIComponent(v);\n }\n\n return params;\n }, {});\n};\n/**\n * convert a comma separated time range to ms\n * @param {String} rangeStr\n */\n\n\nvar parseTimeRange = function parseTimeRange(rangeStr) {\n var _ref;\n\n var _rangeStr$split = rangeStr.split(','),\n _rangeStr$split2 = Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_slicedToArray__WEBPACK_IMPORTED_MODULE_3__[\"default\"])(_rangeStr$split, 2),\n startString = _rangeStr$split2[0],\n endString = _rangeStr$split2[1];\n\n return _ref = {}, Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_2__[\"default\"])(_ref, _constants_range__WEBPACK_IMPORTED_MODULE_6__[\"RANGE\"].START_TIME, sToMs(startString)), Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_2__[\"default\"])(_ref, _constants_range__WEBPACK_IMPORTED_MODULE_6__[\"RANGE\"].END_TIME, endString ? sToMs(endString) : sToMs(startString) + 1), _ref;\n};\n\nvar getLabel = function getLabel(t, defaultValue) {\n var value = Object.values(t)[0];\n\n if (value && value.length) {\n return value[0] || defaultValue;\n }\n\n return defaultValue;\n};\n\nvar parseMarkers = function parseMarkers(manifest) {\n var annotationPages = manifest.annotations;\n var annotations = annotationPages && annotationPages[0] && annotationPages[0].items ? annotationPages[0].items : null;\n\n if (!annotations) {\n return [];\n }\n\n return annotations.map(function (annotation) {\n return {\n id: annotation.id,\n time: annotation.target && annotation.target.selector && annotation.target.selector.t ? annotation.target.selector.t * 1000 : null,\n label: getLabel(annotation.label, 'Untitled marker'),\n summary: annotation.body && annotation.body.value ? annotation.body.value : annotation.body[0] && annotation.body[0].value ? annotation.body[0].value : ''\n };\n }).filter(function (annotation) {\n return annotation.time;\n });\n};\n/**\n * @param {Object} canvas - IIIFCanvas javascript object\n * @returns array all audio annotations url, start and end time\n */\n\nvar getAudioAnnotations = function getAudioAnnotations(canvas) {\n if (!canvas) {\n return [];\n }\n\n var annotations = canvas.items ? canvas.items.reduce(function (acc, annotationPage) {\n if (annotationPage.items) {\n return acc.concat(annotationPage.items);\n } else {\n return acc;\n }\n }, []) : [];\n return annotations.filter(function (annotation) {\n return annotation.motivation === 'painting' && annotation.body && (annotation.body.type === 'Audio' || annotation.body.type === 'Sound' || annotation.body.type === 'Video') || annotation.body && annotation.body.type === 'Choice';\n }).map(function (annotation) {\n var hashParams = hashParamsToObj(annotation.target);\n var audioDescriptor = {};\n\n if (hashParams.t) {\n Object.assign(audioDescriptor, parseTimeRange(hashParams.t));\n }\n\n var body = Object(_containers_AuthResource_AuthResource__WEBPACK_IMPORTED_MODULE_8__[\"resolveAvResource\"])(annotation);\n audioDescriptor.url = body.id || body['@id'];\n\n if (body && body.service) {\n audioDescriptor.service = body.service;\n }\n\n return audioDescriptor;\n });\n};\n/**\n * Simplified version of the canvas processor:\n * - does not deal with multiple audio annotation on a canvas;\n * - does not handle choices with for different audio file formats;\n * - does not load av service documents;\n * @param {Object} canvas -\n */\n\n\nvar processCanvas = function processCanvas(canvas) {\n var _ref2;\n\n var audioAnnotations = getAudioAnnotations(canvas);\n return audioAnnotations.length > 0 ? (_ref2 = {}, Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_2__[\"default\"])(_ref2, _constants_canvas__WEBPACK_IMPORTED_MODULE_5__[\"CANVAS\"].URL, audioAnnotations[0].url), Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_2__[\"default\"])(_ref2, \"service\", audioAnnotations[0].service), _ref2) : Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_2__[\"default\"])({}, _constants_canvas__WEBPACK_IMPORTED_MODULE_5__[\"CANVAS\"].ERROR, {\n code: 6,\n description: 'Manifest does not contain audio annotations'\n });\n};\n\nvar extendRange = function extendRange(parentRange, child) {\n parentRange[_constants_range__WEBPACK_IMPORTED_MODULE_6__[\"RANGE\"].START_TIME] = Math.min(parentRange[_constants_range__WEBPACK_IMPORTED_MODULE_6__[\"RANGE\"].START_TIME], child[_constants_range__WEBPACK_IMPORTED_MODULE_6__[\"RANGE\"].START_TIME]);\n parentRange[_constants_range__WEBPACK_IMPORTED_MODULE_6__[\"RANGE\"].END_TIME] = Math.max(parentRange[_constants_range__WEBPACK_IMPORTED_MODULE_6__[\"RANGE\"].END_TIME], child[_constants_range__WEBPACK_IMPORTED_MODULE_6__[\"RANGE\"].END_TIME]);\n parentRange[_constants_range__WEBPACK_IMPORTED_MODULE_6__[\"RANGE\"].DEPTH] = Math.max(child[_constants_range__WEBPACK_IMPORTED_MODULE_6__[\"RANGE\"].DEPTH] + 1, parentRange[_constants_range__WEBPACK_IMPORTED_MODULE_6__[\"RANGE\"].DEPTH] || 1);\n};\n\nvar processLevel = function processLevel(structure) {\n if (structure.type === 'Canvas') {\n var hashParams = hashParamsToObj(structure.id);\n return Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_objectSpread__WEBPACK_IMPORTED_MODULE_1__[\"default\"])({}, parseTimeRange(hashParams.t), Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_2__[\"default\"])({}, _constants_range__WEBPACK_IMPORTED_MODULE_6__[\"RANGE\"].DEPTH, 0));\n } else if (structure.type === 'Range') {\n var _range;\n\n var range = (_range = {\n id: structure.id\n }, Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_2__[\"default\"])(_range, _constants_range__WEBPACK_IMPORTED_MODULE_6__[\"RANGE\"].LABEL, getLocalisedResource(structure.label) || ''), Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_2__[\"default\"])(_range, _constants_range__WEBPACK_IMPORTED_MODULE_6__[\"RANGE\"].SUMMARY, getLocalisedResource(structure.summary) || ''), Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_2__[\"default\"])(_range, _constants_range__WEBPACK_IMPORTED_MODULE_6__[\"RANGE\"].START_TIME, Number.MAX_SAFE_INTEGER), Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_2__[\"default\"])(_range, _constants_range__WEBPACK_IMPORTED_MODULE_6__[\"RANGE\"].END_TIME, Number.MIN_SAFE_INTEGER), Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_2__[\"default\"])(_range, _constants_range__WEBPACK_IMPORTED_MODULE_6__[\"RANGE\"].DEPTH, 1), Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_2__[\"default\"])(_range, _constants_range__WEBPACK_IMPORTED_MODULE_6__[\"RANGE\"].COLOUR, structure[BACKGROUND_COLOUR_PROPERTY]), Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_2__[\"default\"])(_range, _constants_range__WEBPACK_IMPORTED_MODULE_6__[\"RANGE\"].WHITE_TEXT, ['#fff', '#fffff', 'white', 'rgb(255, 255, 255)', 'rgba(255, 255, 255, 1)'].indexOf(structure[TEXT_COLOUR_PROPERTY]) !== -1), Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_2__[\"default\"])(_range, _constants_range__WEBPACK_IMPORTED_MODULE_6__[\"RANGE\"].IS_SELECTED, false), _range);\n var ranges = [];\n structure.items.forEach(function (struct) {\n var result = processLevel(struct);\n\n if (struct.type === 'Canvas') {\n extendRange(range, result);\n } else if (struct.type === 'Range') {\n result.forEach(function (childRange) {\n if (childRange) {\n extendRange(range, childRange);\n }\n });\n ranges.push.apply(ranges, result);\n }\n });\n return [range].concat(ranges);\n }\n};\n\nvar processStructures = function processStructures(manifest) {\n var allStructures = (manifest.structures || []).map(function (structure) {\n return processLevel(structure);\n });\n\n if (manifest.items.length > 1) {\n console.warn('Timeliner does not have full support for multi-canvas elements');\n }\n\n var finalRanges = Array.prototype.concat.apply([], allStructures);\n var startMin = Math.min.apply(Math, Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_toConsumableArray__WEBPACK_IMPORTED_MODULE_0__[\"default\"])(finalRanges.map(function (range) {\n return range.startTime;\n })));\n var endMax = Math.max.apply(Math, Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_toConsumableArray__WEBPACK_IMPORTED_MODULE_0__[\"default\"])(finalRanges.map(function (range) {\n return range.endTime;\n })));\n var canvas = manifest.items[0];\n\n if (canvas.duration && (startMin !== 0 || endMax !== canvas.duration * 1000)) {\n console.log('Unstable state, sections/ranges must go from start to finish', {\n startMin: startMin,\n endMax: endMax,\n canvasDuration: canvas.duration\n });\n return [Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_objectSpread__WEBPACK_IMPORTED_MODULE_1__[\"default\"])({}, _constants_range__WEBPACK_IMPORTED_MODULE_6__[\"DEFAULT_RANGE\"], {\n id: Object(_generateId__WEBPACK_IMPORTED_MODULE_9__[\"default\"])(),\n startTime: 0,\n endTime: canvas.duration * 1000\n })];\n }\n\n return finalRanges.reduce(function (ranges, range) {\n if (range) {\n var colour = getColour(range);\n\n if (colour) {\n range[_constants_range__WEBPACK_IMPORTED_MODULE_6__[\"RANGE\"].COLOUR] = colour;\n }\n\n ranges[range.id] = range;\n }\n\n return ranges;\n }, {});\n};\n\nvar mapSettings = function mapSettings(iiifSettings) {\n return Object.entries(iiifSettings || {}).reduce(function (settings, _ref4) {\n var _ref5 = Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_slicedToArray__WEBPACK_IMPORTED_MODULE_3__[\"default\"])(_ref4, 2),\n rdfKey = _ref5[0],\n value = _ref5[1];\n\n var key = rdfKey.split(':')[1];\n settings[key] = value;\n return settings;\n }, {});\n};\n\nvar manifestToProject = function manifestToProject(manifest) {\n var _objectSpread3;\n\n return Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_objectSpread__WEBPACK_IMPORTED_MODULE_1__[\"default\"])((_objectSpread3 = {}, Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_2__[\"default\"])(_objectSpread3, _constants_project__WEBPACK_IMPORTED_MODULE_4__[\"PROJECT\"].DESCRIPTION, getLocalisedResource(manifest.summary) || ''), Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_2__[\"default\"])(_objectSpread3, _constants_project__WEBPACK_IMPORTED_MODULE_4__[\"PROJECT\"].TITLE, getLocalisedResource(manifest.label) || ''), Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_2__[\"default\"])(_objectSpread3, _constants_project__WEBPACK_IMPORTED_MODULE_4__[\"PROJECT\"].HOMEPAGE, getHomepage(manifest)), Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_2__[\"default\"])(_objectSpread3, _constants_project__WEBPACK_IMPORTED_MODULE_4__[\"PROJECT\"].HOMEPAGE_LABEL, getHomepageLabel(manifest)), Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_2__[\"default\"])(_objectSpread3, _constants_project__WEBPACK_IMPORTED_MODULE_4__[\"PROJECT\"].LOADED_JSON, manifest), Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_2__[\"default\"])(_objectSpread3, _constants_project__WEBPACK_IMPORTED_MODULE_4__[\"PROJECT\"].IS_SAVED, true), _objectSpread3), mapSettings(manifest[\"\".concat(_constants_project__WEBPACK_IMPORTED_MODULE_4__[\"RDF_NAMESPACE\"], \":settings\")]));\n};\n\nfunction getHomepage(manifest) {\n return manifest && manifest.homepage && manifest.homepage.id ? manifest.homepage.id : null;\n}\n\nfunction getHomepageLabel(manifest) {\n return manifest && manifest.homepage && manifest.homepage.label ? getLocalisedResource(manifest.homepage.label) : null;\n}\n\nfunction getDuration(manifest) {\n if (manifest.items && manifest.items[0] && manifest.items[0].duration) {\n return sToMs(manifest.items[0].duration);\n }\n\n return manifest && manifest.items && manifest.items[0] && manifest.items[0].items && manifest.items[0].items[0] && manifest.items[0].items[0].items && manifest.items[0].items[0].items[0] && manifest.items[0].items[0].items[0].body && manifest.items[0].items[0].items[0].body.duration ? sToMs(manifest.items[0].items[0].items[0].body.duration) : 0;\n}\n\nfunction getStartTime(manifest) {\n var selector = manifest && manifest.items && manifest.items[0] && manifest.items[0].items && manifest.items[0].items[0] && manifest.items[0].items[0].items && manifest.items[0].items[0].items[0] && manifest.items[0].items[0].items[0].body && manifest.items[0].items[0].items[0].body.id;\n\n if (selector && selector.indexOf('#') !== -1) {\n var _qs$parse = query_string__WEBPACK_IMPORTED_MODULE_10__[\"parse\"](selector.split('#')[1]),\n t = _qs$parse.t;\n\n if (!t) {\n return 0;\n }\n\n if (t.indexOf(',') === -1) {\n console.warn('Time points cannot be used as a range for a resource');\n return 0;\n }\n\n return sToMs(parseFloat(t.split(',')[0]) || 0);\n }\n\n return 0;\n}\n\nvar manifestToViewState = function manifestToViewState(manifest) {\n var _ref6;\n\n var startTime = getStartTime(manifest);\n return _ref6 = {}, Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_2__[\"default\"])(_ref6, _constants_viewState__WEBPACK_IMPORTED_MODULE_7__[\"VIEWSTATE\"].RUNTIME, getDuration(manifest)), Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_2__[\"default\"])(_ref6, _constants_viewState__WEBPACK_IMPORTED_MODULE_7__[\"VIEWSTATE\"].IS_IMPORT_OPEN, false), Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_2__[\"default\"])(_ref6, _constants_viewState__WEBPACK_IMPORTED_MODULE_7__[\"VIEWSTATE\"].IS_SETTINGS_OPEN, false), Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_2__[\"default\"])(_ref6, _constants_viewState__WEBPACK_IMPORTED_MODULE_7__[\"VIEWSTATE\"].START_TIME, startTime), _ref6;\n};\n\nvar loadProjectState = function loadProjectState(manifest, source) {\n return {\n project: manifestToProject(manifest),\n canvas: processCanvas(manifest.items ? manifest.items[0] : null),\n range: processStructures(manifest),\n viewState: manifestToViewState(manifest),\n source: source\n };\n};\n\n//# sourceURL=webpack:///./src/utils/iiifLoader.js?"); + eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"parseMarkers\", function() { return parseMarkers; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"loadProjectState\", function() { return loadProjectState; });\n/* harmony import */ var _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_toConsumableArray__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./node_modules/babel-preset-react-app/node_modules/@babel/runtime/helpers/esm/toConsumableArray */ \"./node_modules/babel-preset-react-app/node_modules/@babel/runtime/helpers/esm/toConsumableArray.js\");\n/* harmony import */ var _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_objectSpread__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./node_modules/babel-preset-react-app/node_modules/@babel/runtime/helpers/esm/objectSpread */ \"./node_modules/babel-preset-react-app/node_modules/@babel/runtime/helpers/esm/objectSpread.js\");\n/* harmony import */ var _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./node_modules/babel-preset-react-app/node_modules/@babel/runtime/helpers/esm/defineProperty */ \"./node_modules/babel-preset-react-app/node_modules/@babel/runtime/helpers/esm/defineProperty.js\");\n/* harmony import */ var _home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_slicedToArray__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./node_modules/babel-preset-react-app/node_modules/@babel/runtime/helpers/esm/slicedToArray */ \"./node_modules/babel-preset-react-app/node_modules/@babel/runtime/helpers/esm/slicedToArray.js\");\n/* harmony import */ var _constants_project__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ../constants/project */ \"./src/constants/project.js\");\n/* harmony import */ var _constants_canvas__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ../constants/canvas */ \"./src/constants/canvas.js\");\n/* harmony import */ var _constants_range__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ../constants/range */ \"./src/constants/range.js\");\n/* harmony import */ var _constants_viewState__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ../constants/viewState */ \"./src/constants/viewState.js\");\n/* harmony import */ var _containers_AuthResource_AuthResource__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(/*! ../containers/AuthResource/AuthResource */ \"./src/containers/AuthResource/AuthResource.js\");\n/* harmony import */ var _generateId__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(/*! ./generateId */ \"./src/utils/generateId.js\");\n/* harmony import */ var query_string__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(/*! query-string */ \"./node_modules/query-string/index.js\");\n/* harmony import */ var query_string__WEBPACK_IMPORTED_MODULE_10___default = /*#__PURE__*/__webpack_require__.n(query_string__WEBPACK_IMPORTED_MODULE_10__);\n\n\n\n\n\n\n\n\n\n\n // Constants\n\nvar BACKGROUND_COLOUR_PROPERTY = 'tl:backgroundColour';\nvar TEXT_COLOUR_PROPERTY = 'tl:textColour'; // Helpers\n\n/**\n * seconds to microseconds\n * @param {String|Number} s seconds to convert\n * @return {Number} milliseconds\n */\n\nvar sToMs = function sToMs(s) {\n return parseFloat(s) * 1000;\n};\n/**\n * returns localised property\n * @param {Object} resource\n * @param {String} locale default en\n */\n\n\nvar getLocalisedResource = function getLocalisedResource(resource) {\n var locale = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'en';\n\n if (!resource) {\n return '';\n } else {\n return (resource[locale] || resource['@none'] || ['']).join('');\n }\n};\n/**\n * get custom bubble colour\n * @param {Object} range the range may has the background property\n */\n\n\nvar getColour = function getColour(range) {\n return range ? range[_constants_range__WEBPACK_IMPORTED_MODULE_6__[\"RANGE\"].COLOUR] || '' : '';\n};\n/**\n * converts url hash parameters to an object\n * @param {String} url input url\n * @returns {Object} the key-value pairs converted to object property-values.\n */\n\n\nvar hashParamsToObj = function hashParamsToObj(url) {\n return (url.split('#')[1] || '').split('&').reduce(function (params, item) {\n var _item$split = item.split('='),\n _item$split2 = Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_slicedToArray__WEBPACK_IMPORTED_MODULE_3__[\"default\"])(_item$split, 2),\n k = _item$split2[0],\n v = _item$split2[1];\n\n if (v !== undefined) {\n params[decodeURIComponent(k)] = decodeURIComponent(v);\n }\n\n return params;\n }, {});\n};\n/**\n * convert a comma separated time range to ms\n * @param {String} rangeStr\n */\n\n\nvar parseTimeRange = function parseTimeRange(rangeStr) {\n var _ref;\n\n var _rangeStr$split = rangeStr.split(','),\n _rangeStr$split2 = Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_slicedToArray__WEBPACK_IMPORTED_MODULE_3__[\"default\"])(_rangeStr$split, 2),\n startString = _rangeStr$split2[0],\n endString = _rangeStr$split2[1];\n\n return _ref = {}, Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_2__[\"default\"])(_ref, _constants_range__WEBPACK_IMPORTED_MODULE_6__[\"RANGE\"].START_TIME, sToMs(startString)), Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_2__[\"default\"])(_ref, _constants_range__WEBPACK_IMPORTED_MODULE_6__[\"RANGE\"].END_TIME, endString ? sToMs(endString) : sToMs(startString) + 1), _ref;\n};\n\nvar getLabel = function getLabel(t, defaultValue) {\n var value = Object.values(t)[0];\n\n if (value && value.length) {\n return value[0] || defaultValue;\n }\n\n return defaultValue;\n};\n\nvar parseMarkers = function parseMarkers(manifest) {\n var annotationPages = manifest.annotations;\n var annotations = annotationPages && annotationPages[0] && annotationPages[0].items ? annotationPages[0].items : null;\n\n if (!annotations) {\n return [];\n }\n\n return annotations.map(function (annotation) {\n return {\n id: annotation.id,\n time: annotation.target && annotation.target.selector && annotation.target.selector.t ? annotation.target.selector.t * 1000 : null,\n label: getLabel(annotation.label, 'Untitled marker'),\n summary: annotation.body && annotation.body.value ? annotation.body.value : annotation.body[0] && annotation.body[0].value ? annotation.body[0].value : ''\n };\n }).filter(function (annotation) {\n return annotation.time;\n });\n};\n/**\n * @param {Object} canvas - IIIFCanvas javascript object\n * @returns array all audio annotations url, start and end time\n */\n\nvar getAudioAnnotations = function getAudioAnnotations(canvas) {\n if (!canvas) {\n return [];\n }\n\n var annotations = canvas.items ? canvas.items.reduce(function (acc, annotationPage) {\n if (annotationPage.items) {\n return acc.concat(annotationPage.items);\n } else {\n return acc;\n }\n }, []) : [];\n return annotations.filter(function (annotation) {\n return annotation.motivation === 'painting' && annotation.body && (annotation.body.type === 'Audio' || annotation.body.type === 'Sound' || annotation.body.type === 'Video') || annotation.body && annotation.body.type === 'Choice';\n }).map(function (annotation) {\n var hashParams = hashParamsToObj(annotation.target);\n var audioDescriptor = {};\n\n if (hashParams.t) {\n Object.assign(audioDescriptor, parseTimeRange(hashParams.t));\n }\n\n var body = Object(_containers_AuthResource_AuthResource__WEBPACK_IMPORTED_MODULE_8__[\"resolveAvResource\"])(annotation);\n audioDescriptor.url = body.id || body['@id'];\n\n if (body && body.service) {\n audioDescriptor.service = body.service;\n }\n\n return audioDescriptor;\n });\n};\n/**\n * Simplified version of the canvas processor:\n * - does not deal with multiple audio annotation on a canvas;\n * - does not handle choices with for different audio file formats;\n * - does not load av service documents;\n * @param {Object} canvas -\n */\n\n\nvar processCanvas = function processCanvas(canvas) {\n var _ref2;\n\n var audioAnnotations = getAudioAnnotations(canvas);\n return audioAnnotations.length > 0 ? (_ref2 = {}, Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_2__[\"default\"])(_ref2, _constants_canvas__WEBPACK_IMPORTED_MODULE_5__[\"CANVAS\"].URL, audioAnnotations[0].url), Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_2__[\"default\"])(_ref2, \"service\", audioAnnotations[0].service), _ref2) : Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_2__[\"default\"])({}, _constants_canvas__WEBPACK_IMPORTED_MODULE_5__[\"CANVAS\"].ERROR, {\n code: 6,\n description: 'Manifest does not contain audio annotations'\n });\n};\n\nvar extendRange = function extendRange(parentRange, child) {\n parentRange[_constants_range__WEBPACK_IMPORTED_MODULE_6__[\"RANGE\"].START_TIME] = Math.min(parentRange[_constants_range__WEBPACK_IMPORTED_MODULE_6__[\"RANGE\"].START_TIME], child[_constants_range__WEBPACK_IMPORTED_MODULE_6__[\"RANGE\"].START_TIME]);\n parentRange[_constants_range__WEBPACK_IMPORTED_MODULE_6__[\"RANGE\"].END_TIME] = Math.max(parentRange[_constants_range__WEBPACK_IMPORTED_MODULE_6__[\"RANGE\"].END_TIME], child[_constants_range__WEBPACK_IMPORTED_MODULE_6__[\"RANGE\"].END_TIME]);\n parentRange[_constants_range__WEBPACK_IMPORTED_MODULE_6__[\"RANGE\"].DEPTH] = Math.max(child[_constants_range__WEBPACK_IMPORTED_MODULE_6__[\"RANGE\"].DEPTH] + 1, parentRange[_constants_range__WEBPACK_IMPORTED_MODULE_6__[\"RANGE\"].DEPTH] || 1);\n};\n\nvar processLevel = function processLevel(structure) {\n if (structure.type === 'Canvas') {\n var hashParams = hashParamsToObj(structure.id);\n return Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_objectSpread__WEBPACK_IMPORTED_MODULE_1__[\"default\"])({}, parseTimeRange(hashParams.t), Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_2__[\"default\"])({}, _constants_range__WEBPACK_IMPORTED_MODULE_6__[\"RANGE\"].DEPTH, 0));\n } else if (structure.type === 'Range') {\n var _range;\n\n var range = (_range = {\n id: structure.id\n }, Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_2__[\"default\"])(_range, _constants_range__WEBPACK_IMPORTED_MODULE_6__[\"RANGE\"].LABEL, getLocalisedResource(structure.label) || ''), Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_2__[\"default\"])(_range, _constants_range__WEBPACK_IMPORTED_MODULE_6__[\"RANGE\"].SUMMARY, getLocalisedResource(structure.summary) || ''), Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_2__[\"default\"])(_range, _constants_range__WEBPACK_IMPORTED_MODULE_6__[\"RANGE\"].START_TIME, Number.MAX_SAFE_INTEGER), Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_2__[\"default\"])(_range, _constants_range__WEBPACK_IMPORTED_MODULE_6__[\"RANGE\"].END_TIME, Number.MIN_SAFE_INTEGER), Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_2__[\"default\"])(_range, _constants_range__WEBPACK_IMPORTED_MODULE_6__[\"RANGE\"].DEPTH, 1), Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_2__[\"default\"])(_range, _constants_range__WEBPACK_IMPORTED_MODULE_6__[\"RANGE\"].COLOUR, structure[BACKGROUND_COLOUR_PROPERTY]), Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_2__[\"default\"])(_range, _constants_range__WEBPACK_IMPORTED_MODULE_6__[\"RANGE\"].WHITE_TEXT, ['#fff', '#fffff', 'white', 'rgb(255, 255, 255)', 'rgba(255, 255, 255, 1)'].indexOf(structure[TEXT_COLOUR_PROPERTY]) !== -1), Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_2__[\"default\"])(_range, _constants_range__WEBPACK_IMPORTED_MODULE_6__[\"RANGE\"].IS_SELECTED, false), _range);\n var ranges = [];\n structure.items.forEach(function (struct) {\n var result = processLevel(struct);\n\n if (struct.type === 'Canvas') {\n extendRange(range, result);\n } else if (struct.type === 'Range') {\n result.forEach(function (childRange) {\n if (childRange) {\n extendRange(range, childRange);\n }\n });\n ranges.push.apply(ranges, result);\n }\n });\n return [range].concat(ranges);\n }\n};\n\nvar processStructures = function processStructures(manifest) {\n var allStructures = (manifest.structures || []).map(function (structure) {\n return processLevel(structure);\n });\n\n if (manifest.items.length > 1) {\n console.warn('Timeliner does not have full support for multi-canvas elements');\n }\n\n var finalRanges = Array.prototype.concat.apply([], allStructures);\n var startMin = Math.min.apply(Math, Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_toConsumableArray__WEBPACK_IMPORTED_MODULE_0__[\"default\"])(finalRanges.map(function (range) {\n return range.startTime;\n })));\n var endMax = Math.max.apply(Math, Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_toConsumableArray__WEBPACK_IMPORTED_MODULE_0__[\"default\"])(finalRanges.map(function (range) {\n return range.endTime;\n })));\n var canvas = manifest.items[0];\n\n if (canvas.duration && (startMin !== 0 || endMax !== canvas.duration * 1000)) {\n console.log('Unstable state, sections/ranges must go from start to finish', {\n startMin: startMin,\n endMax: endMax,\n canvasDuration: canvas.duration\n });\n return [Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_objectSpread__WEBPACK_IMPORTED_MODULE_1__[\"default\"])({}, _constants_range__WEBPACK_IMPORTED_MODULE_6__[\"DEFAULT_RANGE\"], {\n id: Object(_generateId__WEBPACK_IMPORTED_MODULE_9__[\"default\"])(),\n startTime: 0,\n endTime: canvas.duration * 1000\n })];\n }\n\n return finalRanges.reduce(function (ranges, range) {\n if (range) {\n var colour = getColour(range);\n\n if (colour) {\n range[_constants_range__WEBPACK_IMPORTED_MODULE_6__[\"RANGE\"].COLOUR] = colour;\n }\n\n ranges[range.id] = range;\n }\n\n return ranges;\n }, {});\n};\n\nvar mapSettings = function mapSettings(iiifSettings) {\n return Object.entries(iiifSettings || {}).reduce(function (settings, _ref4) {\n var _ref5 = Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_slicedToArray__WEBPACK_IMPORTED_MODULE_3__[\"default\"])(_ref4, 2),\n rdfKey = _ref5[0],\n value = _ref5[1];\n\n var key = rdfKey.split(':')[1];\n settings[key] = value;\n return settings;\n }, {});\n};\n\nvar manifestToProject = function manifestToProject(manifest) {\n var _objectSpread3;\n\n return Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_objectSpread__WEBPACK_IMPORTED_MODULE_1__[\"default\"])((_objectSpread3 = {}, Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_2__[\"default\"])(_objectSpread3, _constants_project__WEBPACK_IMPORTED_MODULE_4__[\"PROJECT\"].DESCRIPTION, getLocalisedResource(manifest.summary) || ''), Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_2__[\"default\"])(_objectSpread3, _constants_project__WEBPACK_IMPORTED_MODULE_4__[\"PROJECT\"].TITLE, getLocalisedResource(manifest.label) || ''), Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_2__[\"default\"])(_objectSpread3, _constants_project__WEBPACK_IMPORTED_MODULE_4__[\"PROJECT\"].HOMEPAGE, getHomepage(manifest)), Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_2__[\"default\"])(_objectSpread3, _constants_project__WEBPACK_IMPORTED_MODULE_4__[\"PROJECT\"].HOMEPAGE_LABEL, getHomepageLabel(manifest)), Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_2__[\"default\"])(_objectSpread3, _constants_project__WEBPACK_IMPORTED_MODULE_4__[\"PROJECT\"].LOADED_JSON, manifest), Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_2__[\"default\"])(_objectSpread3, _constants_project__WEBPACK_IMPORTED_MODULE_4__[\"PROJECT\"].IS_CHANGED, true), _objectSpread3), mapSettings(manifest[\"\".concat(_constants_project__WEBPACK_IMPORTED_MODULE_4__[\"RDF_NAMESPACE\"], \":settings\")]));\n};\n\nfunction getHomepage(manifest) {\n return manifest && manifest.homepage && manifest.homepage.id ? manifest.homepage.id : null;\n}\n\nfunction getHomepageLabel(manifest) {\n return manifest && manifest.homepage && manifest.homepage.label ? getLocalisedResource(manifest.homepage.label) : null;\n}\n\nfunction getDuration(manifest) {\n if (manifest.items && manifest.items[0] && manifest.items[0].duration) {\n return sToMs(manifest.items[0].duration);\n }\n\n return manifest && manifest.items && manifest.items[0] && manifest.items[0].items && manifest.items[0].items[0] && manifest.items[0].items[0].items && manifest.items[0].items[0].items[0] && manifest.items[0].items[0].items[0].body && manifest.items[0].items[0].items[0].body.duration ? sToMs(manifest.items[0].items[0].items[0].body.duration) : 0;\n}\n\nfunction getStartTime(manifest) {\n var selector = manifest && manifest.items && manifest.items[0] && manifest.items[0].items && manifest.items[0].items[0] && manifest.items[0].items[0].items && manifest.items[0].items[0].items[0] && manifest.items[0].items[0].items[0].body && manifest.items[0].items[0].items[0].body.id;\n\n if (selector && selector.indexOf('#') !== -1) {\n var _qs$parse = query_string__WEBPACK_IMPORTED_MODULE_10__[\"parse\"](selector.split('#')[1]),\n t = _qs$parse.t;\n\n if (!t) {\n return 0;\n }\n\n if (t.indexOf(',') === -1) {\n console.warn('Time points cannot be used as a range for a resource');\n return 0;\n }\n\n return sToMs(parseFloat(t.split(',')[0]) || 0);\n }\n\n return 0;\n}\n\nvar manifestToViewState = function manifestToViewState(manifest) {\n var _ref6;\n\n var startTime = getStartTime(manifest);\n return _ref6 = {}, Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_2__[\"default\"])(_ref6, _constants_viewState__WEBPACK_IMPORTED_MODULE_7__[\"VIEWSTATE\"].RUNTIME, getDuration(manifest)), Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_2__[\"default\"])(_ref6, _constants_viewState__WEBPACK_IMPORTED_MODULE_7__[\"VIEWSTATE\"].IS_IMPORT_OPEN, false), Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_2__[\"default\"])(_ref6, _constants_viewState__WEBPACK_IMPORTED_MODULE_7__[\"VIEWSTATE\"].IS_SETTINGS_OPEN, false), Object(_home_dwithana_github_iu_timeliner_node_modules_babel_preset_react_app_node_modules_babel_runtime_helpers_esm_defineProperty__WEBPACK_IMPORTED_MODULE_2__[\"default\"])(_ref6, _constants_viewState__WEBPACK_IMPORTED_MODULE_7__[\"VIEWSTATE\"].START_TIME, startTime), _ref6;\n};\n\nvar loadProjectState = function loadProjectState(manifest, source) {\n return {\n project: manifestToProject(manifest),\n canvas: processCanvas(manifest.items ? manifest.items[0] : null),\n range: processStructures(manifest),\n viewState: manifestToViewState(manifest),\n source: source\n };\n};\n\n//# sourceURL=webpack:///./src/utils/iiifLoader.js?"); /***/ }), From 8f9e61445cb4f2ddf2db481866e5e5f0670cea80 Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Mon, 29 Aug 2022 14:02:20 -0400 Subject: [PATCH 119/230] Change return button styling on item view page --- app/views/media_objects/_destroy_checkout.html.erb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/media_objects/_destroy_checkout.html.erb b/app/views/media_objects/_destroy_checkout.html.erb index d0e914c494..e54de68d75 100644 --- a/app/views/media_objects/_destroy_checkout.html.erb +++ b/app/views/media_objects/_destroy_checkout.html.erb @@ -22,7 +22,7 @@ Unless required by applicable law or agreed to in writing, software distributed 00:00
      hh:mm
      - <%= link_to 'Return now', return_checkout_url(current_checkout), class: 'btn btn-danger', method: :patch, + <%= link_to 'Return now', return_checkout_url(current_checkout), class: 'btn btn-primary', method: :patch, id: "return-btn", data: { checkout_returntime: current_checkout.return_time } %>
      From cde17e047cd8d585f0ff6631c3855f7b00bb0823 Mon Sep 17 00:00:00 2001 From: dananji Date: Mon, 29 Aug 2022 11:15:48 -0700 Subject: [PATCH 120/230] Fix html encoded titles in collection cards --- app/javascript/components/collections/Collection.scss | 11 +++++++++++ .../collections/landing/SearchResultsCard.js | 5 +---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/app/javascript/components/collections/Collection.scss b/app/javascript/components/collections/Collection.scss index 10b8f1c314..37249ee6e9 100644 --- a/app/javascript/components/collections/Collection.scss +++ b/app/javascript/components/collections/Collection.scss @@ -83,6 +83,17 @@ .italic { font-style: italic; } + + // When `line-clamp` is used with `display: -webkit-box` + // contains text to a given amount of lines (e.g.: 2) + // Reference: https://mgearon.com/css/line-clamp-css-guide/ + h4 { + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + text-overflow: ellipsis; + } } .card-text { diff --git a/app/javascript/components/collections/landing/SearchResultsCard.js b/app/javascript/components/collections/landing/SearchResultsCard.js index 1ad2a43190..6ae6426e7e 100644 --- a/app/javascript/components/collections/landing/SearchResultsCard.js +++ b/app/javascript/components/collections/landing/SearchResultsCard.js @@ -74,10 +74,7 @@ const thumbnailSrc = (doc, props) => { }; const titleHTML = (doc) => { - var title = doc.attributes['title_tesi'] && doc.attributes['title_tesi'].attributes.value.substring(0, 50) || doc['id']; - if (doc.attributes['title_tesi'] && doc.attributes['title_tesi'].attributes.value.length >= 50) { - title += "..."; - } + var title = doc.attributes['title_tesi'] && doc.attributes['title_tesi'].attributes.value || doc['id']; return { __html: title }; }; From e5a034ec991cd6dd2e51c9e77b33949354b5b199 Mon Sep 17 00:00:00 2001 From: Mason Ballengee <68433277+masaball@users.noreply.github.com> Date: Mon, 29 Aug 2022 15:31:55 -0400 Subject: [PATCH 121/230] Update _destroy_checkout.html.erb --- app/views/media_objects/_destroy_checkout.html.erb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/media_objects/_destroy_checkout.html.erb b/app/views/media_objects/_destroy_checkout.html.erb index e54de68d75..55d7e917e4 100644 --- a/app/views/media_objects/_destroy_checkout.html.erb +++ b/app/views/media_objects/_destroy_checkout.html.erb @@ -22,7 +22,7 @@ Unless required by applicable law or agreed to in writing, software distributed 00:00
      hh:mm
      - <%= link_to 'Return now', return_checkout_url(current_checkout), class: 'btn btn-primary', method: :patch, + <%= link_to 'Return now', return_checkout_url(current_checkout), class: 'btn btn-outline', method: :patch, id: "return-btn", data: { checkout_returntime: current_checkout.return_time } %>
      From f5f98fc2a8cca1255d2e65c6d9d93844398a9f6b Mon Sep 17 00:00:00 2001 From: dananji Date: Mon, 29 Aug 2022 15:02:22 -0700 Subject: [PATCH 122/230] Center CDL checkout text, button in embed player --- app/assets/stylesheets/avalon.scss | 21 ++++++++++----------- app/views/media_objects/_checkout.html.erb | 2 +- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/app/assets/stylesheets/avalon.scss b/app/assets/stylesheets/avalon.scss index 9af271e80c..3382180878 100644 --- a/app/assets/stylesheets/avalon.scss +++ b/app/assets/stylesheets/avalon.scss @@ -1219,11 +1219,14 @@ td { p { position: relative; text-align: center; + @include media-breakpoint-down(md) { + margin-bottom: 0; + } } form { - position: relative; - left: 25%; + width: fit-content; + margin: 0 auto; } .centered { @@ -1234,15 +1237,10 @@ td { } .centered.video { - top: -50%; - left: 0; - right: 0; - bottom: 0; - height: 4rem; - - @include media-breakpoint-down(sm) { - top: -25%; - } + top: 50%; + position: absolute; + margin: 0; + transform: translateY(-50%); } } @@ -1254,5 +1252,6 @@ td { .checkout.video { padding: 3rem; height: 50%; + position: relative; } /* End of CDL controls on view page styles */ diff --git a/app/views/media_objects/_checkout.html.erb b/app/views/media_objects/_checkout.html.erb index 6e727ac3ed..4d6898ed24 100644 --- a/app/views/media_objects/_checkout.html.erb +++ b/app/views/media_objects/_checkout.html.erb @@ -14,7 +14,7 @@ Unless required by applicable law or agreed to in writing, software distributed --- END LICENSE_HEADER BLOCK --- %> <% media_object_id=@media_object.id %> -<%= form_for(Checkout.new, html: { style: "display: inline;" }) do |f| %> +<%= form_for(Checkout.new) do |f| %> <%= hidden_field_tag "authenticity_token", form_authenticity_token %> <%= hidden_field_tag "checkout[media_object_id]", media_object_id %> <%= f.submit "Check Out", class: "btn btn-info check-out-btn", From c4901e65b9e41525d1ddfddd328d9280e42a5129 Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Tue, 30 Aug 2022 12:36:31 -0400 Subject: [PATCH 123/230] Add authentication prompt for cdl items --- .../_checkout_authenticate.html.erb | 64 +++++++++++++++++++ .../media_objects/_embed_checkout.html.erb | 8 ++- config/locales/en.yml | 1 + 3 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 app/views/media_objects/_checkout_authenticate.html.erb diff --git a/app/views/media_objects/_checkout_authenticate.html.erb b/app/views/media_objects/_checkout_authenticate.html.erb new file mode 100644 index 0000000000..a08edffa27 --- /dev/null +++ b/app/views/media_objects/_checkout_authenticate.html.erb @@ -0,0 +1,64 @@ +<%# +Copyright 2011-2022, The Trustees of Indiana University and Northwestern + University. Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed + under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + CONDITIONS OF ANY KIND, either express or implied. See the License for the + specific language governing permissions and limitations under the License. +--- END LICENSE_HEADER BLOCK --- +%> + + + +<% content_for :page_scripts do %> + +<% end %> diff --git a/app/views/media_objects/_embed_checkout.html.erb b/app/views/media_objects/_embed_checkout.html.erb index a3cf9e9764..cbc37a9b1a 100644 --- a/app/views/media_objects/_embed_checkout.html.erb +++ b/app/views/media_objects/_embed_checkout.html.erb @@ -15,9 +15,13 @@ Unless required by applicable law or agreed to in writing, software distributed %> <% if !@masterFiles.blank? %> <% master_file=@media_object.master_files.first %> -
      + <% h = current_user ? 100 : 120 %> +
      - <% if @media_object.lending_status == "available" %> + <% if !current_user %> + <%= t('media_object.cdl.unauthenticated_message').html_safe %> + <%= render "checkout_authenticate" %> + <% elsif @media_object.lending_status == "available" %> <%= t('media_object.cdl.checkout_message').html_safe %> <%= render "checkout" %> <% else %> diff --git a/config/locales/en.yml b/config/locales/en.yml index ae46722a66..e8cb5b533f 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -26,6 +26,7 @@ en: checkout_message: "

      Borrow this item to access media resources.

      " not_available_message: "

      This resource is not available to be checked out at the moment. Please check again later.

      " time_remaining: "

      Time
      remaining:

      " + unauthenticated_message: "

      You are not signed in. You may be able to borrow this item after signing in.

      " expire: heading: "Check Out Expired!" message: "Your lending period has been expired. Please return the item, and check for availability later." From c07ac37b40a4e6aa147a581052645e76d4acfa41 Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Tue, 30 Aug 2022 14:38:50 -0400 Subject: [PATCH 124/230] Change from authenticaiton popup to redirect --- .../_checkout_authenticate.html.erb | 46 +------------------ 1 file changed, 2 insertions(+), 44 deletions(-) diff --git a/app/views/media_objects/_checkout_authenticate.html.erb b/app/views/media_objects/_checkout_authenticate.html.erb index a08edffa27..2b706f9813 100644 --- a/app/views/media_objects/_checkout_authenticate.html.erb +++ b/app/views/media_objects/_checkout_authenticate.html.erb @@ -16,49 +16,7 @@ Unless required by applicable law or agreed to in writing, software distributed - -<% content_for :page_scripts do %> - -<% end %> From 68959867d6fa9f2df4b99c8738f72b78f4b44949 Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Mon, 29 Aug 2022 11:21:15 -0400 Subject: [PATCH 125/230] Improve devise error display --- app/helpers/devise_helper.rb | 29 +++++++++++++++----- app/views/devise/passwords/edit.html.erb | 10 +++---- app/views/devise/passwords/new.html.erb | 6 ++-- app/views/devise/registrations/edit.html.erb | 18 ++++++------ app/views/devise/registrations/new.html.erb | 18 ++++++------ config/application.rb | 2 ++ config/locales/devise.en.yml | 4 +-- 7 files changed, 52 insertions(+), 35 deletions(-) diff --git a/app/helpers/devise_helper.rb b/app/helpers/devise_helper.rb index a2654dd0e9..a5830527a9 100644 --- a/app/helpers/devise_helper.rb +++ b/app/helpers/devise_helper.rb @@ -1,11 +1,11 @@ # Copyright 2011-2022, The Trustees of Indiana University and Northwestern # University. Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. -# +# # You may obtain a copy of the License at -# +# # http://www.apache.org/licenses/LICENSE-2.0 -# +# # Unless required by applicable law or agreed to in writing, software distributed # under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR # CONDITIONS OF ANY KIND, either express or implied. See the License for the @@ -15,10 +15,25 @@ module DeviseHelper def devise_error_messages! return "" if resource.errors.empty? - - flash[:error] = I18n.t("errors.messages.not_saved", + flash[:error] =simple_format I18n.t("errors.messages.not_saved", count: resource.errors.count, - resource: resource.class.model_name.human.downcase - ) + resource: resource.class.model_name.human.downcase, + message: error_message(resource) + ) end + + private + + def error_message(resource) + message = [] + resource.errors.messages.each do |key, messages| + if key == :email || key == :username + m = "#{key.capitalize} \"#{resource.errors&.details[key]&.first&.[](:value)}\" #{messages.join(' and ')}" + else + m = "#{key.to_s.gsub(/_/, ' ').capitalize} #{messages.join(' and ')}" + end + message.append(m) + end + message.join("\n- ").prepend("- ") + end end diff --git a/app/views/devise/passwords/edit.html.erb b/app/views/devise/passwords/edit.html.erb index 79e78ef756..688f375361 100644 --- a/app/views/devise/passwords/edit.html.erb +++ b/app/views/devise/passwords/edit.html.erb @@ -17,20 +17,20 @@ Unless required by applicable law or agreed to in writing, software distributed

      Change your password

      <%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put }) do |f| %> - <%= devise_error_messages! %> + <% devise_error_messages! %> <%= f.hidden_field :reset_password_token %> -
      +
      <%= f.label :password, "New password" %>
      <% if @minimum_password_length %> (<%= @minimum_password_length %> characters minimum)
      <% end %> - <%= f.password_field :password, autofocus: true, autocomplete: "off" %> + <%= f.password_field :password, autofocus: true, autocomplete: "off", class: (resource.errors[:password].any? ? 'form-control is-invalid' : 'form-control') %>
      -
      +
      <%= f.label :password_confirmation, "Confirm new password" %>
      - <%= f.password_field :password_confirmation, autocomplete: "off" %> + <%= f.password_field :password_confirmation, autocomplete: "off", class: (resource.errors[:password_confirmation].any? ? 'form-control is-invalid' : 'form-control') %>
      diff --git a/app/views/devise/passwords/new.html.erb b/app/views/devise/passwords/new.html.erb index 5e15019450..cbdd0319f8 100644 --- a/app/views/devise/passwords/new.html.erb +++ b/app/views/devise/passwords/new.html.erb @@ -15,11 +15,11 @@ Unless required by applicable law or agreed to in writing, software distributed %>
      <%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post }) do |f| %> - <%= devise_error_messages! %> + <% devise_error_messages! %> -
      +
      <%= f.label :email, 'Email:' %>
      - <%= f.email_field :email, autofocus: true, autocomplete: "email" %> + <%= f.email_field :email, autofocus: true, autocomplete: "email", class: (resource.errors[:email].any? ? 'form-control is-invalid' : 'form-control') %>
      diff --git a/app/views/devise/registrations/edit.html.erb b/app/views/devise/registrations/edit.html.erb index 6ab246d948..7bd5e75ac0 100644 --- a/app/views/devise/registrations/edit.html.erb +++ b/app/views/devise/registrations/edit.html.erb @@ -18,34 +18,34 @@ Unless required by applicable law or agreed to in writing, software distributed

      Edit <%= resource_name.to_s.humanize %>

      <%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }) do |f| %> - <%= devise_error_messages! %> + <% devise_error_messages! %> -
      +
      <%= f.label :email %>
      - <%= f.email_field :email, autofocus: true, autocomplete: "email" %> + <%= f.email_field :email, autofocus: true, autocomplete: "email", class: (resource.errors[:email].any? ? 'form-control is-invalid' : 'form-control') %>
      <% if devise_mapping.confirmable? && resource.pending_reconfirmation? %>
      Currently waiting confirmation for: <%= resource.unconfirmed_email %>
      <% end %> -
      +
      <%= f.label :password %> (leave blank if you don't want to change it)
      - <%= f.password_field :password, autocomplete: "off" %> + <%= f.password_field :password, autocomplete: "off", class: (resource.errors[:password].any? ? 'form-control is-invalid' : 'form-control') %> <% if @minimum_password_length %>
      <%= @minimum_password_length %> characters minimum <% end %>
      -
      +
      <%= f.label :password_confirmation %>
      - <%= f.password_field :password_confirmation, autocomplete: "off" %> + <%= f.password_field :password_confirmation, autocomplete: "off", class: (resource.errors[:password_confirmation].any? ? 'form-control is-invalid' : 'form-control') %>
      -
      +
      <%= f.label :current_password %> (we need your current password to confirm your changes)
      - <%= f.password_field :current_password, autocomplete: "off" %> + <%= f.password_field :current_password, autocomplete: "off", class: (resource.errors[:current_password].any? ? 'form-control is-invalid' : 'form-control') %>
      diff --git a/app/views/devise/registrations/new.html.erb b/app/views/devise/registrations/new.html.erb index 6e88497f15..8f697a0b2f 100644 --- a/app/views/devise/registrations/new.html.erb +++ b/app/views/devise/registrations/new.html.erb @@ -17,29 +17,29 @@ Unless required by applicable law or agreed to in writing, software distributed

      Sign up

      <%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %> - <%= devise_error_messages! %> + <% devise_error_messages! %> -
      +
      <%= f.label :username, class: 'font-weight-bold' %>
      - <%= f.text_field :username, autofocus: true, autocomplete: 'username' %> + <%= f.text_field :username, autofocus: true, autocomplete: 'username', class: (resource.errors[:username].any? ? 'form-control is-invalid' : 'form-control') %>
      -
      +
      <%= f.label :email, class: 'font-weight-bold' %>
      - <%= f.email_field :email, autofocus: true, autocomplete: "email" %> + <%= f.email_field :email, autofocus: true, autocomplete: "email", class: (resource.errors[:email].any? ? 'form-control is-invalid' : 'form-control') %>
      -
      +
      <%= f.label :password, class: 'font-weight-bold' %> <% if @minimum_password_length %> (<%= @minimum_password_length %> characters minimum) <% end %>
      - <%= f.password_field :password, autocomplete: "off" %> + <%= f.password_field :password, autocomplete: "off", class: (resource.errors[:password].any? ? 'form-control is-invalid' : 'form-control') %>
      -
      +
      <%= f.label :password_confirmation, class: 'font-weight-bold' %>
      - <%= f.password_field :password_confirmation, autocomplete: "off" %> + <%= f.password_field :password_confirmation, autocomplete: "off", class: (resource.errors[:password_confirmation].any? ? 'form-control is-invalid' : 'form-control') %>
      diff --git a/config/application.rb b/config/application.rb index 56156cf0f2..4ecbf19dbb 100644 --- a/config/application.rb +++ b/config/application.rb @@ -50,5 +50,7 @@ class Application < Rails::Application end config.active_storage.service = (Settings&.active_storage&.service.presence || "local").to_sym + + config.action_view.field_error_proc = Proc.new { |html_tag, instance| "#{html_tag}".html_safe } end end diff --git a/config/locales/devise.en.yml b/config/locales/devise.en.yml index 8d31732b7d..c339855bed 100644 --- a/config/locales/devise.en.yml +++ b/config/locales/devise.en.yml @@ -8,8 +8,8 @@ en: already_confirmed: "was already confirmed, please try signing in" not_locked: "was not locked" not_saved: - one: "An error prohibited this %{resource} from being saved" - other: "%{count} errors prohibited this %{resource} from being saved" + one: "An error prohibited this %{resource} from being saved:\n%{message}" + other: "%{count} errors prohibited this %{resource} from being saved:\n%{message}" devise: failure: From 4b98d988d9233cfc0c16ca93399164c01ca40fd3 Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Mon, 29 Aug 2022 12:12:00 -0400 Subject: [PATCH 126/230] Fixes for codeclimate --- app/helpers/devise_helper.rb | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/app/helpers/devise_helper.rb b/app/helpers/devise_helper.rb index a5830527a9..4e6c2efefa 100644 --- a/app/helpers/devise_helper.rb +++ b/app/helpers/devise_helper.rb @@ -15,11 +15,11 @@ module DeviseHelper def devise_error_messages! return "" if resource.errors.empty? - flash[:error] =simple_format I18n.t("errors.messages.not_saved", - count: resource.errors.count, - resource: resource.class.model_name.human.downcase, - message: error_message(resource) - ) + flash[:error] = simple_format I18n.t("errors.messages.not_saved", + count: resource.errors.count, + resource: resource.class.model_name.human.downcase, + message: error_message(resource) + ) end private @@ -27,11 +27,11 @@ def devise_error_messages! def error_message(resource) message = [] resource.errors.messages.each do |key, messages| - if key == :email || key == :username - m = "#{key.capitalize} \"#{resource.errors&.details[key]&.first&.[](:value)}\" #{messages.join(' and ')}" - else - m = "#{key.to_s.gsub(/_/, ' ').capitalize} #{messages.join(' and ')}" - end + m = if key == :email || key == :username + "#{key.capitalize} \"#{resource.errors&.details[key]&.first&.[](:value)}\" #{messages&.join(' and ')}" + else + "#{key.to_s.tr('_', ' ').capitalize} #{messages.join(' and ')}" + end message.append(m) end message.join("\n- ").prepend("- ") From 91ab8082b8e5828be0e918bf2eca52881ca8aa23 Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Mon, 29 Aug 2022 13:41:55 -0400 Subject: [PATCH 127/230] More codeclimate fixes --- app/helpers/devise_helper.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/helpers/devise_helper.rb b/app/helpers/devise_helper.rb index 4e6c2efefa..2e59877559 100644 --- a/app/helpers/devise_helper.rb +++ b/app/helpers/devise_helper.rb @@ -16,9 +16,9 @@ module DeviseHelper def devise_error_messages! return "" if resource.errors.empty? flash[:error] = simple_format I18n.t("errors.messages.not_saved", - count: resource.errors.count, - resource: resource.class.model_name.human.downcase, - message: error_message(resource) + count: resource.errors.count, + resource: resource.class.model_name.human.downcase, + message: error_message(resource) ) end @@ -28,7 +28,7 @@ def error_message(resource) message = [] resource.errors.messages.each do |key, messages| m = if key == :email || key == :username - "#{key.capitalize} \"#{resource.errors&.details[key]&.first&.[](:value)}\" #{messages&.join(' and ')}" + "#{key.capitalize} \"#{resource.errors.details[key]&.first&.[](:value)}\" #{messages.join(' and ')}" else "#{key.to_s.tr('_', ' ').capitalize} #{messages.join(' and ')}" end From 295d0681d0844cec9592ce74edc5346f27c6ee1a Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Wed, 31 Aug 2022 11:28:59 -0400 Subject: [PATCH 128/230] Create view for devise errors Devise is deprecating the old helper method for displaying errors. --- app/helpers/devise_helper.rb | 39 ------------------- .../devise/shared/_error_messages.html.erb | 17 ++++++++ config/application.rb | 2 - config/locales/devise.en.yml | 4 +- 4 files changed, 19 insertions(+), 43 deletions(-) delete mode 100644 app/helpers/devise_helper.rb create mode 100644 app/views/devise/shared/_error_messages.html.erb diff --git a/app/helpers/devise_helper.rb b/app/helpers/devise_helper.rb deleted file mode 100644 index 2e59877559..0000000000 --- a/app/helpers/devise_helper.rb +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2011-2022, The Trustees of Indiana University and Northwestern -# University. Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software distributed -# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR -# CONDITIONS OF ANY KIND, either express or implied. See the License for the -# specific language governing permissions and limitations under the License. -# --- END LICENSE_HEADER BLOCK --- - -module DeviseHelper - def devise_error_messages! - return "" if resource.errors.empty? - flash[:error] = simple_format I18n.t("errors.messages.not_saved", - count: resource.errors.count, - resource: resource.class.model_name.human.downcase, - message: error_message(resource) - ) - end - - private - - def error_message(resource) - message = [] - resource.errors.messages.each do |key, messages| - m = if key == :email || key == :username - "#{key.capitalize} \"#{resource.errors.details[key]&.first&.[](:value)}\" #{messages.join(' and ')}" - else - "#{key.to_s.tr('_', ' ').capitalize} #{messages.join(' and ')}" - end - message.append(m) - end - message.join("\n- ").prepend("- ") - end -end diff --git a/app/views/devise/shared/_error_messages.html.erb b/app/views/devise/shared/_error_messages.html.erb new file mode 100644 index 0000000000..355aade066 --- /dev/null +++ b/app/views/devise/shared/_error_messages.html.erb @@ -0,0 +1,17 @@ +<% if resource.errors.any? %> +
      +
      +

      + <%= I18n.t("errors.messages.not_saved", + count: resource.errors.count, + resource:resource.class.model_name.human.downcase) + %> +

      +
        + <% resource.errors.full_messages.each do |message| %> +
      • <%= message %>
      • + <% end %> +
      +
      +
      +<% end %> diff --git a/config/application.rb b/config/application.rb index 4ecbf19dbb..56156cf0f2 100644 --- a/config/application.rb +++ b/config/application.rb @@ -50,7 +50,5 @@ class Application < Rails::Application end config.active_storage.service = (Settings&.active_storage&.service.presence || "local").to_sym - - config.action_view.field_error_proc = Proc.new { |html_tag, instance| "#{html_tag}".html_safe } end end diff --git a/config/locales/devise.en.yml b/config/locales/devise.en.yml index c339855bed..aea197d59a 100644 --- a/config/locales/devise.en.yml +++ b/config/locales/devise.en.yml @@ -8,8 +8,8 @@ en: already_confirmed: "was already confirmed, please try signing in" not_locked: "was not locked" not_saved: - one: "An error prohibited this %{resource} from being saved:\n%{message}" - other: "%{count} errors prohibited this %{resource} from being saved:\n%{message}" + one: "An error prohibited this %{resource} from being saved:" + other: "%{count} errors prohibited this %{resource} from being saved:" devise: failure: From 75151b0a898660d64e1fbd2c5a44ec8df92869da Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Wed, 31 Aug 2022 13:03:32 -0400 Subject: [PATCH 129/230] Change devise pages to use bootstrap forms --- app/views/devise/passwords/edit.html.erb | 31 ++++++-------- app/views/devise/passwords/new.html.erb | 16 +++---- app/views/devise/registrations/edit.html.erb | 45 +++++++------------- app/views/devise/registrations/new.html.erb | 44 +++++++------------ 4 files changed, 50 insertions(+), 86 deletions(-) diff --git a/app/views/devise/passwords/edit.html.erb b/app/views/devise/passwords/edit.html.erb index 688f375361..27b6956432 100644 --- a/app/views/devise/passwords/edit.html.erb +++ b/app/views/devise/passwords/edit.html.erb @@ -13,29 +13,24 @@ Unless required by applicable law or agreed to in writing, software distributed specific language governing permissions and limitations under the License. --- END LICENSE_HEADER BLOCK --- %> + +<%= render "devise/shared/error_messages", resource: resource %> +

      Change your password

      - <%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put }) do |f| %> - <% devise_error_messages! %> + <%= bootstrap_form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put }, inline_errors: false) do |f| %> <%= f.hidden_field :reset_password_token %> -
      - <%= f.label :password, "New password" %>
      - <% if @minimum_password_length %> - (<%= @minimum_password_length %> characters minimum)
      - <% end %> - <%= f.password_field :password, autofocus: true, autocomplete: "off", class: (resource.errors[:password].any? ? 'form-control is-invalid' : 'form-control') %> -
      - -
      - <%= f.label :password_confirmation, "Confirm new password" %>
      - <%= f.password_field :password_confirmation, autocomplete: "off", class: (resource.errors[:password_confirmation].any? ? 'form-control is-invalid' : 'form-control') %> -
      - -
      - <%= f.submit "Change my password", class: 'btn btn-primary' %> -
      + <% if @minimum_password_length %> + <%= f.password_field :password, label: "Password (leave blank if you don't want to change it)".html_safe, help: "#{@minimum_password_length} characters minimum", autocomplete: 'off' %> + <% else %> + <%= f.password_field :password, label: "Password (leave blank if you don't want to change it)".html_safe, autocomplete: 'off' %> + <% end %> + + <%= f.password_field :password_confirmation, autocomplete: "off", label: "Confirm new password" %> + + <%= f.submit "Change my password", class: 'btn btn-primary' %> <% end %> <%= render "devise/shared/links" %> diff --git a/app/views/devise/passwords/new.html.erb b/app/views/devise/passwords/new.html.erb index cbdd0319f8..3e6e27ae4b 100644 --- a/app/views/devise/passwords/new.html.erb +++ b/app/views/devise/passwords/new.html.erb @@ -13,18 +13,14 @@ Unless required by applicable law or agreed to in writing, software distributed specific language governing permissions and limitations under the License. --- END LICENSE_HEADER BLOCK --- %> -
      - <%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post }) do |f| %> - <% devise_error_messages! %> -
      - <%= f.label :email, 'Email:' %>
      - <%= f.email_field :email, autofocus: true, autocomplete: "email", class: (resource.errors[:email].any? ? 'form-control is-invalid' : 'form-control') %> -
      +<%= render "devise/shared/error_messages", resource: resource %> + +
      + <%= bootstrap_form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post }, inline_errors: false) do |f| %> + <%= f.email_field :email, autofocus: true, autocomplete: "email", label: "Email:" %> -
      - <%= f.submit "Send me reset password instructions", class: 'btn btn-primary' %> -
      + <%= f.submit "Send me reset password instructions", class: 'btn btn-primary' %> <% end %>

      Edit <%= resource_name.to_s.humanize %>

      - <%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }) do |f| %> - <% devise_error_messages! %> - -
      - <%= f.label :email %>
      - <%= f.email_field :email, autofocus: true, autocomplete: "email", class: (resource.errors[:email].any? ? 'form-control is-invalid' : 'form-control') %> -
      + <%= bootstrap_form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }, inline_errors: false) do |f| %> + <%= f.email_field :email, autofocus: true, autocomplete: "email" %> <% if devise_mapping.confirmable? && resource.pending_reconfirmation? %>
      Currently waiting confirmation for: <%= resource.unconfirmed_email %>
      <% end %> -
      - <%= f.label :password %> (leave blank if you don't want to change it)
      - <%= f.password_field :password, autocomplete: "off", class: (resource.errors[:password].any? ? 'form-control is-invalid' : 'form-control') %> - <% if @minimum_password_length %> -
      - <%= @minimum_password_length %> characters minimum - <% end %> -
      - -
      - <%= f.label :password_confirmation %>
      - <%= f.password_field :password_confirmation, autocomplete: "off", class: (resource.errors[:password_confirmation].any? ? 'form-control is-invalid' : 'form-control') %> -
      - -
      - <%= f.label :current_password %> (we need your current password to confirm your changes)
      - <%= f.password_field :current_password, autocomplete: "off", class: (resource.errors[:current_password].any? ? 'form-control is-invalid' : 'form-control') %> -
      - -
      - <%= f.submit "Update", class: 'btn btn-primary' %> -
      + <% if @minimum_password_length %> + <%= f.password_field :password, label: "Password (leave blank if you don't want to change it)".html_safe, help: "#{@minimum_password_length} characters minimum", autocomplete: 'off' %> + <% else %> + <%= f.password_field :password, label: "Password (leave blank if you don't want to change it)".html_safe, autocomplete: 'off' %> + <% end %> + + <%= f.password_field :password_confirmation, autocomplete: "off" %> + + <%= f.password_field :current_password, autocomplete: "off", label: "Current password (we need your current password to confirm your changes)".html_safe %> + + <%= f.submit "Update", class: 'btn btn-primary' %> <% end %> <%= link_to "Back", :back %>
      diff --git a/app/views/devise/registrations/new.html.erb b/app/views/devise/registrations/new.html.erb index 8f697a0b2f..ecdbcc6860 100644 --- a/app/views/devise/registrations/new.html.erb +++ b/app/views/devise/registrations/new.html.erb @@ -13,38 +13,24 @@ Unless required by applicable law or agreed to in writing, software distributed specific language governing permissions and limitations under the License. --- END LICENSE_HEADER BLOCK --- %> + +<%= render "devise/shared/error_messages", resource: resource %> +

      Sign up

      - <%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %> - <% devise_error_messages! %> - -
      - <%= f.label :username, class: 'font-weight-bold' %>
      - <%= f.text_field :username, autofocus: true, autocomplete: 'username', class: (resource.errors[:username].any? ? 'form-control is-invalid' : 'form-control') %> -
      - -
      - <%= f.label :email, class: 'font-weight-bold' %>
      - <%= f.email_field :email, autofocus: true, autocomplete: "email", class: (resource.errors[:email].any? ? 'form-control is-invalid' : 'form-control') %> -
      - -
      - <%= f.label :password, class: 'font-weight-bold' %> - <% if @minimum_password_length %> - (<%= @minimum_password_length %> characters minimum) - <% end %>
      - <%= f.password_field :password, autocomplete: "off", class: (resource.errors[:password].any? ? 'form-control is-invalid' : 'form-control') %> -
      - -
      - <%= f.label :password_confirmation, class: 'font-weight-bold' %>
      - <%= f.password_field :password_confirmation, autocomplete: "off", class: (resource.errors[:password_confirmation].any? ? 'form-control is-invalid' : 'form-control') %> -
      - -
      - <%= f.submit "Sign up", class: 'btn btn-primary' %> -
      + <%= bootstrap_form_for(resource, as: resource_name, url: registration_path(resource_name), inline_errors: false) do |f| %> + <%= f.text_field :username, label_class: 'font-weight-bold', autofocus: true, autocomplete: 'username' %> + <%= f.email_field :email, label_class: 'font-weight-bold', autofocus: true, autocomplete: 'email' %> + + <% if @minimum_password_length %> + <%= f.password_field :password, label_class: 'font-weight-bold', help: "#{@minimum_password_length} characters minimum", autocomplete: 'off' %> + <% else %> + <%= f.password_field :password, label_class: 'font-weight-bold', autocomplete: 'off' %> + <% end %> + + <%= f.password_field :password_confirmation, label_class: 'font-weight-bold', autocomplete: 'off' %> + <%= f.submit "Sign up", class: 'btn btn-primary' %> <% end %> <%= render "devise/shared/links" %> From ea2187dd11121754f7145f7b8c13c5b5f8eda148 Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Wed, 31 Aug 2022 14:25:09 -0400 Subject: [PATCH 130/230] Fix label on reset password page --- app/views/devise/passwords/edit.html.erb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/devise/passwords/edit.html.erb b/app/views/devise/passwords/edit.html.erb index 27b6956432..a6816a610a 100644 --- a/app/views/devise/passwords/edit.html.erb +++ b/app/views/devise/passwords/edit.html.erb @@ -23,9 +23,9 @@ Unless required by applicable law or agreed to in writing, software distributed <%= f.hidden_field :reset_password_token %> <% if @minimum_password_length %> - <%= f.password_field :password, label: "Password (leave blank if you don't want to change it)".html_safe, help: "#{@minimum_password_length} characters minimum", autocomplete: 'off' %> + <%= f.password_field :password, label: "New password", help: "#{@minimum_password_length} characters minimum", autocomplete: 'off' %> <% else %> - <%= f.password_field :password, label: "Password (leave blank if you don't want to change it)".html_safe, autocomplete: 'off' %> + <%= f.password_field :password, label: "New password", autocomplete: 'off' %> <% end %> <%= f.password_field :password_confirmation, autocomplete: "off", label: "Confirm new password" %> From 96b36dbac4213c8b8c34bd1b393973d71af90dac Mon Sep 17 00:00:00 2001 From: dananji Date: Wed, 31 Aug 2022 14:44:55 -0400 Subject: [PATCH 131/230] Change check out button text on item page --- app/assets/stylesheets/avalon/_buttons.scss | 6 ----- app/views/media_objects/_checkout.html.erb | 17 ++----------- spec/features/media_object_spec.rb | 27 +++++++++++++++++++++ 3 files changed, 29 insertions(+), 21 deletions(-) diff --git a/app/assets/stylesheets/avalon/_buttons.scss b/app/assets/stylesheets/avalon/_buttons.scss index 7383eab877..9ff8d7c324 100644 --- a/app/assets/stylesheets/avalon/_buttons.scss +++ b/app/assets/stylesheets/avalon/_buttons.scss @@ -38,9 +38,3 @@ button.close { text-decoration: none; } } - -.check-out-btn:hover { - transform:scale(1.1); - -webkit-transform:scale(1.1); - -moz-transform:scale(1.1); -} diff --git a/app/views/media_objects/_checkout.html.erb b/app/views/media_objects/_checkout.html.erb index 4d6898ed24..25e965ec3b 100644 --- a/app/views/media_objects/_checkout.html.erb +++ b/app/views/media_objects/_checkout.html.erb @@ -14,22 +14,9 @@ Unless required by applicable law or agreed to in writing, software distributed --- END LICENSE_HEADER BLOCK --- %> <% media_object_id=@media_object.id %> +<% lending_period=ActiveSupport::Duration.build(@media_object.lending_period).to_day_hour_s%> <%= form_for(Checkout.new) do |f| %> <%= hidden_field_tag "authenticity_token", form_authenticity_token %> <%= hidden_field_tag "checkout[media_object_id]", media_object_id %> - <%= f.submit "Check Out", class: "btn btn-info check-out-btn", - data: { lending_period: ActiveSupport::Duration.build(@media_object.lending_period).to_day_hour_s } %> -<% end %> - -<% content_for :page_scripts do %> - + <%= f.submit "Borrow for #{lending_period}", class: "btn btn-info" %> <% end %> diff --git a/spec/features/media_object_spec.rb b/spec/features/media_object_spec.rb index 8a81c29388..97e06181c8 100644 --- a/spec/features/media_object_spec.rb +++ b/spec/features/media_object_spec.rb @@ -85,4 +85,31 @@ end end end + describe 'displays cdl controls' do + before { allow(Settings.controlled_digital_lending).to receive(:enable).and_return(true) } + let(:available_media_object) { FactoryBot.build(:media_object) } + let!(:mf) { FactoryBot.create(:master_file, media_object: available_media_object) } + + context 'displays embedded player' do + it 'with proper text when available' do + visit media_object_path(available_media_object) + expect(page.has_content?('Borrow this item to access media resources.')).to be_truthy + expect(page).to have_selector(:link_or_button, 'Borrow for 14 days') + end + it 'with proper text when not available' do + # Checkout the available media object with a different user + normal_user = FactoryBot.create(:user) + FactoryBot.create(:checkout, media_object_id: available_media_object.id, user_id: normal_user.id).save + + visit media_object_path(available_media_object) + expect(page.has_content?('This resource is not available to be checked out at the moment. Please check again later.')).to be_truthy + end + end + + it 'displays countdown timer when checked out' do + visit media_object_path(media_object) + expect(page.has_content?('Time remaining:')).to be_truthy + expect(page).to have_selector(:link_or_button, 'Return now') + end + end end From 1694919a7b5df2d63fa2b036b1fae81bba51c1f9 Mon Sep 17 00:00:00 2001 From: Dananji Withana Date: Wed, 31 Aug 2022 16:02:39 -0400 Subject: [PATCH 132/230] Update spec/features/media_object_spec.rb Co-authored-by: Chris Colvard --- spec/features/media_object_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/features/media_object_spec.rb b/spec/features/media_object_spec.rb index 97e06181c8..5ab705bbf6 100644 --- a/spec/features/media_object_spec.rb +++ b/spec/features/media_object_spec.rb @@ -93,7 +93,7 @@ context 'displays embedded player' do it 'with proper text when available' do visit media_object_path(available_media_object) - expect(page.has_content?('Borrow this item to access media resources.')).to be_truthy + expect(page).to have_content('Borrow this item to access media resources.') expect(page).to have_selector(:link_or_button, 'Borrow for 14 days') end it 'with proper text when not available' do From cb10784eecbb1e27c5680992c36c533c398e5a91 Mon Sep 17 00:00:00 2001 From: dananji Date: Wed, 31 Aug 2022 16:43:27 -0400 Subject: [PATCH 133/230] Adjust collection card and metadata styling to properly align content on page --- .../components/collections/Collection.scss | 37 +++++++++++++++---- .../collections/landing/SearchResultsCard.js | 3 -- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/app/javascript/components/collections/Collection.scss b/app/javascript/components/collections/Collection.scss index 37249ee6e9..c5691361c7 100644 --- a/app/javascript/components/collections/Collection.scss +++ b/app/javascript/components/collections/Collection.scss @@ -50,16 +50,11 @@ /* Collection cards */ .collection-card { - height: 415px; + height: 95%; overflow: hidden; - // For medium screens - @media (min-width: 768px) and (max-width: 1199.98px) { - height: 420px; - } - - // For smaller screens - @media screen and (max-width: 767px) { + // For smaller screens set height to auto + @media screen and (max-width: 575.98px) { height: auto; } @@ -104,6 +99,15 @@ padding-right: 8px; padding-left: 8px; } + + // Limit metadata values to 2 lines in cards + dd { + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + text-overflow: ellipsis; + } } .search-within-facets { @@ -124,6 +128,23 @@ margin-top: 3rem; padding-left: 0; + // Using flexbox to adjust all card heights to match the tallest card height + @media screen and (min-width: 575.98px) { + display: flex; + flex-direction: row; + padding: 10px; + + li { + padding-right: 10px; + flex-grow: 1; + width: 33%; + + &:last-child { + padding-right: 0; + } + } + } + .document-thumbnail { position: relative; } diff --git a/app/javascript/components/collections/landing/SearchResultsCard.js b/app/javascript/components/collections/landing/SearchResultsCard.js index 6ae6426e7e..35ddd42276 100644 --- a/app/javascript/components/collections/landing/SearchResultsCard.js +++ b/app/javascript/components/collections/landing/SearchResultsCard.js @@ -25,9 +25,6 @@ const CardMetaData = ({ doc, fieldLabel, fieldName }) => { let value = doc.attributes[fieldName]?.attributes?.value; if (Array.isArray(value) && value.length > 1) { metaData = value.join(', '); - } else if (typeof value == 'string') { - const summary = value.substring(0, 50); - metaData = value.length >= 50 ? `${summary}...` : value; } else { metaData = value; } From 965780254f9737b85c2a9c9e400ceb6cc195d2dc Mon Sep 17 00:00:00 2001 From: Chris Colvard Date: Fri, 2 Sep 2022 10:18:52 -0400 Subject: [PATCH 134/230] Outline buttons of bulk actions on selected items page --- app/views/bookmarks/_document_action.html.erb | 2 +- app/views/bookmarks/_formless_document_action.html.erb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/bookmarks/_document_action.html.erb b/app/views/bookmarks/_document_action.html.erb index 4a5fba6aa9..f96591867f 100644 --- a/app/views/bookmarks/_document_action.html.erb +++ b/app/views/bookmarks/_document_action.html.erb @@ -16,5 +16,5 @@ Unless required by applicable law or agreed to in writing, software distributed <%= link_to document_action_label(document_action_config.key, document_action_config), document_action_path(document_action_config, (local_assigns.has_key?(:url_opts) ? url_opts : {}).merge(({id: document} if document) || {})), id: document_action_config.fetch(:id, "#{document_action_config.key}Link"), - class: "btn", + class: "btn btn-outline", data: {}.merge(({blacklight_modal: "trigger"} if document_action_config.modal != false) || {}) %> diff --git a/app/views/bookmarks/_formless_document_action.html.erb b/app/views/bookmarks/_formless_document_action.html.erb index cc5166599c..da78dcc72e 100644 --- a/app/views/bookmarks/_formless_document_action.html.erb +++ b/app/views/bookmarks/_formless_document_action.html.erb @@ -17,6 +17,6 @@ Unless required by applicable law or agreed to in writing, software distributed <%= link_to document_action_label(document_action_config.key, document_action_config), document_action_path(document_action_config, (local_assigns.has_key?(:url_opts) ? url_opts : {}).merge(({id: document} if document) || {})), id: document_action_config.fetch(:id, "#{document_action_config.key}Link"), - class: "btn btn-default", + class: "btn btn-default btn-outline", method: :post, data: {}.merge(({blacklight_modal: "trigger"} if document_action_config.modal != false) || {}) %> From 501e3d4fa4699ecd1ad85fcdeb11292ee32ccc03 Mon Sep 17 00:00:00 2001 From: Mason Ballengee <68433277+masaball@users.noreply.github.com> Date: Fri, 2 Sep 2022 15:22:48 -0400 Subject: [PATCH 135/230] Update CORS to echo request origin (#4867) * Update CORS to echo request origin * Add testing for Access-Control-Allow-Origin header * Update cors_spec.rb --- config/application.rb | 2 +- spec/requests/cors_spec.rb | 27 +++++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 spec/requests/cors_spec.rb diff --git a/config/application.rb b/config/application.rb index 56156cf0f2..f6ff2c87b3 100644 --- a/config/application.rb +++ b/config/application.rb @@ -38,7 +38,7 @@ class Application < Rails::Application config.middleware.insert_before 0, Rack::Cors do allow do - origins '*' + origins { |source| true } resource '/media_objects/*/manifest*', headers: :any, methods: [:get] resource '/master_files/*/thumbnail', headers: :any, methods: [:get] resource '/master_files/*/transcript/*/*', headers: :any, methods: [:get] diff --git a/spec/requests/cors_spec.rb b/spec/requests/cors_spec.rb new file mode 100644 index 0000000000..c662c88d5a --- /dev/null +++ b/spec/requests/cors_spec.rb @@ -0,0 +1,27 @@ +# Copyright 2011-2022, The Trustees of Indiana University and Northwestern +# University. Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed +# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +# CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. +# --- END LICENSE_HEADER BLOCK --- + +require 'rails_helper.rb' + +describe 'CORS', type: :request do + let(:media_object) { FactoryBot.create(:published_media_object) } + let(:headers) { ['localhost', 'http://example.com', 'https://example.edu'] } + + it 'echoes the request origin in the CORS headers' do + headers.each do |header| + get "/media_objects/#{media_object.id}/manifest", headers: { 'HTTP_ORIGIN': header } + expect(response.headers['Access-Control-Allow-Origin']).to eq(header) + end + end +end From fb4965cffee2f184a9f034937362f3843172e916 Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Tue, 6 Sep 2022 16:39:29 -0400 Subject: [PATCH 136/230] Add LDAP group auths to API requests --- app/controllers/application_controller.rb | 7 +-- spec/requests/api_authentication_spec.rb | 54 +++++++++++++++++++++++ 2 files changed, 58 insertions(+), 3 deletions(-) create mode 100644 spec/requests/api_authentication_spec.rb diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 7970642e36..93da08c2b7 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,11 +1,11 @@ # Copyright 2011-2022, The Trustees of Indiana University and Northwestern # University. Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. -# +# # You may obtain a copy of the License at -# +# # http://www.apache.org/licenses/LICENSE-2.0 -# +# # Unless required by applicable law or agreed to in writing, software distributed # under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR # CONDITIONS OF ANY KIND, either express or implied. See the License for the @@ -101,6 +101,7 @@ def handle_api_request sign_in user, event: :authentication user_session[:json_api_login] = true user_session[:full_login] = false + user_session[:virtual_groups] = user.ldap_groups else render json: {errors: ["Permission denied."]}, status: 403 return diff --git a/spec/requests/api_authentication_spec.rb b/spec/requests/api_authentication_spec.rb new file mode 100644 index 0000000000..e50792b131 --- /dev/null +++ b/spec/requests/api_authentication_spec.rb @@ -0,0 +1,54 @@ +# Copyright 2011-2022, The Trustees of Indiana University and Northwestern +# University. Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed +# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +# CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. +# --- END LICENSE_HEADER BLOCK --- + +require 'rails_helper.rb' + +# These tests are for the #handle_api_request method in the ApplicationController +describe "API authentication", type: :request do + let(:user) { FactoryBot.create(:user) } + let!(:media_object) { FactoryBot.create(:published_media_object, visibility: 'private', read_groups: ['ldap_group_2']) } + let!(:unauthorized_media_object) { FactoryBot.create(:published_media_object, visibility: 'private') } + let(:ldap_groups) { ['ldap_group_1'] } + before do + ApiToken.create token: 'secret_token', username: user.username, email: user.email + allow_any_instance_of(User).to receive(:ldap_groups).and_return(ldap_groups) + end + it "sets the session information properly" do + get "/media_objects/#{media_object.id}.json", headers: { 'Avalon-Api-Key': 'secret_token' } + expect(@controller.user_session[:json_api_login]).to eq true + expect(@controller.user_session[:full_login]).to be false + expect(@controller.user_session[:virtual_groups]).to eq(['ldap_group_1']) + end + context 'without external groups' do + let(:ldap_groups) { [] } + it "does not allow user to access unauthorized media object" do + get "/media_objects/#{media_object.id}.json", headers: { 'Avalon-Api-Key': 'secret_token' } + expect(response.status).to be 401 + end + end + context 'with external groups' do + let(:ldap_groups) { ['ldap_group_2'] } + context '/media_objects/:id.json endpoint' do + it 'allows the user to access authorized media objects' do + get "/media_objects/#{media_object.id}.json", headers: { 'Avalon-Api-Key': 'secret_token' } + expect(response.status).to be 200 + expect(response.body).to include(media_object.id) + end + it 'does not allow user to access unauthorized media objects' do + get "/media_objects/#{unauthorized_media_object.id}.json", headers: { 'Avalon-Api-Key': 'secret_token' } + expect(response.status).to be 401 + end + end + end +end From 6861968d4ae6466d072ee2ab1058bbb82747f19f Mon Sep 17 00:00:00 2001 From: dananji Date: Tue, 13 Sep 2022 09:05:31 -0400 Subject: [PATCH 137/230] Fix overflow of encode records table --- app/views/encode_records/index.html.erb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/views/encode_records/index.html.erb b/app/views/encode_records/index.html.erb index 034720e987..7d3abe93d7 100644 --- a/app/views/encode_records/index.html.erb +++ b/app/views/encode_records/index.html.erb @@ -85,6 +85,7 @@ Unless required by applicable law or agreed to in writing, software distributed stateSave: true, processing: true, serverSide: true, + scrollX: true, dom:'<"dataTableToolsTop"Blf><"dataTableBody"t><"dataTableToolsBottom"ipr>', autoWidth:true, order: [[ 6, 'desc' ]], From cdd83941ac73a3fb6234c8ee5c022b73d318c7b6 Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Tue, 13 Sep 2022 16:13:05 -0400 Subject: [PATCH 138/230] Make embed login popup autoclose --- app/views/layouts/avalon.html.erb | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/app/views/layouts/avalon.html.erb b/app/views/layouts/avalon.html.erb index bee422c561..17135b46d2 100644 --- a/app/views/layouts/avalon.html.erb +++ b/app/views/layouts/avalon.html.erb @@ -20,7 +20,7 @@ Unless required by applicable law or agreed to in writing, software distributed <%= render_page_title %> - + /> /> @@ -37,7 +37,7 @@ Unless required by applicable law or agreed to in writing, software distributed <%= render "modules/google_analytics" %> - + >
      Skip to main content @@ -68,6 +68,16 @@ Unless required by applicable law or agreed to in writing, software distributed <%= render 'modules/footer' %>
      + <% if request.fullpath.include?('login_popup') %> + <% content_for :page_scripts do %> + + <% end %> + <% end %> +
      UserMedia objectReturn time Time remaining - <% if Checkout.checkouts(current_user.id).count > 1 or current_user.groups.include? 'administrator' %> + <% if Checkout.active_for_user(current_user.id).count > 1 || current_ability.is_administrator? %> <%= link_to 'Return All', main_app.checkouts_path , class: 'btn btn-primary btn-xs', method: :delete, data: { confirm: 'Are you sure?' } %> <% end %>
      <%= checkout.user.user_key %><%= link_to checkout.media_object.title, main_app.media_object_url(checkout.media_object) %>Return time Time remaining - <% if Checkout.active_for_user(current_user.id).count > 1 || current_ability.is_administrator? %> - <%= link_to 'Return All', main_app.checkouts_path , class: 'btn btn-primary btn-xs', method: :delete, data: { confirm: 'Are you sure?' } %> - <% end %> + Return All
      Return time Time remaining - Return All + <% if Checkout.active_for_user(current_user.id).count > 1 || current_ability.is_administrator? %> + <%= link_to 'Return All', main_app.return_all_checkouts_path , class: 'btn btn-primary btn-xs', method: :patch, data: { confirm: 'Are you sure you want to return all items?' } %> + <% end %>
      <%= distance_of_time_in_words(checkout.return_time - DateTime.current) %> <% if checkout.return_time > DateTime.current %> - <%= link_to 'Return', checkout, class: 'btn btn-danger btn-xs', method: :delete, data: { confirm: 'Are you sure?' } %> - <% else %> + <%= link_to 'Return', return_checkout_url(checkout), class: 'btn btn-danger btn-xs', method: :patch, data: { confirm: "Are you sure you want to return this item?" } %> + <% elsif checkout.return_time < DateTime.current && media_object.lending_status == 'available' %> <%= link_to 'Checkout', checkout, class: 'btn btn-primary btn-xs', method: :post %> + <% else %> + <%= 'Item Unavailable' %> <% end %>
      <% if checkout.return_time > DateTime.current %> <%= link_to 'Return', return_checkout_url(checkout), class: 'btn btn-danger btn-xs', method: :patch, data: { confirm: "Are you sure you want to return this item?" } %> - <% elsif checkout.return_time < DateTime.current && media_object.lending_status == 'available' %> - <%= link_to 'Checkout', checkout, class: 'btn btn-primary btn-xs', method: :post %> + <% elsif checkout.return_time < DateTime.current && checkout.media_object.lending_status == 'available' %> + <%= link_to 'Checkout', checkouts_url(params: { checkout: { media_object_id: checkout.media_object_id } }), class: 'btn btn-primary btn-xs', method: :post %> <% else %> <%= 'Item Unavailable' %> <% end %> From ac5d5d8aa2e6a4a67b9330a0093aa5ea5fabde2a Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Fri, 24 Jun 2022 17:01:28 -0400 Subject: [PATCH 030/230] WIP Testing --- .../checkouts/_inactive_checkout.html.erb | 2 +- spec/requests/checkouts_spec.rb | 73 ++++++++++++++++++- spec/routing/checkouts_routing_spec.rb | 4 +- 3 files changed, 72 insertions(+), 7 deletions(-) diff --git a/app/views/checkouts/_inactive_checkout.html.erb b/app/views/checkouts/_inactive_checkout.html.erb index beeaca29d1..bd5f2add61 100644 --- a/app/views/checkouts/_inactive_checkout.html.erb +++ b/app/views/checkouts/_inactive_checkout.html.erb @@ -15,6 +15,6 @@ Unless required by applicable law or agreed to in writing, software distributed %> <% # container for display inactive checkouts checkbox -%>
      - <%= check_box_tag :inactive_checkouts, autocomplete: 'off' %> + <%= check_box_tag :inactive_checkouts %> <%= label_tag :inactive_checkouts, 'Display Returned Items', class: 'font-weight-bold' %>
      diff --git a/spec/requests/checkouts_spec.rb b/spec/requests/checkouts_spec.rb index 2fa90fc599..0d516f10d1 100644 --- a/spec/requests/checkouts_spec.rb +++ b/spec/requests/checkouts_spec.rb @@ -34,9 +34,40 @@ describe "GET /index" do before { checkout } - it "renders a successful response" do - get checkouts_url - expect(response).to be_successful + context "html request" do + it "renders a successful response" do + get checkouts_url + expect(response).to be_successful + end + it "renders the index partial" do + get checkouts_url + expect(response).to render_template(:index) + end + end + + context "json request" do + it "renders a successful JSON response" do + get checkouts_url(format: :json) + expect(response).to be_successful + expect(response.content_type).to eq("application/json; charset=utf-8") + end + context "as a regular user" do + before { FactoryBot.create(:checkout) } + it "returns only the active user's checkouts" do + get checkouts_url(format: :json) + parsed_body = JSON.parse(response.body) + expect(parsed_body['data'].count).to eq(1) + end + end + context "as an admin user" do + let(:user) { FactoryBot.create(:admin) } + before { FactoryBot.create(:checkout) } + it "returns all checkouts" do + get checkouts_url(format: :json) + parsed_body = JSON.parse(response.body) + expect(parsed_body['data'].count).to eq(2) + end + end end end @@ -128,7 +159,7 @@ expect(checkout.return_time).to be <= DateTime.current end context "user is on the checkouts page" do - it "redirects to the checkouts list" do + it "redirects to the checkouts page" do patch return_checkout_url(checkout), headers: { "HTTP_REFERER" => checkouts_url } expect(response).to redirect_to(checkouts_url) end @@ -180,4 +211,38 @@ end end end + + describe "GET /display_returned" do + before :each do + FactoryBot.create_list(:checkout, 2) + FactoryBot.create(:checkout, user: user) + FactoryBot.create(:checkout, user: user, return_time: DateTime.current - 1.day) + end + context "as a regular user" do + it "renders a successful JSON response" do + get display_returned_checkouts_url(format: :json) + expect(response).to be_successful + expect(response.content_type).to eq("application/json; charset=utf-8") + end + it "returns user's active and inactive checkouts" do + get display_returned_checkouts_url(format: :json) + parsed_body = JSON.parse(response.body) + expect(parsed_body['data'].count).to eq(2) + end + end + + context "as an admin user" do + let (:user) { FactoryBot.create(:admin) } + it "renders a successful JSON response" do + get display_returned_checkouts_url(format: :json) + expect(response).to be_successful + expect(response.content_type).to eq("application/json; charset=utf-8") + end + it "returns all checkouts" do + get display_returned_checkouts_url(format: :json) + parsed_body = JSON.parse(response.body) + expect(parsed_body['data'].count).to eq(4) + end + end + end end diff --git a/spec/routing/checkouts_routing_spec.rb b/spec/routing/checkouts_routing_spec.rb index 68c6e7dbd9..5de2315d86 100644 --- a/spec/routing/checkouts_routing_spec.rb +++ b/spec/routing/checkouts_routing_spec.rb @@ -34,8 +34,8 @@ expect(patch: "/checkouts/1/return").to route_to("checkouts#return", id: "1") end - it "routes to #display_all" do - expect(get: "checkouts/display_returned").to route_to("checkouts#display_all") + it "routes to #display_returned" do + expect(get: "checkouts/display_returned").to route_to("checkouts#display_returned") end end end From bd76eb6e67d359de73c30364817b0491e549241b Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Mon, 27 Jun 2022 10:10:09 -0400 Subject: [PATCH 031/230] Fix checkout link on checkouts page and add tests --- app/controllers/checkouts_controller.rb | 25 ++++++----- spec/requests/checkouts_spec.rb | 57 +++++++++++++++++++------ 2 files changed, 55 insertions(+), 27 deletions(-) diff --git a/app/controllers/checkouts_controller.rb b/app/controllers/checkouts_controller.rb index 7655bd325c..0d34cacc92 100644 --- a/app/controllers/checkouts_controller.rb +++ b/app/controllers/checkouts_controller.rb @@ -7,19 +7,18 @@ class CheckoutsController < ApplicationController def index @checkouts - response = { - "data": @checkouts.collect do |checkout| - if current_ability.is_administrator? - index_array(checkout) - else - user_array(index_array(checkout)) - end - end - } - respond_to do |format| format.html { render :index } format.json do + response = { + "data": @checkouts.collect do |checkout| + if current_ability.is_administrator? + index_array(checkout) + else + user_array(index_array(checkout)) + end + end + } render json: response end end @@ -29,14 +28,14 @@ def index def show end - # POST /checkouts.json + # POST /checkouts or /checkouts.json def create @checkout = Checkout.new(user: current_user, media_object_id: checkout_params[:media_object_id]) respond_to do |format| # TODO: move this can? check into a checkout ability (can?(:create, @checkout)) if can?(:create, @checkout) && @checkout.save - format.html { redirect_to media_object_path(checkout_params[:media_object_id]), flash: { success: "Checkout was successfully created."} } + format.html { redirect_back fallback_location: checkouts_url, notice: "Item has been successfully checked out."} format.json { render :show, status: :created, location: @checkout } else format.json { render json: @checkout.errors, status: :unprocessable_entity } @@ -142,7 +141,7 @@ def index_array(checkout) if checkout.return_time > DateTime.current view_context.link_to('Return', return_checkout_url(checkout), class: 'btn btn-danger btn-xs', method: :patch, data: { confirm: "Are you sure you want to return this item?" }) elsif checkout.return_time < DateTime.current && checkout.media_object.lending_status == 'available' - view_context.link_to('Checkout', checkouts_url(params: { checkout: { media_object_id: checkout.media_object_id } }), class: 'btn btn-primary btn-xs', method: :post) + view_context.link_to('Checkout', checkouts_url(params: { checkout: { media_object_id: checkout.media_object_id }, format: :json }), class: 'btn btn-primary btn-xs', method: :post) else 'Item Unavailable' end diff --git a/spec/requests/checkouts_spec.rb b/spec/requests/checkouts_spec.rb index 0d516f10d1..432dea0ffc 100644 --- a/spec/requests/checkouts_spec.rb +++ b/spec/requests/checkouts_spec.rb @@ -79,29 +79,58 @@ end describe "POST /create" do - context "with valid parameters" do - it "creates a new Checkout" do - expect { + context "json request" do + context "with valid parameters" do + it "creates a new Checkout" do + expect { + post checkouts_url, params: { checkout: valid_attributes, format: :json } + }.to change(Checkout, :count).by(1) + end + + it "redirects to the created checkout" do post checkouts_url, params: { checkout: valid_attributes, format: :json } - }.to change(Checkout, :count).by(1) + expect(response).to be_created + end end - it "redirects to the created checkout" do - post checkouts_url, params: { checkout: valid_attributes, format: :json } - expect(response).to be_created + context "with invalid parameters" do + it "does not create a new Checkout" do + expect { + post checkouts_url, params: { checkout: invalid_attributes, format: :json } + }.to change(Checkout, :count).by(0) + end + + it "returns 404 because the media object cannot be found" do + post checkouts_url, params: { checkout: invalid_attributes, format: :json } + expect(response).to be_not_found + end end end - context "with invalid parameters" do - it "does not create a new Checkout" do + context "html request" do + it "creates a new checkout" do expect { - post checkouts_url, params: { checkout: invalid_attributes, format: :json } - }.to change(Checkout, :count).by(0) + post checkouts_url, params: { checkout: valid_attributes } + }.to change(Checkout, :count).by(1) end - it "returns 404 because the media object cannot be found" do - post checkouts_url, params: { checkout: invalid_attributes, format: :json } - expect(response).to be_not_found + context "user is on the checkouts page" do + it "redirects to the checkouts page" do + post checkouts_url, params: { checkout: valid_attributes }, headers: { "HTTP_REFERER" => checkouts_url } + expect(response).to redirect_to(checkouts_url) + end + end + context "user is on the item view page" do + it "redirects to the item view page" do + post checkouts_url, params: { checkout: valid_attributes }, headers: { "HTTP_REFERER" => media_object_url(checkout.media_object)} + expect(response).to redirect_to(media_object_url(checkout.media_object)) + end + end + context "the http referrer fails" do + it "redirects to the checkouts page" do + post checkouts_url, params: { checkout: valid_attributes } + expect(response).to redirect_to(checkouts_url) + end end end end From 468e1d001e81361fb2a82065ad7d8b036c33ffa8 Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Mon, 27 Jun 2022 14:23:07 -0400 Subject: [PATCH 032/230] Add feature test for checkouts page --- spec/features/checkouts_spec.rb | 44 +++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 spec/features/checkouts_spec.rb diff --git a/spec/features/checkouts_spec.rb b/spec/features/checkouts_spec.rb new file mode 100644 index 0000000000..32b8f23437 --- /dev/null +++ b/spec/features/checkouts_spec.rb @@ -0,0 +1,44 @@ +# Copyright 2011-2022, The Trustees of Indiana University and Northwestern +# University. Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed +# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +# CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. +# --- END LICENSE_HEADER BLOCK --- + +require 'rails_helper' + +describe "Checkouts", skip: "Datatables fails to load only in test" do + after { Warden.test_reset! } + before :each do + @user = FactoryBot.create(:user) + login_as @user, scope: :user + end + let(:active_checkout) { FactoryBot.create(:checkout, user: @user) } + let(:returned_checkout) { FactoryBot.create(:checkout, user: @user, return_time: DateTime.current - 1.day) } + + it "displays a table of active checkouts" do + visit('/checkouts') + expect(page).to have_content('Media object') + expect(page).to have_content('Checkout time') + expect(page).to have_content('Return time') + expect(page).to have_content('Time remaining') + expect(page).to have_link('Return All') + expect(page).to have_link('Return') + expect(page).to have_content(active_checkout.media_object.title) + expect(page).not_to have_content(returned_checkout.media_object.title) + end + + it "displays active and inactive checkouts when checkbox is checked", js: true do + visit('/checkouts') + check('inactive_checkouts') + expect(page).to have_content(active_checkout.media_object.title) + expect(page).to have_content(returned_checkout.media_object.title) + end +end From ed38db71ef77c29bc45f29eb3303035e7b39f9a0 Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Mon, 27 Jun 2022 14:40:28 -0400 Subject: [PATCH 033/230] Fix potential merge conflict and update gemfile --- app/controllers/checkouts_controller.rb | 2 +- spec/requests/checkouts_spec.rb | 21 +++------------------ 2 files changed, 4 insertions(+), 19 deletions(-) diff --git a/app/controllers/checkouts_controller.rb b/app/controllers/checkouts_controller.rb index 0d34cacc92..83a88530b4 100644 --- a/app/controllers/checkouts_controller.rb +++ b/app/controllers/checkouts_controller.rb @@ -35,7 +35,7 @@ def create respond_to do |format| # TODO: move this can? check into a checkout ability (can?(:create, @checkout)) if can?(:create, @checkout) && @checkout.save - format.html { redirect_back fallback_location: checkouts_url, notice: "Item has been successfully checked out."} + format.html { redirect_to media_object_path(checkout_params[:media_object_id]), flash: { "Checkout was successfully created."} } format.json { render :show, status: :created, location: @checkout } else format.json { render json: @checkout.errors, status: :unprocessable_entity } diff --git a/spec/requests/checkouts_spec.rb b/spec/requests/checkouts_spec.rb index 432dea0ffc..43cf69b6f9 100644 --- a/spec/requests/checkouts_spec.rb +++ b/spec/requests/checkouts_spec.rb @@ -113,24 +113,9 @@ post checkouts_url, params: { checkout: valid_attributes } }.to change(Checkout, :count).by(1) end - - context "user is on the checkouts page" do - it "redirects to the checkouts page" do - post checkouts_url, params: { checkout: valid_attributes }, headers: { "HTTP_REFERER" => checkouts_url } - expect(response).to redirect_to(checkouts_url) - end - end - context "user is on the item view page" do - it "redirects to the item view page" do - post checkouts_url, params: { checkout: valid_attributes }, headers: { "HTTP_REFERER" => media_object_url(checkout.media_object)} - expect(response).to redirect_to(media_object_url(checkout.media_object)) - end - end - context "the http referrer fails" do - it "redirects to the checkouts page" do - post checkouts_url, params: { checkout: valid_attributes } - expect(response).to redirect_to(checkouts_url) - end + it "redirects to the media_object page" do + post checkouts_url, params: { checkout: valid_attributes } + expect(response).to redirect_to(media_object_url(checkout.media_object)) end end end From 85fc795a2f73be4f13cec9b8d9b8d33cb4a30326 Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Mon, 27 Jun 2022 14:43:10 -0400 Subject: [PATCH 034/230] Add missing word --- app/controllers/checkouts_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/checkouts_controller.rb b/app/controllers/checkouts_controller.rb index 83a88530b4..6a9b6d0b22 100644 --- a/app/controllers/checkouts_controller.rb +++ b/app/controllers/checkouts_controller.rb @@ -35,7 +35,7 @@ def create respond_to do |format| # TODO: move this can? check into a checkout ability (can?(:create, @checkout)) if can?(:create, @checkout) && @checkout.save - format.html { redirect_to media_object_path(checkout_params[:media_object_id]), flash: { "Checkout was successfully created."} } + format.html { redirect_to media_object_path(checkout_params[:media_object_id]), flash: { success: "Checkout was successfully created."} } format.json { render :show, status: :created, location: @checkout } else format.json { render json: @checkout.errors, status: :unprocessable_entity } From 83e69946e54ada5e14f6365cd9a10b44ddfde9e1 Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Mon, 27 Jun 2022 15:17:52 -0400 Subject: [PATCH 035/230] Rename checkouts feature test --- spec/features/capybara_checkouts_spec.rb | 44 ++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 spec/features/capybara_checkouts_spec.rb diff --git a/spec/features/capybara_checkouts_spec.rb b/spec/features/capybara_checkouts_spec.rb new file mode 100644 index 0000000000..32b8f23437 --- /dev/null +++ b/spec/features/capybara_checkouts_spec.rb @@ -0,0 +1,44 @@ +# Copyright 2011-2022, The Trustees of Indiana University and Northwestern +# University. Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed +# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +# CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. +# --- END LICENSE_HEADER BLOCK --- + +require 'rails_helper' + +describe "Checkouts", skip: "Datatables fails to load only in test" do + after { Warden.test_reset! } + before :each do + @user = FactoryBot.create(:user) + login_as @user, scope: :user + end + let(:active_checkout) { FactoryBot.create(:checkout, user: @user) } + let(:returned_checkout) { FactoryBot.create(:checkout, user: @user, return_time: DateTime.current - 1.day) } + + it "displays a table of active checkouts" do + visit('/checkouts') + expect(page).to have_content('Media object') + expect(page).to have_content('Checkout time') + expect(page).to have_content('Return time') + expect(page).to have_content('Time remaining') + expect(page).to have_link('Return All') + expect(page).to have_link('Return') + expect(page).to have_content(active_checkout.media_object.title) + expect(page).not_to have_content(returned_checkout.media_object.title) + end + + it "displays active and inactive checkouts when checkbox is checked", js: true do + visit('/checkouts') + check('inactive_checkouts') + expect(page).to have_content(active_checkout.media_object.title) + expect(page).to have_content(returned_checkout.media_object.title) + end +end From 49b7fce0ea25e2fc4d38ec22640b22c33020538d Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Mon, 27 Jun 2022 15:23:41 -0400 Subject: [PATCH 036/230] Remove old checkout feature test --- spec/features/capybara_checkouts_spec.rb | 8 ++--- spec/features/checkouts_spec.rb | 44 ------------------------ 2 files changed, 4 insertions(+), 48 deletions(-) delete mode 100644 spec/features/checkouts_spec.rb diff --git a/spec/features/capybara_checkouts_spec.rb b/spec/features/capybara_checkouts_spec.rb index 32b8f23437..6734a21e78 100644 --- a/spec/features/capybara_checkouts_spec.rb +++ b/spec/features/capybara_checkouts_spec.rb @@ -31,14 +31,14 @@ expect(page).to have_content('Time remaining') expect(page).to have_link('Return All') expect(page).to have_link('Return') - expect(page).to have_content(active_checkout.media_object.title) - expect(page).not_to have_content(returned_checkout.media_object.title) + expect(page).to have_link(active_checkout.media_object.title) + expect(page).not_to have_link(returned_checkout.media_object.title) end it "displays active and inactive checkouts when checkbox is checked", js: true do visit('/checkouts') check('inactive_checkouts') - expect(page).to have_content(active_checkout.media_object.title) - expect(page).to have_content(returned_checkout.media_object.title) + expect(page).to have_link(active_checkout.media_object.title) + expect(page).to have_link(returned_checkout.media_object.title) end end diff --git a/spec/features/checkouts_spec.rb b/spec/features/checkouts_spec.rb deleted file mode 100644 index 32b8f23437..0000000000 --- a/spec/features/checkouts_spec.rb +++ /dev/null @@ -1,44 +0,0 @@ -# Copyright 2011-2022, The Trustees of Indiana University and Northwestern -# University. Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software distributed -# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR -# CONDITIONS OF ANY KIND, either express or implied. See the License for the -# specific language governing permissions and limitations under the License. -# --- END LICENSE_HEADER BLOCK --- - -require 'rails_helper' - -describe "Checkouts", skip: "Datatables fails to load only in test" do - after { Warden.test_reset! } - before :each do - @user = FactoryBot.create(:user) - login_as @user, scope: :user - end - let(:active_checkout) { FactoryBot.create(:checkout, user: @user) } - let(:returned_checkout) { FactoryBot.create(:checkout, user: @user, return_time: DateTime.current - 1.day) } - - it "displays a table of active checkouts" do - visit('/checkouts') - expect(page).to have_content('Media object') - expect(page).to have_content('Checkout time') - expect(page).to have_content('Return time') - expect(page).to have_content('Time remaining') - expect(page).to have_link('Return All') - expect(page).to have_link('Return') - expect(page).to have_content(active_checkout.media_object.title) - expect(page).not_to have_content(returned_checkout.media_object.title) - end - - it "displays active and inactive checkouts when checkbox is checked", js: true do - visit('/checkouts') - check('inactive_checkouts') - expect(page).to have_content(active_checkout.media_object.title) - expect(page).to have_content(returned_checkout.media_object.title) - end -end From fc8077cbc5939884368f67507489819baa81f18e Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Tue, 28 Jun 2022 15:44:42 -0400 Subject: [PATCH 037/230] Remove unneeded route and refactor Co-authored-by: Chris Colvard --- .../javascripts/display_returned_items.js | 2 +- app/controllers/checkouts_controller.rb | 83 +++++++++---------- app/models/ability.rb | 1 - config/routes.rb | 1 - spec/requests/checkouts_spec.rb | 66 +++++++-------- spec/routing/checkouts_routing_spec.rb | 4 - 6 files changed, 71 insertions(+), 86 deletions(-) diff --git a/app/assets/javascripts/display_returned_items.js b/app/assets/javascripts/display_returned_items.js index 44e3dacbed..8429370505 100644 --- a/app/assets/javascripts/display_returned_items.js +++ b/app/assets/javascripts/display_returned_items.js @@ -18,7 +18,7 @@ $('#inactive_checkouts').on('change', function() { var table = $('#checkouts-table').DataTable(); if (this.checked) { - table.ajax.url('/checkouts/display_returned').load(); + table.ajax.url('/checkouts.json?display_returned=true').load(); } else { table.ajax.url('/checkouts.json').load(); } diff --git a/app/controllers/checkouts_controller.rb b/app/controllers/checkouts_controller.rb index 6a9b6d0b22..010d8e2eb9 100644 --- a/app/controllers/checkouts_controller.rb +++ b/app/controllers/checkouts_controller.rb @@ -13,9 +13,9 @@ def index response = { "data": @checkouts.collect do |checkout| if current_ability.is_administrator? - index_array(checkout) + admin_array(checkout) else - user_array(index_array(checkout)) + user_array(checkout) end end } @@ -77,31 +77,6 @@ def return_all end end - # GET /checkouts/display_returned.json - def display_returned - if current_ability.is_administrator? - @checkouts = @checkouts.or(Checkout.all.where("return_time <= now()")) - else - @checkouts = @checkouts.or(Checkout.returned_for_user(current_user.id)) - end - - response = { - "data": @checkouts.collect do |checkout| - if current_ability.is_administrator? - index_array(checkout) - else - user_array(index_array(checkout)) - end - end - } - - respond_to do |format| - format.json do - render json: response - end - end - end - # DELETE /checkouts/1 or /checkouts/1.json def destroy @checkout.destroy @@ -120,6 +95,18 @@ def set_checkout end def set_checkouts + unless params[:display_returned] == 'true' + @checkouts = set_active_checkouts + else + @checkouts = if current_ability.is_administrator? + set_active_checkouts.or(Checkout.all.where("return_time <= now()")) + else + set_active_checkouts.or(Checkout.returned_for_user(current_user.id)) + end + end + end + + def set_active_checkouts @checkouts = if current_ability.is_administrator? Checkout.all.where("return_time > now()") else @@ -127,34 +114,40 @@ def set_checkouts end end - def index_array(checkout) + def admin_array(checkout) + [checkout.user.user_key] + user_array(checkout) + end + + def user_array(checkout) [ - checkout.user.user_key, view_context.link_to(checkout.media_object.title, main_app.media_object_url(checkout.media_object)), checkout.checkout_time.to_s(:long_ordinal), checkout.return_time.to_s(:long_ordinal), - if checkout.return_time > DateTime.current - view_context.distance_of_time_in_words(checkout.return_time - DateTime.current) - else - "-" - end, - if checkout.return_time > DateTime.current - view_context.link_to('Return', return_checkout_url(checkout), class: 'btn btn-danger btn-xs', method: :patch, data: { confirm: "Are you sure you want to return this item?" }) - elsif checkout.return_time < DateTime.current && checkout.media_object.lending_status == 'available' - view_context.link_to('Checkout', checkouts_url(params: { checkout: { media_object_id: checkout.media_object_id }, format: :json }), class: 'btn btn-primary btn-xs', method: :post) - else - 'Item Unavailable' - end + time_remaining(checkout), + checkout_actions(checkout) ] end - def user_array(array) - array.shift - array + def time_remaining(checkout) + if checkout.return_time > DateTime.current + view_context.distance_of_time_in_words(checkout.return_time - DateTime.current) + else + "-" + end + end + + def checkout_actions(checkout) + if checkout.return_time > DateTime.current + view_context.link_to('Return', return_checkout_url(checkout), class: 'btn btn-danger btn-xs', method: :patch, data: { confirm: "Are you sure you want to return this item?" }) + elsif checkout.return_time < DateTime.current && checkout.media_object.lending_status == 'available' + view_context.link_to('Checkout', checkouts_url(params: { checkout: { media_object_id: checkout.media_object_id }, format: :json }), class: 'btn btn-primary btn-xs', method: :post) + else + 'Item Unavailable' + end end # Only allow a list of trusted parameters through. def checkout_params - params.require(:checkout).permit(:media_object_id, :return_time) + params.require(:checkout).permit(:media_object_id, :return_time, :display_returned) end end diff --git a/app/models/ability.rb b/app/models/ability.rb index e981c9b418..871c0c1a17 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -228,7 +228,6 @@ def checkout_permissions end can :return, Checkout, user: @user can :return_all, Checkout, user: @user - can :display_returned, Checkout, user: @user can :read, Checkout, user: @user can :update, Checkout, user: @user can :destroy, Checkout, user: @user diff --git a/config/routes.rb b/config/routes.rb index 4312a852a9..3a4a6596c1 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -29,7 +29,6 @@ resources :checkouts, only: [:index, :create, :show, :update, :destroy] do collection do patch :return_all - get :display_returned end member do diff --git a/spec/requests/checkouts_spec.rb b/spec/requests/checkouts_spec.rb index 43cf69b6f9..eb66611ac2 100644 --- a/spec/requests/checkouts_spec.rb +++ b/spec/requests/checkouts_spec.rb @@ -68,6 +68,38 @@ expect(parsed_body['data'].count).to eq(2) end end + context "with display_returned param" do + before :each do + FactoryBot.create_list(:checkout, 2) + FactoryBot.create(:checkout, user: user, return_time: DateTime.current - 1.day) + end + context "as a regular user" do + it "renders a successful JSON response" do + get checkouts_url(format: :json, params: { display_returned: true } ) + expect(response).to be_successful + expect(response.content_type).to eq("application/json; charset=utf-8") + end + it "returns user's active and inactive checkouts" do + get checkouts_url(format: :json, params: { display_returned: true } ) + parsed_body = JSON.parse(response.body) + expect(parsed_body['data'].count).to eq(2) + end + end + + context "as an admin user" do + let (:user) { FactoryBot.create(:admin) } + it "renders a successful JSON response" do + get checkouts_url(format: :json, params: { display_returned: true } ) + expect(response).to be_successful + expect(response.content_type).to eq("application/json; charset=utf-8") + end + it "returns all checkouts" do + get checkouts_url(format: :json, params: { display_returned: true } ) + parsed_body = JSON.parse(response.body) + expect(parsed_body['data'].count).to eq(4) + end + end + end end end @@ -225,38 +257,4 @@ end end end - - describe "GET /display_returned" do - before :each do - FactoryBot.create_list(:checkout, 2) - FactoryBot.create(:checkout, user: user) - FactoryBot.create(:checkout, user: user, return_time: DateTime.current - 1.day) - end - context "as a regular user" do - it "renders a successful JSON response" do - get display_returned_checkouts_url(format: :json) - expect(response).to be_successful - expect(response.content_type).to eq("application/json; charset=utf-8") - end - it "returns user's active and inactive checkouts" do - get display_returned_checkouts_url(format: :json) - parsed_body = JSON.parse(response.body) - expect(parsed_body['data'].count).to eq(2) - end - end - - context "as an admin user" do - let (:user) { FactoryBot.create(:admin) } - it "renders a successful JSON response" do - get display_returned_checkouts_url(format: :json) - expect(response).to be_successful - expect(response.content_type).to eq("application/json; charset=utf-8") - end - it "returns all checkouts" do - get display_returned_checkouts_url(format: :json) - parsed_body = JSON.parse(response.body) - expect(parsed_body['data'].count).to eq(4) - end - end - end end diff --git a/spec/routing/checkouts_routing_spec.rb b/spec/routing/checkouts_routing_spec.rb index 5de2315d86..31860952cb 100644 --- a/spec/routing/checkouts_routing_spec.rb +++ b/spec/routing/checkouts_routing_spec.rb @@ -33,9 +33,5 @@ it "routes to #return" do expect(patch: "/checkouts/1/return").to route_to("checkouts#return", id: "1") end - - it "routes to #display_returned" do - expect(get: "checkouts/display_returned").to route_to("checkouts#display_returned") - end end end From 379431bdffa2308f6beb874b98189a1c3e06747a Mon Sep 17 00:00:00 2001 From: Mason Ballengee <68433277+masaball@users.noreply.github.com> Date: Wed, 29 Jun 2022 13:55:17 -0400 Subject: [PATCH 038/230] Update spec/requests/checkouts_spec.rb Co-authored-by: Chris Colvard --- spec/requests/checkouts_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/requests/checkouts_spec.rb b/spec/requests/checkouts_spec.rb index eb66611ac2..c9eab67b6a 100644 --- a/spec/requests/checkouts_spec.rb +++ b/spec/requests/checkouts_spec.rb @@ -119,7 +119,7 @@ }.to change(Checkout, :count).by(1) end - it "redirects to the created checkout" do + it "returns created status" do post checkouts_url, params: { checkout: valid_attributes, format: :json } expect(response).to be_created end From 7b1b9e1bbb1a5ccfc270ee752ae8c22d1b94d3d3 Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Tue, 5 Jul 2022 11:48:50 -0400 Subject: [PATCH 039/230] Add lending_period to MediaObject model --- app/models/media_object.rb | 9 +++++++++ spec/models/media_object_spec.rb | 18 ++++++++++++++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/app/models/media_object.rb b/app/models/media_object.rb index bceb77e1e5..7dc2531f95 100644 --- a/app/models/media_object.rb +++ b/app/models/media_object.rb @@ -38,6 +38,8 @@ class MediaObject < ActiveFedora::Base after_save :update_dependent_permalinks_job, if: Proc.new { |mo| mo.persisted? && mo.published? } after_save :remove_bookmarks + after_initialize :set_lending_period + # Call custom validation methods to ensure that required fields are present and # that preferred controlled vocabulary standards are used @@ -111,6 +113,9 @@ def validate_date(date_field) property :comment, predicate: ::RDF::Vocab::EBUCore.comments, multiple: true do |index| index.as :stored_searchable end + property :lending_period, predicate: ::RDF::Vocab::SCHEMA.eligibleDuration, multiple: false do |index| + index.as :stored_sortable + end ordered_aggregation :master_files, class_name: 'MasterFile', through: :list_source # ordered_aggregation gives you accessors media_obj.master_files and media_obj.ordered_master_files @@ -371,6 +376,10 @@ def lending_status Checkout.active_for_media_object(id).any? ? "checked_out" : "available" end + def set_lending_period + self.lending_period ||= ActiveSupport::Duration.parse(Settings.controlled_digital_lending.default_lending_period).to_s + end + def current_checkout(user_id) checkouts = Checkout.active_for_media_object(id) checkouts.select{ |ch| ch.user_id == user_id }.first diff --git a/spec/models/media_object_spec.rb b/spec/models/media_object_spec.rb index e1c5e5229f..0447d4fcf6 100644 --- a/spec/models/media_object_spec.rb +++ b/spec/models/media_object_spec.rb @@ -17,7 +17,7 @@ describe MediaObject do let(:media_object) { FactoryBot.create(:media_object) } - + it 'assigns a noid id' do media_object = MediaObject.new expect { media_object.assign_id! }.to change { media_object.id }.from(nil).to(String) @@ -929,7 +929,7 @@ describe '#merge!' do let(:media_objects) { [] } - + before do 2.times { media_objects << FactoryBot.create(:media_object, :with_master_file) } end @@ -1020,4 +1020,18 @@ end end end + + describe 'lending_period' do + context 'a custom lending period has not been set' do + it 'is equal to the default period in the settings.yml' do + expect(media_object.lending_period).to eq ActiveSupport::Duration.parse(Settings.controlled_digital_lending.default_lending_period).to_s + end + end + context 'a custom lending period has been set' do + let(:media_object) { FactoryBot.create(:media_object, lending_period: "1 day")} + it 'is equal to the custom lending period' do + expect(media_object.lending_period).to eq "1 day" + end + end + end end From 9f702010db14b0b0e3c33215b76e20c7b8c2f452 Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Tue, 5 Jul 2022 11:51:00 -0400 Subject: [PATCH 040/230] Add lending period to Media Object edit page --- app/views/media_objects/_resource_description.html.erb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/views/media_objects/_resource_description.html.erb b/app/views/media_objects/_resource_description.html.erb index b070a900ea..a65c13673f 100644 --- a/app/views/media_objects/_resource_description.html.erb +++ b/app/views/media_objects/_resource_description.html.erb @@ -100,6 +100,9 @@ Unless required by applicable law or agreed to in writing, software distributed <%= render partial: 'text_area', locals: {form: form, field: :terms_of_use, options: {display_label: 'Terms of Use'}} %> + <%= render partial: 'text_field', + locals: {form: form, field: :lending_period, + options: {display_label: 'Lending Period'}} %> <%= render partial: 'text_field', locals: {form: form, field: :other_identifier, options: {display_label: 'Other Identifier(s)', From 60b3bdf694545215e7b578c10d5e557d6e15b4a4 Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Tue, 5 Jul 2022 17:30:56 -0400 Subject: [PATCH 041/230] WIP Add lending period to views --- app/helpers/media_objects_helper.rb | 4 ++++ app/models/media_object.rb | 23 ++++++++++++++++++- .../media_objects/_metadata_display.html.erb | 3 ++- app/views/media_objects/_text_field.html.erb | 2 ++ spec/models/media_object_spec.rb | 4 ++-- 5 files changed, 32 insertions(+), 4 deletions(-) diff --git a/app/helpers/media_objects_helper.rb b/app/helpers/media_objects_helper.rb index c435a20b73..34ba8584af 100644 --- a/app/helpers/media_objects_helper.rb +++ b/app/helpers/media_objects_helper.rb @@ -114,6 +114,10 @@ def display_rights_statement media_object content_tag(:dt, 'Rights Statement') + content_tag(:dd) { link } end + def display_lending_period media_object + ActiveSupport::Duration.build(media_object.lending_period).parts.map{ |k, v| "#{v} #{k}" }.join(' ') + end + def current_quality stream_info available_qualities = Array(stream_info[:stream_flash]).collect {|s| s[:quality]} available_qualities += Array(stream_info[:stream_hls]).collect {|s| s[:quality]} diff --git a/app/models/media_object.rb b/app/models/media_object.rb index 7dc2531f95..44ad1ec7d3 100644 --- a/app/models/media_object.rb +++ b/app/models/media_object.rb @@ -377,7 +377,11 @@ def lending_status end def set_lending_period - self.lending_period ||= ActiveSupport::Duration.parse(Settings.controlled_digital_lending.default_lending_period).to_s + build_iso8601_duration + unless self.lending_period.nil? + self.lending_period = ActiveSupport::Duration.parse(@lend_period).to_i + end + self.lending_period ||= ActiveSupport::Duration.parse(Settings.controlled_digital_lending.default_lending_period).to_i end def current_checkout(user_id) @@ -399,4 +403,21 @@ def collect_ips_for_index ip_strings ips.flatten.compact.uniq || [] end + def build_iso8601_duration + @lend_period = self.lending_period.dup + @lend_period = @lend_period.gsub(/\s+[Dd]ays?/, 'D').gsub(/\s+[Hh]ours?/, 'H').gsub(/,?\s+/, 'T') + + if @lend_period.match?(/D/) + unless @lend_period.include? 'P' + @lend_period.prepend('P') + end + else + unless @lend_period.include? 'PT' + @lend_period.prepend('PT') + end + end + + @lend_period + end + end diff --git a/app/views/media_objects/_metadata_display.html.erb b/app/views/media_objects/_metadata_display.html.erb index da357960d5..e9ae62b15d 100644 --- a/app/views/media_objects/_metadata_display.html.erb +++ b/app/views/media_objects/_metadata_display.html.erb @@ -42,6 +42,7 @@ Unless required by applicable law or agreed to in writing, software distributed <%= display_metadata('Language', display_language(@media_object)) %> <%= display_rights_statement(@media_object) %> <%= display_metadata('Terms of Use', @media_object.terms_of_use) %> + <%= display_metadata('Lending Period', display_lending_period(@media_object)) %> <%= display_metadata('Physical Description', @media_object.physical_description) %> <%= display_metadata('Related Item', display_related_item(@media_object)) %> <%= display_metadata('Notes', display_notes(@media_object)) %> @@ -87,4 +88,4 @@ Unless required by applicable law or agreed to in writing, software distributed <% end %> - \ No newline at end of file + diff --git a/app/views/media_objects/_text_field.html.erb b/app/views/media_objects/_text_field.html.erb index c517188370..1e84ff0062 100644 --- a/app/views/media_objects/_text_field.html.erb +++ b/app/views/media_objects/_text_field.html.erb @@ -39,6 +39,8 @@ Unless required by applicable law or agreed to in writing, software distributed <% if options[:dropdown_field] %> <%= render partial: "multipart_dropdown_field", locals: {selected_value: value.present? ? value[options[:secondary_hash_key]] : nil, options: options} %> <% value = value.present? ? value[options[:primary_hash_key]] : "" %> + <% elsif fieldname == "media_object[lending_period]" %> + <% value = ActiveSupport::Duration.build(value).parts.map{ |k, v| "#{v} #{k}" }.join(' ') %> <% end %> <%= text_field_tag fieldname, value || '', class: (@media_object.errors[field].any? ? 'form-control is-invalid' : 'form-control') %> <% end %> diff --git a/spec/models/media_object_spec.rb b/spec/models/media_object_spec.rb index 0447d4fcf6..94679484f8 100644 --- a/spec/models/media_object_spec.rb +++ b/spec/models/media_object_spec.rb @@ -1024,13 +1024,13 @@ describe 'lending_period' do context 'a custom lending period has not been set' do it 'is equal to the default period in the settings.yml' do - expect(media_object.lending_period).to eq ActiveSupport::Duration.parse(Settings.controlled_digital_lending.default_lending_period).to_s + expect(media_object.lending_period).to eq ActiveSupport::Duration.parse(Settings.controlled_digital_lending.default_lending_period).to_i end end context 'a custom lending period has been set' do let(:media_object) { FactoryBot.create(:media_object, lending_period: "1 day")} it 'is equal to the custom lending period' do - expect(media_object.lending_period).to eq "1 day" + expect(media_object.lending_period).to eq 86400 end end end From b64dc2050a3a89a6a186ece440f5b4fbd9b5dca4 Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Wed, 6 Jul 2022 17:12:52 -0400 Subject: [PATCH 042/230] Fix checkouts --- app/models/checkout.rb | 4 +++- spec/requests/checkouts_spec.rb | 13 +++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/app/models/checkout.rb b/app/models/checkout.rb index 18a21ba770..0ff8752095 100644 --- a/app/models/checkout.rb +++ b/app/models/checkout.rb @@ -21,7 +21,9 @@ def set_checkout_return_times! end def duration - # duration = media_object.lending_period + if !media_object_id.nil? + duration = media_object.lending_period + end duration ||= ActiveSupport::Duration.parse(Settings.controlled_digital_lending.default_lending_period) duration end diff --git a/spec/requests/checkouts_spec.rb b/spec/requests/checkouts_spec.rb index c9eab67b6a..55b06164ed 100644 --- a/spec/requests/checkouts_spec.rb +++ b/spec/requests/checkouts_spec.rb @@ -149,6 +149,19 @@ post checkouts_url, params: { checkout: valid_attributes } expect(response).to redirect_to(media_object_url(checkout.media_object)) end + + context "non-default lending period" do + let(:media_object) { FactoryBot.create(:published_media_object, lending_period: "1 day", visibility: 'public') } + it "creates a new checkout" do + expect{ + post checkouts_url, params: { checkout: valid_attributes } + }.to change(Checkout, :count).by(1) + end + it "sets the return time based on the given lending period" do + post checkouts_url, params: { checkout: valid_attributes } + expect(Checkout.find_by(media_object_id: media_object.id).return_time).to eq DateTime.current + 1.day + end + end end end From 12bdae84597e3f35956f1acbb69005466fe34ff3 Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Wed, 6 Jul 2022 17:13:55 -0400 Subject: [PATCH 043/230] Refactor lending period builder --- app/models/media_object.rb | 27 +++++++++++++++++--- config/locales/en.yml | 10 ++++++-- spec/helpers/media_objects_helper_spec.rb | 30 +++++++++++++++++++++++ spec/models/media_object_spec.rb | 21 +++++++++++++--- 4 files changed, 79 insertions(+), 9 deletions(-) diff --git a/app/models/media_object.rb b/app/models/media_object.rb index 44ad1ec7d3..d4a6049a94 100644 --- a/app/models/media_object.rb +++ b/app/models/media_object.rb @@ -377,8 +377,8 @@ def lending_status end def set_lending_period - build_iso8601_duration - unless self.lending_period.nil? + if (!self.lending_period.nil? && !(self.lending_period.is_a? Integer)) + build_lend_period self.lending_period = ActiveSupport::Duration.parse(@lend_period).to_i end self.lending_period ||= ActiveSupport::Duration.parse(Settings.controlled_digital_lending.default_lending_period).to_i @@ -403,10 +403,29 @@ def collect_ips_for_index ip_strings ips.flatten.compact.uniq || [] end - def build_iso8601_duration + def build_lend_period @lend_period = self.lending_period.dup - @lend_period = @lend_period.gsub(/\s+[Dd]ays?/, 'D').gsub(/\s+[Hh]ours?/, 'H').gsub(/,?\s+/, 'T') + replacement = { + /\s+days?/i => 'D', + /\s+hours?/i => 'H', + /,?\s+/ => 'T' + } + + rules = replacement.collect{ |k, v| k } + + matcher = Regexp.union(rules) + + @lend_period = @lend_period.gsub(matcher) do |match| + replacement.detect{ |k, v| k =~ match }[1] + end + + build_iso8601_duration(@lend_period) + + @lend_period + end + + def build_iso8601_duration(lend_period) if @lend_period.match?(/D/) unless @lend_period.include? 'P' @lend_period.prepend('P') diff --git a/config/locales/en.yml b/config/locales/en.yml index 2bc0218adc..2d0a0615bd 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -25,7 +25,7 @@ en: cdl: checkout_message: "

      To enable streaming please check out resource. This content is available to be checked out.

      " not_available_message: "

      This resource is not available to be checked out at the moment. Please check again later.

      " - expire: + expire: heading: "Check Out Expired" message: "Your lending period has been expired. Please return the item, and check for availability later." metadata_tip: @@ -121,6 +121,12 @@ en: Other Identifier should be used for an external record identifier that can connect the Avalon item to a catalog record or other record for the original item. This identifier differs from Bibliographic Identifier in that it is not used to retrieve a record from another system. Type specifies the type of external record identifier. table_of_contents: | Table of Contents is used to provide the titles of separate works or parts of a resource. Information provided may also contain statements of responsibility or other sequential designations. Titles of separate works or parts should be separated by ' -- ' (space-hyphen-hyphen-space). + lending_period: | + Lending Period is the length of time that an item can be checked out for. + Accepted formats are ISO8601 duration (ex. P1DT12H), the time measured in seconds (ex. 129600), + or a human-readable string using numerals without punctuation or conjunctions (ex. 1 day 12 hours). + Values entered as strings must be entered as whole days and/or hours. For example, one month would be entered as + '30 days' and 1.5 days would be entered as '1 day 12 hours' or '36 hours'. file_upload_tip: title: | @@ -252,7 +258,7 @@ en: timeline: title: "Title" description: "Description" - user: + user: login: "Username or email" iiif: diff --git a/spec/helpers/media_objects_helper_spec.rb b/spec/helpers/media_objects_helper_spec.rb index 79dcb7c616..0ddeb591a4 100644 --- a/spec/helpers/media_objects_helper_spec.rb +++ b/spec/helpers/media_objects_helper_spec.rb @@ -102,4 +102,34 @@ end end end + + describe '#display_lending_period' do + context 'when lending period is measured in days' do + let(:media_object) { instance_double("MediaObject", lending_period: 172800) } + + subject { helper.display_lending_period(media_object) } + + it 'returns the lending period as a human readable string' do + expect(subject).to eq("2 days") + end + end + context 'when lending period is measured in hours' do + let(:media_object) { instance_double("MediaObject", lending_period: 7200) } + + subject { helper.display_lending_period(media_object) } + + it 'returns the lending period as a human readable string' do + expect(subject).to eq("2 hours") + end + end + context 'when lending period is measured in days and hours' do + let(:media_object) { instance_double("MediaObject", lending_period: 129600) } + + subject { helper.display_lending_period(media_object) } + + it 'returns the lending period as a human readable string' do + expect(subject).to eq("1 days 12 hours") + end + end + end end diff --git a/spec/models/media_object_spec.rb b/spec/models/media_object_spec.rb index 94679484f8..f73685afa2 100644 --- a/spec/models/media_object_spec.rb +++ b/spec/models/media_object_spec.rb @@ -1021,15 +1021,30 @@ end end - describe 'lending_period' do + describe 'set_lending_period' do context 'a custom lending period has not been set' do it 'is equal to the default period in the settings.yml' do expect(media_object.lending_period).to eq ActiveSupport::Duration.parse(Settings.controlled_digital_lending.default_lending_period).to_i end end - context 'a custom lending period has been set' do + context 'a plain text custom lending period has been set' do let(:media_object) { FactoryBot.create(:media_object, lending_period: "1 day")} - it 'is equal to the custom lending period' do + it 'is equal to the custom lending period measured in seconds' do + media_object.set_lending_period + expect(media_object.lending_period).to eq 86400 + end + end + context 'an ISO8601 duration format custom lending period has been set' do + let(:media_object) { FactoryBot.create(:media_object, lending_period: "P1D")} + it 'is equal to the custom lending period measured in seconds' do + media_object.set_lending_period + expect(media_object.lending_period).to eq 86400 + end + end + context 'a integer custom lending period has been set' do + let(:media_object) { FactoryBot.create(:media_object, lending_period: 86400)} + it 'is equal to the custom lending period measured in seconds' do + media_object.set_lending_period expect(media_object.lending_period).to eq 86400 end end From c621dd94d198b0e071b288a63e21d53bc0f5baf2 Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Thu, 7 Jul 2022 14:55:27 -0400 Subject: [PATCH 044/230] Move edit fields to access control page --- app/helpers/media_objects_helper.rb | 17 +++++++++++- app/models/access_control_step.rb | 2 ++ app/models/media_object.rb | 7 ++--- .../media_objects/_metadata_display.html.erb | 4 +-- .../_resource_description.html.erb | 3 --- app/views/media_objects/_text_field.html.erb | 2 -- app/views/modules/_access_control.html.erb | 27 +++++++++++++++++++ config/locales/en.yml | 14 +++++----- spec/helpers/media_objects_helper_spec.rb | 2 +- spec/models/media_object_spec.rb | 20 +++++++++++--- 10 files changed, 74 insertions(+), 24 deletions(-) diff --git a/app/helpers/media_objects_helper.rb b/app/helpers/media_objects_helper.rb index 34ba8584af..4877c79882 100644 --- a/app/helpers/media_objects_helper.rb +++ b/app/helpers/media_objects_helper.rb @@ -114,8 +114,23 @@ def display_rights_statement media_object content_tag(:dt, 'Rights Statement') + content_tag(:dd) { link } end + # Lending period is stored as seconds. Convert to days for display. def display_lending_period media_object - ActiveSupport::Duration.build(media_object.lending_period).parts.map{ |k, v| "#{v} #{k}" }.join(' ') + if media_object.lending_period % (60*60*24) == 0 + day_count = (media_object.lending_period/(60*60*24)).to_s + build_lending_display(day_count, 'day') + else + hour_count = (media_object.lending_period/(60*60)).to_s + build_lending_display(hour_count, 'hour') + end + end + + def build_lending_display(count, unit) + if count == '1' + count + ' ' + unit + else + count + ' ' + unit + 's' + end end def current_quality stream_info diff --git a/app/models/access_control_step.rb b/app/models/access_control_step.rb index cd47daa719..e0961f8187 100644 --- a/app/models/access_control_step.rb +++ b/app/models/access_control_step.rb @@ -97,6 +97,7 @@ def execute context unless limited_access_submit media_object.visibility = context[:visibility] unless context[:visibility].blank? media_object.hidden = context[:hidden] == "1" + media_object.lending_period = context[:lending_period] end media_object.save! @@ -112,6 +113,7 @@ def execute context context[:ip_leases] = media_object.leases('ip') context[:addable_groups] = Admin::Group.non_system_groups.reject { |g| context[:groups].include? g.name } context[:addable_courses] = Course.all.reject { |c| context[:virtual_groups].include? c.context_id } + context[:lending_period] = media_object.lending_period context end end diff --git a/app/models/media_object.rb b/app/models/media_object.rb index d4a6049a94..edf1e20402 100644 --- a/app/models/media_object.rb +++ b/app/models/media_object.rb @@ -285,6 +285,7 @@ def as_json(options={}) summary: abstract, visibility: visibility, read_groups: read_groups, + lending_period: lending_period, lending_status: lending_status, }.merge(to_ingest_api_hash(options.fetch(:include_structure, false))) end @@ -420,9 +421,7 @@ def build_lend_period replacement.detect{ |k, v| k =~ match }[1] end - build_iso8601_duration(@lend_period) - - @lend_period + @lend_period.match(/P/) ? @lend_period : build_iso8601_duration(@lend_period) end def build_iso8601_duration(lend_period) @@ -435,8 +434,6 @@ def build_iso8601_duration(lend_period) @lend_period.prepend('PT') end end - - @lend_period end end diff --git a/app/views/media_objects/_metadata_display.html.erb b/app/views/media_objects/_metadata_display.html.erb index e9ae62b15d..4faa3d6d2a 100644 --- a/app/views/media_objects/_metadata_display.html.erb +++ b/app/views/media_objects/_metadata_display.html.erb @@ -42,7 +42,6 @@ Unless required by applicable law or agreed to in writing, software distributed <%= display_metadata('Language', display_language(@media_object)) %> <%= display_rights_statement(@media_object) %> <%= display_metadata('Terms of Use', @media_object.terms_of_use) %> - <%= display_metadata('Lending Period', display_lending_period(@media_object)) %> <%= display_metadata('Physical Description', @media_object.physical_description) %> <%= display_metadata('Related Item', display_related_item(@media_object)) %> <%= display_metadata('Notes', display_notes(@media_object)) %> @@ -73,7 +72,8 @@ Unless required by applicable law or agreed to in writing, software distributed
      - <%= @media_object.access_text %> + <%= @media_object.access_text %>
      + Lending Period: <%= display_lending_period(@media_object) %>
      diff --git a/app/views/media_objects/_resource_description.html.erb b/app/views/media_objects/_resource_description.html.erb index a65c13673f..b070a900ea 100644 --- a/app/views/media_objects/_resource_description.html.erb +++ b/app/views/media_objects/_resource_description.html.erb @@ -100,9 +100,6 @@ Unless required by applicable law or agreed to in writing, software distributed <%= render partial: 'text_area', locals: {form: form, field: :terms_of_use, options: {display_label: 'Terms of Use'}} %> - <%= render partial: 'text_field', - locals: {form: form, field: :lending_period, - options: {display_label: 'Lending Period'}} %> <%= render partial: 'text_field', locals: {form: form, field: :other_identifier, options: {display_label: 'Other Identifier(s)', diff --git a/app/views/media_objects/_text_field.html.erb b/app/views/media_objects/_text_field.html.erb index 1e84ff0062..c517188370 100644 --- a/app/views/media_objects/_text_field.html.erb +++ b/app/views/media_objects/_text_field.html.erb @@ -39,8 +39,6 @@ Unless required by applicable law or agreed to in writing, software distributed <% if options[:dropdown_field] %> <%= render partial: "multipart_dropdown_field", locals: {selected_value: value.present? ? value[options[:secondary_hash_key]] : nil, options: options} %> <% value = value.present? ? value[options[:primary_hash_key]] : "" %> - <% elsif fieldname == "media_object[lending_period]" %> - <% value = ActiveSupport::Duration.build(value).parts.map{ |k, v| "#{v} #{k}" }.join(' ') %> <% end %> <%= text_field_tag fieldname, value || '', class: (@media_object.errors[field].any? ? 'form-control is-invalid' : 'form-control') %> <% end %> diff --git a/app/views/modules/_access_control.html.erb b/app/views/modules/_access_control.html.erb index 7156e66585..982e8a1b0a 100644 --- a/app/views/modules/_access_control.html.erb +++ b/app/views/modules/_access_control.html.erb @@ -64,6 +64,21 @@ Unless required by applicable law or agreed to in writing, software distributed +
      +
      +

      Item lending period

      +
      +
      +
      + <%= render partial: "modules/tooltip", locals: { form: vid, field: :lending_period, tooltip: t("access_control.#{:lending_period}"), options: {display_label: (t("access_control.#{:lending_period}label")+'*').html_safe} } %>
      +
      + <% value = display_lending_period(object) %> + <%= text_field_tag "lending_period", value || '', class: 'form-control' %> +
      +
      +
      +
      + <%= render modal[:partial], modal_title: modal[:title] if defined? modal %> <% end #form_for %> @@ -104,6 +119,18 @@ Unless required by applicable law or agreed to in writing, software distributed +
      +
      +

      Item lending period

      +
      +
      +
      + Item is available to be checked out for + <%= display_lending_period(object) %> +
      +
      +
      + <% end #form_for %> <% end %> diff --git a/config/locales/en.yml b/config/locales/en.yml index 2d0a0615bd..b5c5532cd2 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -121,12 +121,6 @@ en: Other Identifier should be used for an external record identifier that can connect the Avalon item to a catalog record or other record for the original item. This identifier differs from Bibliographic Identifier in that it is not used to retrieve a record from another system. Type specifies the type of external record identifier. table_of_contents: | Table of Contents is used to provide the titles of separate works or parts of a resource. Information provided may also contain statements of responsibility or other sequential designations. Titles of separate works or parts should be separated by ' -- ' (space-hyphen-hyphen-space). - lending_period: | - Lending Period is the length of time that an item can be checked out for. - Accepted formats are ISO8601 duration (ex. P1DT12H), the time measured in seconds (ex. 129600), - or a human-readable string using numerals without punctuation or conjunctions (ex. 1 day 12 hours). - Values entered as strings must be entered as whole days and/or hours. For example, one month would be entered as - '30 days' and 1.5 days would be entered as '1 day 12 hours' or '36 hours'. file_upload_tip: title: | @@ -147,6 +141,7 @@ en: grouplabel: "Avalon Group" classlabel: "External Group" ipaddresslabel: "IP Address or Range" + lending_periodlabel: "Lending Period" depositor: | Depositors add media to the collection and describe it with metadata. They can publish items but not unpublish. They can only modify or @@ -175,6 +170,13 @@ en: or ffaa:aaff:bbcc:ddee:1122:3344:5566:7777 or ffaa:aaff:bbcc:ddee:1122:3344:5566:7777/ffff:ffff:ffff:ffff:ffff:ffff:ffff:ff00 Start and end dates are not required; if included, access for the specified IP Address(es) will start at the beginning of the start date and end at the beginning of the end date. Otherwise, access will be open ended for the specified IP Address(es). + lending_period: | + Lending Period is the length of time that an item can be checked out for. + Accepted formats are ISO8601 duration (ex. P1DT12H) or a human-readable + string using numerals without punctuation or conjunctions (ex. 1 day 12 hours). + Values entered as strings may only contain days and/or hours and must use whole numbers. + For example, 1 month would be entered as '30 days' and 1.5 days would + be entered as '1 day 12 hours' or '36 hours'. contact: title: 'Contact Us - %{application_name}' diff --git a/spec/helpers/media_objects_helper_spec.rb b/spec/helpers/media_objects_helper_spec.rb index 0ddeb591a4..1677ab511c 100644 --- a/spec/helpers/media_objects_helper_spec.rb +++ b/spec/helpers/media_objects_helper_spec.rb @@ -128,7 +128,7 @@ subject { helper.display_lending_period(media_object) } it 'returns the lending period as a human readable string' do - expect(subject).to eq("1 days 12 hours") + expect(subject).to eq("36 hours") end end end diff --git a/spec/models/media_object_spec.rb b/spec/models/media_object_spec.rb index f73685afa2..8b21c87957 100644 --- a/spec/models/media_object_spec.rb +++ b/spec/models/media_object_spec.rb @@ -1028,21 +1028,33 @@ end end context 'a plain text custom lending period has been set' do - let(:media_object) { FactoryBot.create(:media_object, lending_period: "1 day")} + let(:media_object) { FactoryBot.create(:media_object, lending_period: "1 day") } + let(:complex_date) { FactoryBot.create(:media_object, lending_period: "6 days 4 hours")} it 'is equal to the custom lending period measured in seconds' do media_object.set_lending_period expect(media_object.lending_period).to eq 86400 end + it 'accepts strings containing day and hour' do + expect { complex_date.set_lending_period }.not_to raise_error + end end context 'an ISO8601 duration format custom lending period has been set' do - let(:media_object) { FactoryBot.create(:media_object, lending_period: "P1D")} + let(:media_object) { FactoryBot.create(:media_object, lending_period: "P1D") } + let(:year_month) { FactoryBot.create(:media_object, lending_period: "P1Y2M") } + let(:day_hr_min_sec) { FactoryBot.create(:media_object, lending_period: "P4DT6H3M30S")} + let(:sec) { FactoryBot.create(:media_object, lending_period: "PT3650.015S")} it 'is equal to the custom lending period measured in seconds' do media_object.set_lending_period expect(media_object.lending_period).to eq 86400 end + it 'accepts any ISO8601 duration' do + expect { year_month.set_lending_period }.not_to raise_error + expect { day_hr_min_sec.set_lending_period }.not_to raise_error + expect { sec.set_lending_period }.not_to raise_error + end end - context 'a integer custom lending period has been set' do - let(:media_object) { FactoryBot.create(:media_object, lending_period: 86400)} + context 'an integer custom lending period has been set' do + let(:media_object) { FactoryBot.create(:media_object, lending_period: 86400) } it 'is equal to the custom lending period measured in seconds' do media_object.set_lending_period expect(media_object.lending_period).to eq 86400 From 7526c40e899c46904b0f6bfa0489672b2f8b6f3d Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Thu, 7 Jul 2022 16:15:45 -0400 Subject: [PATCH 045/230] Fix collection edit page --- app/helpers/media_objects_helper.rb | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/app/helpers/media_objects_helper.rb b/app/helpers/media_objects_helper.rb index 4877c79882..3a826ebd76 100644 --- a/app/helpers/media_objects_helper.rb +++ b/app/helpers/media_objects_helper.rb @@ -115,13 +115,17 @@ def display_rights_statement media_object end # Lending period is stored as seconds. Convert to days for display. - def display_lending_period media_object - if media_object.lending_period % (60*60*24) == 0 - day_count = (media_object.lending_period/(60*60*24)).to_s - build_lending_display(day_count, 'day') + def display_lending_period object + if object.is_a?(Admin::Collection) + 0 else - hour_count = (media_object.lending_period/(60*60)).to_s - build_lending_display(hour_count, 'hour') + if object.lending_period % (60*60*24) == 0 + day_count = (object.lending_period/(60*60*24)).to_s + build_lending_display(day_count, 'day') + else + hour_count = (object.lending_period/(60*60)).to_s + build_lending_display(hour_count, 'hour') + end end end From b19b88dbdfbbbdda846813cd9e0ae0ffd8167985 Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Fri, 8 Jul 2022 13:24:37 -0400 Subject: [PATCH 046/230] Move lending period methods to concerns --- app/models/admin/collection.rb | 1 + app/models/concerns/lending_period.rb | 62 +++++++++++++++++++++++++++ app/models/media_object.rb | 52 +--------------------- 3 files changed, 64 insertions(+), 51 deletions(-) create mode 100644 app/models/concerns/lending_period.rb diff --git a/app/models/admin/collection.rb b/app/models/admin/collection.rb index 32636ca1ec..3c0c52faa5 100644 --- a/app/models/admin/collection.rb +++ b/app/models/admin/collection.rb @@ -22,6 +22,7 @@ class Admin::Collection < ActiveFedora::Base include ActiveFedora::Associations include Identifier include MigrationTarget + include LendingPeriod has_many :media_objects, class_name: 'MediaObject', predicate: ActiveFedora::RDF::Fcrepo::RelsExt.isMemberOfCollection diff --git a/app/models/concerns/lending_period.rb b/app/models/concerns/lending_period.rb new file mode 100644 index 0000000000..47d5c8ca78 --- /dev/null +++ b/app/models/concerns/lending_period.rb @@ -0,0 +1,62 @@ +module LendingPeriod + + extend ActiveSupport::Concern + + included do + property :lending_period, predicate: ::RDF::Vocab::SCHEMA.eligibleDuration, multiple: false do |index| + index.as :stored_sortable + end + + after_initialize :set_lending_period + end + + def set_lending_period + if (!self.lending_period.nil? && !(self.lending_period.is_a? Integer)) + build_lend_period + self.lending_period = ActiveSupport::Duration.parse(@lend_period).to_i + end + self.lending_period ||= ActiveSupport::Duration.parse(Settings.controlled_digital_lending.default_lending_period).to_i + end + + def current_checkout(user_id) + checkouts = Checkout.active_for_media_object(id) + checkouts.select{ |ch| ch.user_id == user_id }.first + end + + private + + def build_lend_period + @lend_period = self.lending_period.dup + + replacement = { + /\s+days?/i => 'D', + /\s+hours?/i => 'H', + /,?\s+/ => 'T' + } + + rules = replacement.collect{ |k, v| k } + + matcher = Regexp.union(rules) + + @lend_period = @lend_period.gsub(matcher) do |match| + replacement.detect{ |k, v| k =~ match }[1] + end + + @lend_period.match(/P/) ? @lend_period : build_iso8601_duration(@lend_period) + end + + def build_iso8601_duration(lend_period) + if @lend_period.match?(/D/) + unless @lend_period.include? 'P' + @lend_period.prepend('P') + end + else + unless @lend_period.include? 'PT' + @lend_period.prepend('PT') + end + end + end + + + +end diff --git a/app/models/media_object.rb b/app/models/media_object.rb index edf1e20402..5e962224ae 100644 --- a/app/models/media_object.rb +++ b/app/models/media_object.rb @@ -25,6 +25,7 @@ class MediaObject < ActiveFedora::Base include SpeedyAF::OrderedAggregationIndex include MediaObjectIntercom include SupplementalFileBehavior + include LendingPeriod require 'avalon/controlled_vocabulary' include Kaminari::ActiveFedoraModelExtension @@ -38,8 +39,6 @@ class MediaObject < ActiveFedora::Base after_save :update_dependent_permalinks_job, if: Proc.new { |mo| mo.persisted? && mo.published? } after_save :remove_bookmarks - after_initialize :set_lending_period - # Call custom validation methods to ensure that required fields are present and # that preferred controlled vocabulary standards are used @@ -113,9 +112,6 @@ def validate_date(date_field) property :comment, predicate: ::RDF::Vocab::EBUCore.comments, multiple: true do |index| index.as :stored_searchable end - property :lending_period, predicate: ::RDF::Vocab::SCHEMA.eligibleDuration, multiple: false do |index| - index.as :stored_sortable - end ordered_aggregation :master_files, class_name: 'MasterFile', through: :list_source # ordered_aggregation gives you accessors media_obj.master_files and media_obj.ordered_master_files @@ -377,19 +373,6 @@ def lending_status Checkout.active_for_media_object(id).any? ? "checked_out" : "available" end - def set_lending_period - if (!self.lending_period.nil? && !(self.lending_period.is_a? Integer)) - build_lend_period - self.lending_period = ActiveSupport::Duration.parse(@lend_period).to_i - end - self.lending_period ||= ActiveSupport::Duration.parse(Settings.controlled_digital_lending.default_lending_period).to_i - end - - def current_checkout(user_id) - checkouts = Checkout.active_for_media_object(id) - checkouts.select{ |ch| ch.user_id == user_id }.first - end - private def calculate_duration @@ -403,37 +386,4 @@ def collect_ips_for_index ip_strings end ips.flatten.compact.uniq || [] end - - def build_lend_period - @lend_period = self.lending_period.dup - - replacement = { - /\s+days?/i => 'D', - /\s+hours?/i => 'H', - /,?\s+/ => 'T' - } - - rules = replacement.collect{ |k, v| k } - - matcher = Regexp.union(rules) - - @lend_period = @lend_period.gsub(matcher) do |match| - replacement.detect{ |k, v| k =~ match }[1] - end - - @lend_period.match(/P/) ? @lend_period : build_iso8601_duration(@lend_period) - end - - def build_iso8601_duration(lend_period) - if @lend_period.match?(/D/) - unless @lend_period.include? 'P' - @lend_period.prepend('P') - end - else - unless @lend_period.include? 'PT' - @lend_period.prepend('PT') - end - end - end - end From a75b6f28328b60dbba2d8b94dd9f7be74c2ab08d Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Fri, 8 Jul 2022 17:38:15 -0400 Subject: [PATCH 047/230] Add lending period to collection --- .../admin/collections_controller.rb | 2 ++ app/helpers/media_objects_helper.rb | 36 ++++++++++--------- app/jobs/bulk_action_jobs.rb | 1 + app/models/concerns/lending_period.rb | 23 ++++++------ app/models/media_object.rb | 5 +++ app/views/admin/collections/show.html.erb | 7 ++-- 6 files changed, 43 insertions(+), 31 deletions(-) diff --git a/app/controllers/admin/collections_controller.rb b/app/controllers/admin/collections_controller.rb index d0bc1b6e92..a2f69db8e6 100644 --- a/app/controllers/admin/collections_controller.rb +++ b/app/controllers/admin/collections_controller.rb @@ -56,6 +56,7 @@ def show @virtual_groups = @collection.default_virtual_read_groups @ip_groups = @collection.default_ip_read_groups @visibility = @collection.default_visibility + @lending_period = @collection.lending_period @addable_groups = Admin::Group.non_system_groups.reject { |g| @groups.include? g.name } @addable_courses = Course.all.reject { |c| @virtual_groups.include? c.context_id } @@ -298,6 +299,7 @@ def update_access(collection, params) collection.default_visibility = params[:visibility] unless params[:visibility].blank? collection.default_hidden = params[:hidden] == "1" + collection.lending_period = params[:lending_period] end def apply_access(collection, params) diff --git a/app/helpers/media_objects_helper.rb b/app/helpers/media_objects_helper.rb index 3a826ebd76..c9a8f03d49 100644 --- a/app/helpers/media_objects_helper.rb +++ b/app/helpers/media_objects_helper.rb @@ -114,26 +114,28 @@ def display_rights_statement media_object content_tag(:dt, 'Rights Statement') + content_tag(:dd) { link } end - # Lending period is stored as seconds. Convert to days for display. + # Lending period is stored as seconds. Convert to days and hours for display. def display_lending_period object - if object.is_a?(Admin::Collection) - 0 - else - if object.lending_period % (60*60*24) == 0 - day_count = (object.lending_period/(60*60*24)).to_s - build_lending_display(day_count, 'day') - else - hour_count = (object.lending_period/(60*60)).to_s - build_lending_display(hour_count, 'hour') - end - end - end + d, h = (object.lending_period/3600).divmod(24) + # TODO: Figure out how to get this to not have 's' on the end of singular day/hour + replacement = { + /(?<=1\s)hours/ => ' hour', + /(?<=^1\s)days/ => 'day' + } + + rules = replacement.collect{ |k, v| k } - def build_lending_display(count, unit) - if count == '1' - count + ' ' + unit + matcher = Regexp.union(rules) + + if d == 0 + (h.to_s + ' hours').gsub(matcher, 'hour') + elsif h == 0 + (d.to_s + ' days').gsub(matcher, 'day') else - count + ' ' + unit + 's' + date_string = "%d days %d hours" % [d, h] + date_string = date_string.gsub(matcher) do |match| + replacement.detect{ |k, v| k =~ match }[1] + end end end diff --git a/app/jobs/bulk_action_jobs.rb b/app/jobs/bulk_action_jobs.rb index 8c0e2f32db..b0f6575a96 100644 --- a/app/jobs/bulk_action_jobs.rb +++ b/app/jobs/bulk_action_jobs.rb @@ -205,6 +205,7 @@ def perform(collection_id, overwrite = true) media_object = MediaObject.find(id) media_object.hidden = collection.default_hidden media_object.visibility = collection.default_visibility + media_object.lending_period = collection.lending_period # Special access if overwrite diff --git a/app/models/concerns/lending_period.rb b/app/models/concerns/lending_period.rb index 47d5c8ca78..11d267e192 100644 --- a/app/models/concerns/lending_period.rb +++ b/app/models/concerns/lending_period.rb @@ -11,20 +11,24 @@ module LendingPeriod end def set_lending_period + if self.is_a? Admin::Collection + custom_lend_period + self.lending_period ||= ActiveSupport::Duration.parse(Settings.controlled_digital_lending.default_lending_period).to_i + elsif !self.collection_id.nil? + custom_lend_period + self.lending_period ||= Admin::Collection.find(self.collection_id).lending_period + end + end + + private + + def custom_lend_period if (!self.lending_period.nil? && !(self.lending_period.is_a? Integer)) build_lend_period self.lending_period = ActiveSupport::Duration.parse(@lend_period).to_i end - self.lending_period ||= ActiveSupport::Duration.parse(Settings.controlled_digital_lending.default_lending_period).to_i - end - - def current_checkout(user_id) - checkouts = Checkout.active_for_media_object(id) - checkouts.select{ |ch| ch.user_id == user_id }.first end - private - def build_lend_period @lend_period = self.lending_period.dup @@ -56,7 +60,4 @@ def build_iso8601_duration(lend_period) end end end - - - end diff --git a/app/models/media_object.rb b/app/models/media_object.rb index 5e962224ae..69c306e114 100644 --- a/app/models/media_object.rb +++ b/app/models/media_object.rb @@ -373,6 +373,11 @@ def lending_status Checkout.active_for_media_object(id).any? ? "checked_out" : "available" end + def current_checkout(user_id) + checkouts = Checkout.active_for_media_object(id) + checkouts.select{ |ch| ch.user_id == user_id }.first + end + private def calculate_duration diff --git a/app/views/admin/collections/show.html.erb b/app/views/admin/collections/show.html.erb index 2c29e1d2bb..0dcc43f4d9 100644 --- a/app/views/admin/collections/show.html.erb +++ b/app/views/admin/collections/show.html.erb @@ -125,9 +125,10 @@ Unless required by applicable law or agreed to in writing, software distributed

      Set Default Access Control for New Items

      - <%= render "modules/access_control", { object: @collection, - visibility: @collection.default_visibility, - hidden: @collection.default_hidden, + <%= render "modules/access_control", { object: @collection, + visibility: @collection.default_visibility, + hidden: @collection.default_hidden, + lending_period: @collection.lending_period, modal: { partial: "apply_access_control", title: "Apply current Default Access settings to all existing Items" } } %> <% if can? :update_access_control, @collection %> From 841a9d39f5618a88f1e22d8983abd1207a5ff8ae Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Mon, 11 Jul 2022 16:50:35 -0400 Subject: [PATCH 048/230] Add handling for singular dates, fix tests Date strings originally displayed as "1 days 1 hours" rather than the singular forms. Added parsing to handle generation of singular date strings. --- app/helpers/media_objects_helper.rb | 10 +++++----- spec/helpers/media_objects_helper_spec.rb | 15 ++++++++++++++- spec/jobs/bulk_action_job_spec.rb | 8 ++++++++ spec/models/media_object_spec.rb | 8 +------- 4 files changed, 28 insertions(+), 13 deletions(-) diff --git a/app/helpers/media_objects_helper.rb b/app/helpers/media_objects_helper.rb index c9a8f03d49..0e8e366f4c 100644 --- a/app/helpers/media_objects_helper.rb +++ b/app/helpers/media_objects_helper.rb @@ -117,10 +117,10 @@ def display_rights_statement media_object # Lending period is stored as seconds. Convert to days and hours for display. def display_lending_period object d, h = (object.lending_period/3600).divmod(24) - # TODO: Figure out how to get this to not have 's' on the end of singular day/hour + replacement = { - /(?<=1\s)hours/ => ' hour', - /(?<=^1\s)days/ => 'day' + /^?\s?1\shours/ => ' 1 hour', + /^1\sdays/ => '1 day' } rules = replacement.collect{ |k, v| k } @@ -128,9 +128,9 @@ def display_lending_period object matcher = Regexp.union(rules) if d == 0 - (h.to_s + ' hours').gsub(matcher, 'hour') + (h.to_s + ' hours').gsub(matcher, '1 hour') elsif h == 0 - (d.to_s + ' days').gsub(matcher, 'day') + (d.to_s + ' days').gsub(matcher, '1 day') else date_string = "%d days %d hours" % [d, h] date_string = date_string.gsub(matcher) do |match| diff --git a/spec/helpers/media_objects_helper_spec.rb b/spec/helpers/media_objects_helper_spec.rb index 1677ab511c..1aae720feb 100644 --- a/spec/helpers/media_objects_helper_spec.rb +++ b/spec/helpers/media_objects_helper_spec.rb @@ -128,7 +128,20 @@ subject { helper.display_lending_period(media_object) } it 'returns the lending period as a human readable string' do - expect(subject).to eq("36 hours") + expect(subject).to eq("1 day 12 hours") + end + end + context 'when lending period includes 1 day and/or 1 hour' do + let(:day) { instance_double("MediaObject", lending_period: 86400) } + let(:hour) { instance_double("MediaObject", lending_period: 3600) } + let(:day_hour) { instance_double("MediaObject", lending_period: 90000) } + + subject { [helper.display_lending_period(day), helper.display_lending_period(hour), helper.display_lending_period(day_hour)] } + + it 'returns the lending period as a human readable string with singular day and/or hour' do + expect(subject[0]).to eq("1 day") + expect(subject[1]).to eq("1 hour") + expect(subject[2]).to eq("1 day 1 hour") end end end diff --git a/spec/jobs/bulk_action_job_spec.rb b/spec/jobs/bulk_action_job_spec.rb index 1e12823e46..3fa810d980 100644 --- a/spec/jobs/bulk_action_job_spec.rb +++ b/spec/jobs/bulk_action_job_spec.rb @@ -79,12 +79,14 @@ def check_push(result) co.default_read_groups = ["co_group"] co.default_hidden = true co.default_visibility = 'public' + co.lending_period = 129600 co.save! mo.read_users = ["mo_user"] mo.read_groups = ["mo_group"] mo.hidden = false mo.visibility = 'restricted' + mo.lending_period = 1209600 mo.save! end @@ -95,6 +97,12 @@ def check_push(result) expect(mo.visibility).to eq('public') end + it "changes item lending period" do + BulkActionJobs::ApplyCollectionAccessControl.perform_now co.id, true + mo.reload + expect(mo.lending_period).to eq(co.lending_period) + end + context "overwrite is true" do it "replaces existing Special Access" do BulkActionJobs::ApplyCollectionAccessControl.perform_now co.id, true diff --git a/spec/models/media_object_spec.rb b/spec/models/media_object_spec.rb index 8b21c87957..dd5d7608da 100644 --- a/spec/models/media_object_spec.rb +++ b/spec/models/media_object_spec.rb @@ -1024,6 +1024,7 @@ describe 'set_lending_period' do context 'a custom lending period has not been set' do it 'is equal to the default period in the settings.yml' do + media_object.set_lending_period expect(media_object.lending_period).to eq ActiveSupport::Duration.parse(Settings.controlled_digital_lending.default_lending_period).to_i end end @@ -1053,12 +1054,5 @@ expect { sec.set_lending_period }.not_to raise_error end end - context 'an integer custom lending period has been set' do - let(:media_object) { FactoryBot.create(:media_object, lending_period: 86400) } - it 'is equal to the custom lending period measured in seconds' do - media_object.set_lending_period - expect(media_object.lending_period).to eq 86400 - end - end end end From 2559c988080c2206ac2a95aa6c0d02dc341fa0b5 Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Tue, 12 Jul 2022 15:55:15 -0400 Subject: [PATCH 049/230] Refactor tests --- app/models/media_object.rb | 1 + spec/models/concerns/lending_period_spec.rb | 73 +++++++++++++++++++++ spec/models/media_object_spec.rb | 41 ++---------- 3 files changed, 79 insertions(+), 36 deletions(-) create mode 100644 spec/models/concerns/lending_period_spec.rb diff --git a/app/models/media_object.rb b/app/models/media_object.rb index 69c306e114..f987173821 100644 --- a/app/models/media_object.rb +++ b/app/models/media_object.rb @@ -146,6 +146,7 @@ def collection= co self.visibility = co.default_visibility self.read_users = co.default_read_users.to_a self.read_groups = co.default_read_groups.to_a + self.read_groups #Make sure to include any groups added by visibility + self.lending_period = co.lending_period end end diff --git a/spec/models/concerns/lending_period_spec.rb b/spec/models/concerns/lending_period_spec.rb new file mode 100644 index 0000000000..45cf90320e --- /dev/null +++ b/spec/models/concerns/lending_period_spec.rb @@ -0,0 +1,73 @@ +# Copyright 2011-2022, The Trustees of Indiana University and Northwestern +# University. Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed +# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +# CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. +# --- END LICENSE_HEADER BLOCK --- + +require 'rails_helper' + +describe LendingPeriod do + + before(:all) do + class Foo < ActiveFedora::Base + include LendingPeriod + + attr_accessor :collection_id + end + end + + after(:all) { Object.send(:remove_const, :Foo) } + + subject { Foo.new } + + let(:co) { FactoryBot.create(:collection) } + + before { subject.collection_id = co.id } + + it 'defines lending_period' do + expect(subject.attributes).to include('lending_period') + end + + describe 'set_lending_period' do + context 'a custom lending period has not been set' do + it 'is equal to the default period in the settings.yml' do + subject.set_lending_period + expect(subject.lending_period).to eq ActiveSupport::Duration.parse(Settings.controlled_digital_lending.default_lending_period).to_i + end + end + context 'a plain text custom lending period has been set' do + let(:media_object) { FactoryBot.create(:media_object, lending_period: "1 day") } + let(:complex_date) { FactoryBot.create(:collection, lending_period: "6 days 4 hours")} + it 'is equal to the custom lending period measured in seconds' do + media_object.set_lending_period + expect(media_object.lending_period).to eq 86400 + end + it 'accepts strings containing day and hour' do + expect { complex_date.set_lending_period }.not_to raise_error + end + end + context 'an ISO8601 duration format custom lending period has been set' do + let(:media_object) { FactoryBot.create(:collection, lending_period: "P1D") } + let(:year_month) { FactoryBot.create(:media_object, lending_period: "P1Y2M") } + let(:day_hr_min_sec) { FactoryBot.create(:collection, lending_period: "P4DT6H3M30S")} + let(:sec) { FactoryBot.create(:media_object, lending_period: "PT3650.015S")} + it 'is equal to the custom lending period measured in seconds' do + media_object.set_lending_period + expect(media_object.lending_period).to eq 86400 + end + it 'accepts any ISO8601 duration' do + expect { year_month.set_lending_period }.not_to raise_error + expect { day_hr_min_sec.set_lending_period }.not_to raise_error + expect { sec.set_lending_period }.not_to raise_error + end + end + end +end diff --git a/spec/models/media_object_spec.rb b/spec/models/media_object_spec.rb index dd5d7608da..964c75396d 100644 --- a/spec/models/media_object_spec.rb +++ b/spec/models/media_object_spec.rb @@ -770,7 +770,7 @@ describe '#collection=' do let(:new_media_object) { MediaObject.new } - let(:collection) { FactoryBot.create(:collection, default_hidden: true, default_read_users: ['archivist1@example.com'], default_read_groups: ['TestGroup', 'public'])} + let(:collection) { FactoryBot.create(:collection, default_hidden: true, default_read_users: ['archivist1@example.com'], default_read_groups: ['TestGroup', 'public'], lending_period: 86400)} it 'sets hidden based upon collection for new media objects' do expect {new_media_object.collection = collection}.to change {new_media_object.hidden?}.to(true).from(false) @@ -785,11 +785,15 @@ expect(new_media_object.read_groups).not_to include "TestGroup" expect {new_media_object.collection = collection}.to change {new_media_object.read_groups}.to include 'TestGroup' end + it 'sets lending_period based upon collection for new media objects' do + expect {new_media_object.collection = collection}.to change {new_media_object.lending_period}.to(86400).from(nil) + end it 'does not change access control fields if not new media object' do expect {media_object.collection = collection}.not_to change {new_media_object.hidden?} expect {media_object.collection = collection}.not_to change {new_media_object.visibility} expect {media_object.collection = collection}.not_to change {new_media_object.read_users} expect {media_object.collection = collection}.not_to change {new_media_object.read_users} + expect {media_object.collection = collection}.not_to change {new_media_object.lending_period} end end @@ -1020,39 +1024,4 @@ end end end - - describe 'set_lending_period' do - context 'a custom lending period has not been set' do - it 'is equal to the default period in the settings.yml' do - media_object.set_lending_period - expect(media_object.lending_period).to eq ActiveSupport::Duration.parse(Settings.controlled_digital_lending.default_lending_period).to_i - end - end - context 'a plain text custom lending period has been set' do - let(:media_object) { FactoryBot.create(:media_object, lending_period: "1 day") } - let(:complex_date) { FactoryBot.create(:media_object, lending_period: "6 days 4 hours")} - it 'is equal to the custom lending period measured in seconds' do - media_object.set_lending_period - expect(media_object.lending_period).to eq 86400 - end - it 'accepts strings containing day and hour' do - expect { complex_date.set_lending_period }.not_to raise_error - end - end - context 'an ISO8601 duration format custom lending period has been set' do - let(:media_object) { FactoryBot.create(:media_object, lending_period: "P1D") } - let(:year_month) { FactoryBot.create(:media_object, lending_period: "P1Y2M") } - let(:day_hr_min_sec) { FactoryBot.create(:media_object, lending_period: "P4DT6H3M30S")} - let(:sec) { FactoryBot.create(:media_object, lending_period: "PT3650.015S")} - it 'is equal to the custom lending period measured in seconds' do - media_object.set_lending_period - expect(media_object.lending_period).to eq 86400 - end - it 'accepts any ISO8601 duration' do - expect { year_month.set_lending_period }.not_to raise_error - expect { day_hr_min_sec.set_lending_period }.not_to raise_error - expect { sec.set_lending_period }.not_to raise_error - end - end - end end From 374184a33ad487ec9e02239338e4320e91a6e8a2 Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Wed, 13 Jul 2022 10:16:05 -0400 Subject: [PATCH 050/230] Fix flaky return time test There is a ~0.07 second delay between the test writing the DateTime to the DB and the test checking the value. freeze_time should eliminate the discrepancy. --- spec/requests/checkouts_spec.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/spec/requests/checkouts_spec.rb b/spec/requests/checkouts_spec.rb index 55b06164ed..a280f457f4 100644 --- a/spec/requests/checkouts_spec.rb +++ b/spec/requests/checkouts_spec.rb @@ -13,6 +13,7 @@ # sticking to rails and rspec-rails APIs to keep things simple and stable. RSpec.describe "/checkouts", type: :request do + include ActiveSupport::Testing::TimeHelpers let(:user) { FactoryBot.create(:user) } let(:media_object) { FactoryBot.create(:published_media_object, visibility: 'public') } @@ -152,6 +153,8 @@ context "non-default lending period" do let(:media_object) { FactoryBot.create(:published_media_object, lending_period: "1 day", visibility: 'public') } + before { freeze_time } + after { travel_back } it "creates a new checkout" do expect{ post checkouts_url, params: { checkout: valid_attributes } From 9dd15b2874e599b3eeae2b8ec540ecd1b376d891 Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Fri, 15 Jul 2022 11:53:26 -0400 Subject: [PATCH 051/230] Add subclass to ActiveSupport::Duration Co-authored-by: Chris Colvard --- app/helpers/media_objects_helper.rb | 25 --------- .../media_objects/_metadata_display.html.erb | 2 +- app/views/modules/_access_control.html.erb | 2 +- config/initializers/avalon.rb | 1 + lib/human_readable_duration.rb | 32 ++++++++++++ spec/helpers/media_objects_helper_spec.rb | 43 --------------- spec/lib/human_readable_duration_spec.rb | 52 +++++++++++++++++++ 7 files changed, 87 insertions(+), 70 deletions(-) create mode 100644 lib/human_readable_duration.rb create mode 100644 spec/lib/human_readable_duration_spec.rb diff --git a/app/helpers/media_objects_helper.rb b/app/helpers/media_objects_helper.rb index 0e8e366f4c..c435a20b73 100644 --- a/app/helpers/media_objects_helper.rb +++ b/app/helpers/media_objects_helper.rb @@ -114,31 +114,6 @@ def display_rights_statement media_object content_tag(:dt, 'Rights Statement') + content_tag(:dd) { link } end - # Lending period is stored as seconds. Convert to days and hours for display. - def display_lending_period object - d, h = (object.lending_period/3600).divmod(24) - - replacement = { - /^?\s?1\shours/ => ' 1 hour', - /^1\sdays/ => '1 day' - } - - rules = replacement.collect{ |k, v| k } - - matcher = Regexp.union(rules) - - if d == 0 - (h.to_s + ' hours').gsub(matcher, '1 hour') - elsif h == 0 - (d.to_s + ' days').gsub(matcher, '1 day') - else - date_string = "%d days %d hours" % [d, h] - date_string = date_string.gsub(matcher) do |match| - replacement.detect{ |k, v| k =~ match }[1] - end - end - end - def current_quality stream_info available_qualities = Array(stream_info[:stream_flash]).collect {|s| s[:quality]} available_qualities += Array(stream_info[:stream_hls]).collect {|s| s[:quality]} diff --git a/app/views/media_objects/_metadata_display.html.erb b/app/views/media_objects/_metadata_display.html.erb index 4faa3d6d2a..d124841aca 100644 --- a/app/views/media_objects/_metadata_display.html.erb +++ b/app/views/media_objects/_metadata_display.html.erb @@ -73,7 +73,7 @@ Unless required by applicable law or agreed to in writing, software distributed
      <%= @media_object.access_text %>
      - Lending Period: <%= display_lending_period(@media_object) %> + Lending Period: <%= ActiveSupport::Duration.build(@media_object.lending_period).to_human_readable_s %>
      diff --git a/app/views/modules/_access_control.html.erb b/app/views/modules/_access_control.html.erb index 982e8a1b0a..48bfbd65ee 100644 --- a/app/views/modules/_access_control.html.erb +++ b/app/views/modules/_access_control.html.erb @@ -72,7 +72,7 @@ Unless required by applicable law or agreed to in writing, software distributed
      <%= render partial: "modules/tooltip", locals: { form: vid, field: :lending_period, tooltip: t("access_control.#{:lending_period}"), options: {display_label: (t("access_control.#{:lending_period}label")+'*').html_safe} } %>
      - <% value = display_lending_period(object) %> + <% value = ActiveSupport::Duration.build(object.lending_period).to_human_readable_s %> <%= text_field_tag "lending_period", value || '', class: 'form-control' %>
      diff --git a/config/initializers/avalon.rb b/config/initializers/avalon.rb index 9892b90f98..425f032d57 100644 --- a/config/initializers/avalon.rb +++ b/config/initializers/avalon.rb @@ -1,5 +1,6 @@ require 'string_additions' require 'avalon/errors' +require 'human_readable_duration' # Loads configuration information from the YAML file and then sets up the # dropbox # diff --git a/lib/human_readable_duration.rb b/lib/human_readable_duration.rb new file mode 100644 index 0000000000..369c5be2c5 --- /dev/null +++ b/lib/human_readable_duration.rb @@ -0,0 +1,32 @@ +# Copyright 2011-2022, The Trustees of Indiana University and Northwestern +# University. Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed +# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +# CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. +# --- END LICENSE_HEADER BLOCK --- + + +module HumanReadableDuration + def to_human_readable_s + d, h = (self/3600).divmod(24) + + day_string = d > 0 ? d.to_s + ' day'.pluralize(d) : nil + hour_string = h > 0 ? h.to_s + ' hour'.pluralize(h) : nil + + if day_string.nil? + hour_string + elsif hour_string.nil? + day_string + else + day_string + ' ' + hour_string + end + end +end +ActiveSupport::Duration.prepend(HumanReadableDuration) diff --git a/spec/helpers/media_objects_helper_spec.rb b/spec/helpers/media_objects_helper_spec.rb index 1aae720feb..79dcb7c616 100644 --- a/spec/helpers/media_objects_helper_spec.rb +++ b/spec/helpers/media_objects_helper_spec.rb @@ -102,47 +102,4 @@ end end end - - describe '#display_lending_period' do - context 'when lending period is measured in days' do - let(:media_object) { instance_double("MediaObject", lending_period: 172800) } - - subject { helper.display_lending_period(media_object) } - - it 'returns the lending period as a human readable string' do - expect(subject).to eq("2 days") - end - end - context 'when lending period is measured in hours' do - let(:media_object) { instance_double("MediaObject", lending_period: 7200) } - - subject { helper.display_lending_period(media_object) } - - it 'returns the lending period as a human readable string' do - expect(subject).to eq("2 hours") - end - end - context 'when lending period is measured in days and hours' do - let(:media_object) { instance_double("MediaObject", lending_period: 129600) } - - subject { helper.display_lending_period(media_object) } - - it 'returns the lending period as a human readable string' do - expect(subject).to eq("1 day 12 hours") - end - end - context 'when lending period includes 1 day and/or 1 hour' do - let(:day) { instance_double("MediaObject", lending_period: 86400) } - let(:hour) { instance_double("MediaObject", lending_period: 3600) } - let(:day_hour) { instance_double("MediaObject", lending_period: 90000) } - - subject { [helper.display_lending_period(day), helper.display_lending_period(hour), helper.display_lending_period(day_hour)] } - - it 'returns the lending period as a human readable string with singular day and/or hour' do - expect(subject[0]).to eq("1 day") - expect(subject[1]).to eq("1 hour") - expect(subject[2]).to eq("1 day 1 hour") - end - end - end end diff --git a/spec/lib/human_readable_duration_spec.rb b/spec/lib/human_readable_duration_spec.rb new file mode 100644 index 0000000000..98a63e636c --- /dev/null +++ b/spec/lib/human_readable_duration_spec.rb @@ -0,0 +1,52 @@ +# Copyright 2011-2022, The Trustees of Indiana University and Northwestern +# University. Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed +# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +# CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. +# --- END LICENSE_HEADER BLOCK --- + +require 'rails_helper' + +describe 'HumanReadableDuration' do + describe 'to_human_readable_s' do + context 'when lending period is measured in days' do + let(:media_object) { instance_double("MediaObject", lending_period: 172800) } + + it 'returns the lending period as a human readable string' do + expect(ActiveSupport::Duration.build(media_object.lending_period).to_human_readable_s).to eq("2 days") + end + end + context 'when lending period is measured in hours' do + let(:collection) { instance_double("Admin::Collection", lending_period: 7200) } + + it 'returns the lending period as a human readable string' do + expect(ActiveSupport::Duration.build(collection.lending_period).to_human_readable_s).to eq("2 hours") + end + end + context 'when lending period is measured in days and hours' do + let(:media_object) { instance_double("MediaObject", lending_period: 129600) } + + it 'returns the lending period as a human readable string' do + expect(ActiveSupport::Duration.build(media_object.lending_period).to_human_readable_s).to eq("1 day 12 hours") + end + end + context 'when lending period includes 1 day and/or 1 hour' do + let(:day) { instance_double("MediaObject", lending_period: 86400) } + let(:hour) { instance_double("MediaObject", lending_period: 3600) } + let(:day_hour) { instance_double("Admin::Collection", lending_period: 90000) } + + it 'returns the lending period as a human readable string with singular day and/or hour' do + expect(ActiveSupport::Duration.build(day.lending_period).to_human_readable_s).to eq("1 day") + expect(ActiveSupport::Duration.build(hour.lending_period).to_human_readable_s).to eq("1 hour") + expect(ActiveSupport::Duration.build(day_hour.lending_period).to_human_readable_s).to eq("1 day 1 hour") + end + end + end +end From a29d734b146ed226c5729e1668911992196fc246 Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Fri, 15 Jul 2022 16:17:26 -0400 Subject: [PATCH 052/230] Rename variables to fit established patterns Co-authored-by: Chris Colvard --- .../admin/collections_controller.rb | 4 +-- app/jobs/bulk_action_jobs.rb | 2 +- app/models/admin/collection.rb | 3 ++ app/models/concerns/lending_period.rb | 28 +++++++++---------- app/models/media_object.rb | 9 +++++- app/views/admin/collections/show.html.erb | 2 +- app/views/modules/_access_control.html.erb | 11 ++++++-- spec/models/concerns/lending_period_spec.rb | 18 ++++++------ 8 files changed, 46 insertions(+), 31 deletions(-) diff --git a/app/controllers/admin/collections_controller.rb b/app/controllers/admin/collections_controller.rb index a2f69db8e6..a51738566d 100644 --- a/app/controllers/admin/collections_controller.rb +++ b/app/controllers/admin/collections_controller.rb @@ -56,7 +56,7 @@ def show @virtual_groups = @collection.default_virtual_read_groups @ip_groups = @collection.default_ip_read_groups @visibility = @collection.default_visibility - @lending_period = @collection.lending_period + @default_lending_period = @collection.default_lending_period @addable_groups = Admin::Group.non_system_groups.reject { |g| @groups.include? g.name } @addable_courses = Course.all.reject { |c| @virtual_groups.include? c.context_id } @@ -299,7 +299,7 @@ def update_access(collection, params) collection.default_visibility = params[:visibility] unless params[:visibility].blank? collection.default_hidden = params[:hidden] == "1" - collection.lending_period = params[:lending_period] + collection.default_lending_period = params[:default_lending_period] end def apply_access(collection, params) diff --git a/app/jobs/bulk_action_jobs.rb b/app/jobs/bulk_action_jobs.rb index b0f6575a96..cb735cedb0 100644 --- a/app/jobs/bulk_action_jobs.rb +++ b/app/jobs/bulk_action_jobs.rb @@ -205,7 +205,7 @@ def perform(collection_id, overwrite = true) media_object = MediaObject.find(id) media_object.hidden = collection.default_hidden media_object.visibility = collection.default_visibility - media_object.lending_period = collection.lending_period + media_object.lending_period = collection.default_lending_period # Special access if overwrite diff --git a/app/models/admin/collection.rb b/app/models/admin/collection.rb index 3c0c52faa5..9e7c656583 100644 --- a/app/models/admin/collection.rb +++ b/app/models/admin/collection.rb @@ -65,6 +65,9 @@ class Admin::Collection < ActiveFedora::Base property :identifier, predicate: ::RDF::Vocab::Identifiers.local, multiple: true do |index| index.as :symbol end + property :default_lending_period, predicate: ::RDF::Vocab::SCHEMA.eligibleDuration, multiple: false do |index| + index.as :stored_sortable + end has_subresource 'poster', class_name: 'IndexedFile' diff --git a/app/models/concerns/lending_period.rb b/app/models/concerns/lending_period.rb index 11d267e192..6911508308 100644 --- a/app/models/concerns/lending_period.rb +++ b/app/models/concerns/lending_period.rb @@ -3,34 +3,34 @@ module LendingPeriod extend ActiveSupport::Concern included do - property :lending_period, predicate: ::RDF::Vocab::SCHEMA.eligibleDuration, multiple: false do |index| - index.as :stored_sortable - end - after_initialize :set_lending_period end def set_lending_period if self.is_a? Admin::Collection - custom_lend_period - self.lending_period ||= ActiveSupport::Duration.parse(Settings.controlled_digital_lending.default_lending_period).to_i + param_name = 'default_lending_period' + custom_lend_period(param_name) + self.default_lending_period = @duration + self.default_lending_period ||= ActiveSupport::Duration.parse(Settings.controlled_digital_lending.default_lending_period).to_i elsif !self.collection_id.nil? - custom_lend_period - self.lending_period ||= Admin::Collection.find(self.collection_id).lending_period + param_name = 'lending_period' + custom_lend_period(param_name) + self.lending_period = @duration + self.lending_period ||= Admin::Collection.find(self.collection_id).default_lending_period end end private - def custom_lend_period - if (!self.lending_period.nil? && !(self.lending_period.is_a? Integer)) - build_lend_period - self.lending_period = ActiveSupport::Duration.parse(@lend_period).to_i + def custom_lend_period(param_name) + if (!self.send(param_name).nil? && !(self.send(param_name).is_a? Integer)) + build_lend_period(param_name) + @duration = ActiveSupport::Duration.parse(@lend_period).to_i end end - def build_lend_period - @lend_period = self.lending_period.dup + def build_lend_period(param_name) + @lend_period = self.send(param_name).dup replacement = { /\s+days?/i => 'D', diff --git a/app/models/media_object.rb b/app/models/media_object.rb index f987173821..184bbf79dc 100644 --- a/app/models/media_object.rb +++ b/app/models/media_object.rb @@ -112,6 +112,9 @@ def validate_date(date_field) property :comment, predicate: ::RDF::Vocab::EBUCore.comments, multiple: true do |index| index.as :stored_searchable end + property :lending_period, predicate: ::RDF::Vocab::SCHEMA.eligibleDuration, multiple: false do |index| + index.as :stored_sortable + end ordered_aggregation :master_files, class_name: 'MasterFile', through: :list_source # ordered_aggregation gives you accessors media_obj.master_files and media_obj.ordered_master_files @@ -146,7 +149,7 @@ def collection= co self.visibility = co.default_visibility self.read_users = co.default_read_users.to_a self.read_groups = co.default_read_groups.to_a + self.read_groups #Make sure to include any groups added by visibility - self.lending_period = co.lending_period + self.lending_period = co.default_lending_period end end @@ -374,6 +377,10 @@ def lending_status Checkout.active_for_media_object(id).any? ? "checked_out" : "available" end + # def lending_period + # self.lending_period || Settings.controlled_digital_lending.default_lending_period + # end + def current_checkout(user_id) checkouts = Checkout.active_for_media_object(id) checkouts.select{ |ch| ch.user_id == user_id }.first diff --git a/app/views/admin/collections/show.html.erb b/app/views/admin/collections/show.html.erb index 0dcc43f4d9..d7c1c10ee9 100644 --- a/app/views/admin/collections/show.html.erb +++ b/app/views/admin/collections/show.html.erb @@ -128,7 +128,7 @@ Unless required by applicable law or agreed to in writing, software distributed <%= render "modules/access_control", { object: @collection, visibility: @collection.default_visibility, hidden: @collection.default_hidden, - lending_period: @collection.lending_period, + default_lending_period: @collection.default_lending_period, modal: { partial: "apply_access_control", title: "Apply current Default Access settings to all existing Items" } } %> <% if can? :update_access_control, @collection %> diff --git a/app/views/modules/_access_control.html.erb b/app/views/modules/_access_control.html.erb index 48bfbd65ee..c29bbf315e 100644 --- a/app/views/modules/_access_control.html.erb +++ b/app/views/modules/_access_control.html.erb @@ -70,10 +70,15 @@ Unless required by applicable law or agreed to in writing, software distributed
      - <%= render partial: "modules/tooltip", locals: { form: vid, field: :lending_period, tooltip: t("access_control.#{:lending_period}"), options: {display_label: (t("access_control.#{:lending_period}label")+'*').html_safe} } %>
      + <% field = object.is_a?(Admin::Collection) ? :default_lending_period : :lending_period %> + <%= render partial: "modules/tooltip", locals: { form: vid, field: field, tooltip: t("access_control.#{:lending_period}"), options: {display_label: (t("access_control.#{:lending_period}label")+'*').html_safe} } %>
      - <% value = ActiveSupport::Duration.build(object.lending_period).to_human_readable_s %> - <%= text_field_tag "lending_period", value || '', class: 'form-control' %> + <% if object.is_a? Admin::Collection %> + <% value = ActiveSupport::Duration.build(object.default_lending_period).to_human_readable_s %> + <% else %> + <% value = ActiveSupport::Duration.build(object.lending_period).to_human_readable_s %> + <% end %> + <%= text_field_tag field, value || '', class: 'form-control' %>
      diff --git a/spec/models/concerns/lending_period_spec.rb b/spec/models/concerns/lending_period_spec.rb index 45cf90320e..476630cc0d 100644 --- a/spec/models/concerns/lending_period_spec.rb +++ b/spec/models/concerns/lending_period_spec.rb @@ -20,6 +20,10 @@ class Foo < ActiveFedora::Base include LendingPeriod + property :lending_period, predicate: ::RDF::Vocab::SCHEMA.eligibleDuration, multiple: false do |index| + index.as :stored_sortable + end + attr_accessor :collection_id end end @@ -32,10 +36,6 @@ class Foo < ActiveFedora::Base before { subject.collection_id = co.id } - it 'defines lending_period' do - expect(subject.attributes).to include('lending_period') - end - describe 'set_lending_period' do context 'a custom lending period has not been set' do it 'is equal to the default period in the settings.yml' do @@ -45,7 +45,7 @@ class Foo < ActiveFedora::Base end context 'a plain text custom lending period has been set' do let(:media_object) { FactoryBot.create(:media_object, lending_period: "1 day") } - let(:complex_date) { FactoryBot.create(:collection, lending_period: "6 days 4 hours")} + let(:complex_date) { FactoryBot.create(:collection, default_lending_period: "6 days 4 hours")} it 'is equal to the custom lending period measured in seconds' do media_object.set_lending_period expect(media_object.lending_period).to eq 86400 @@ -55,13 +55,13 @@ class Foo < ActiveFedora::Base end end context 'an ISO8601 duration format custom lending period has been set' do - let(:media_object) { FactoryBot.create(:collection, lending_period: "P1D") } + let(:collection) { FactoryBot.create(:collection, default_lending_period: "P1D") } let(:year_month) { FactoryBot.create(:media_object, lending_period: "P1Y2M") } - let(:day_hr_min_sec) { FactoryBot.create(:collection, lending_period: "P4DT6H3M30S")} + let(:day_hr_min_sec) { FactoryBot.create(:collection, default_lending_period: "P4DT6H3M30S")} let(:sec) { FactoryBot.create(:media_object, lending_period: "PT3650.015S")} it 'is equal to the custom lending period measured in seconds' do - media_object.set_lending_period - expect(media_object.lending_period).to eq 86400 + collection.set_lending_period + expect(collection.default_lending_period).to eq 86400 end it 'accepts any ISO8601 duration' do expect { year_month.set_lending_period }.not_to raise_error From 126f641ad82ca382f64ea551eb61e78ffb5675b6 Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Mon, 18 Jul 2022 13:51:18 -0400 Subject: [PATCH 053/230] Refactor lending period form --- .../admin/collections_controller.rb | 7 +- app/models/access_control_step.rb | 4 +- app/models/admin/collection.rb | 7 +- app/models/concerns/lending_period.rb | 63 ---------------- app/models/media_object.rb | 9 ++- app/views/modules/_access_control.html.erb | 27 +++++-- spec/models/concerns/lending_period_spec.rb | 73 ------------------- 7 files changed, 39 insertions(+), 151 deletions(-) delete mode 100644 app/models/concerns/lending_period.rb delete mode 100644 spec/models/concerns/lending_period_spec.rb diff --git a/app/controllers/admin/collections_controller.rb b/app/controllers/admin/collections_controller.rb index a51738566d..2cfec5525f 100644 --- a/app/controllers/admin/collections_controller.rb +++ b/app/controllers/admin/collections_controller.rb @@ -299,7 +299,12 @@ def update_access(collection, params) collection.default_visibility = params[:visibility] unless params[:visibility].blank? collection.default_hidden = params[:hidden] == "1" - collection.default_lending_period = params[:default_lending_period] + collection.default_lending_period = build_default_lending_period + end + + def build_default_lending_period + iso_duration = "P#{params["add_default_lending_period_days"]}DT#{params["add_default_lending_period_hours"]}H" + int_duration = ActiveSupport::Duration.parse(iso_duration).to_i end def apply_access(collection, params) diff --git a/app/models/access_control_step.rb b/app/models/access_control_step.rb index e0961f8187..78acc9a12a 100644 --- a/app/models/access_control_step.rb +++ b/app/models/access_control_step.rb @@ -95,9 +95,11 @@ def execute context lease.destroy end unless limited_access_submit + iso_duration = "P#{context[:add_lending_period_days]}DT#{context[:add_lending_period_hours]}H" + int_duration = ActiveSupport::Duration.parse(iso_duration).to_i media_object.visibility = context[:visibility] unless context[:visibility].blank? media_object.hidden = context[:hidden] == "1" - media_object.lending_period = context[:lending_period] + media_object.lending_period = int_duration end media_object.save! diff --git a/app/models/admin/collection.rb b/app/models/admin/collection.rb index 9e7c656583..a000188cfb 100644 --- a/app/models/admin/collection.rb +++ b/app/models/admin/collection.rb @@ -22,7 +22,7 @@ class Admin::Collection < ActiveFedora::Base include ActiveFedora::Associations include Identifier include MigrationTarget - include LendingPeriod + # include LendingPeriod has_many :media_objects, class_name: 'MediaObject', predicate: ActiveFedora::RDF::Fcrepo::RelsExt.isMemberOfCollection @@ -71,6 +71,7 @@ class Admin::Collection < ActiveFedora::Base has_subresource 'poster', class_name: 'IndexedFile' + before_save :set_default_lending_period around_save :reindex_members, if: Proc.new{ |c| c.name_changed? or c.unit_changed? } before_create :create_dropbox_directory! @@ -269,6 +270,10 @@ def default_visibility_changed? !!@default_visibility_will_change end + def set_default_lending_period + self.default_lending_period ||= Settings.controlled_digital_lending.default_lending_period.to_i + end + private def remove_edit_user(name) diff --git a/app/models/concerns/lending_period.rb b/app/models/concerns/lending_period.rb deleted file mode 100644 index 6911508308..0000000000 --- a/app/models/concerns/lending_period.rb +++ /dev/null @@ -1,63 +0,0 @@ -module LendingPeriod - - extend ActiveSupport::Concern - - included do - after_initialize :set_lending_period - end - - def set_lending_period - if self.is_a? Admin::Collection - param_name = 'default_lending_period' - custom_lend_period(param_name) - self.default_lending_period = @duration - self.default_lending_period ||= ActiveSupport::Duration.parse(Settings.controlled_digital_lending.default_lending_period).to_i - elsif !self.collection_id.nil? - param_name = 'lending_period' - custom_lend_period(param_name) - self.lending_period = @duration - self.lending_period ||= Admin::Collection.find(self.collection_id).default_lending_period - end - end - - private - - def custom_lend_period(param_name) - if (!self.send(param_name).nil? && !(self.send(param_name).is_a? Integer)) - build_lend_period(param_name) - @duration = ActiveSupport::Duration.parse(@lend_period).to_i - end - end - - def build_lend_period(param_name) - @lend_period = self.send(param_name).dup - - replacement = { - /\s+days?/i => 'D', - /\s+hours?/i => 'H', - /,?\s+/ => 'T' - } - - rules = replacement.collect{ |k, v| k } - - matcher = Regexp.union(rules) - - @lend_period = @lend_period.gsub(matcher) do |match| - replacement.detect{ |k, v| k =~ match }[1] - end - - @lend_period.match(/P/) ? @lend_period : build_iso8601_duration(@lend_period) - end - - def build_iso8601_duration(lend_period) - if @lend_period.match?(/D/) - unless @lend_period.include? 'P' - @lend_period.prepend('P') - end - else - unless @lend_period.include? 'PT' - @lend_period.prepend('PT') - end - end - end -end diff --git a/app/models/media_object.rb b/app/models/media_object.rb index 184bbf79dc..39f7356354 100644 --- a/app/models/media_object.rb +++ b/app/models/media_object.rb @@ -25,7 +25,7 @@ class MediaObject < ActiveFedora::Base include SpeedyAF::OrderedAggregationIndex include MediaObjectIntercom include SupplementalFileBehavior - include LendingPeriod + # include LendingPeriod require 'avalon/controlled_vocabulary' include Kaminari::ActiveFedoraModelExtension @@ -36,6 +36,7 @@ class MediaObject < ActiveFedora::Base before_save :update_dependent_properties!, prepend: true before_save :update_permalink, if: Proc.new { |mo| mo.persisted? && mo.published? }, prepend: true before_save :assign_id!, prepend: true + before_save :set_lending_period after_save :update_dependent_permalinks_job, if: Proc.new { |mo| mo.persisted? && mo.published? } after_save :remove_bookmarks @@ -377,9 +378,9 @@ def lending_status Checkout.active_for_media_object(id).any? ? "checked_out" : "available" end - # def lending_period - # self.lending_period || Settings.controlled_digital_lending.default_lending_period - # end + def set_lending_period + self.lending_period ||= Admin::Collection.find(self.collection_id).default_lending_period + end def current_checkout(user_id) checkouts = Checkout.active_for_media_object(id) diff --git a/app/views/modules/_access_control.html.erb b/app/views/modules/_access_control.html.erb index c29bbf315e..a7aeb27cf2 100644 --- a/app/views/modules/_access_control.html.erb +++ b/app/views/modules/_access_control.html.erb @@ -72,13 +72,24 @@ Unless required by applicable law or agreed to in writing, software distributed
      <% field = object.is_a?(Admin::Collection) ? :default_lending_period : :lending_period %> <%= render partial: "modules/tooltip", locals: { form: vid, field: field, tooltip: t("access_control.#{:lending_period}"), options: {display_label: (t("access_control.#{:lending_period}label")+'*').html_safe} } %>
      -
      - <% if object.is_a? Admin::Collection %> - <% value = ActiveSupport::Duration.build(object.default_lending_period).to_human_readable_s %> - <% else %> - <% value = ActiveSupport::Duration.build(object.lending_period).to_human_readable_s %> - <% end %> - <%= text_field_tag field, value || '', class: 'form-control' %> + <% if object.is_a? Admin::Collection %> + <% d, h = (object.default_lending_period/3600).divmod(24) %> + <% else %> + <% d, h = (object.lending_period/3600).divmod(24) %> + <% end %> +
      +
      + + <%= text_field_tag "add_#{field}_days", d ? d : 0, class: 'form-control' %> +
      +
      + + <%= text_field_tag "add_#{field}_hours", h ? h : 0, class: 'form-control' %> +
      @@ -131,7 +142,7 @@ Unless required by applicable law or agreed to in writing, software distributed
      Item is available to be checked out for - <%= display_lending_period(object) %> + <%= ActiveSupport::Duration.build(object.lending_period).to_human_readable_s %>
      diff --git a/spec/models/concerns/lending_period_spec.rb b/spec/models/concerns/lending_period_spec.rb deleted file mode 100644 index 476630cc0d..0000000000 --- a/spec/models/concerns/lending_period_spec.rb +++ /dev/null @@ -1,73 +0,0 @@ -# Copyright 2011-2022, The Trustees of Indiana University and Northwestern -# University. Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software distributed -# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR -# CONDITIONS OF ANY KIND, either express or implied. See the License for the -# specific language governing permissions and limitations under the License. -# --- END LICENSE_HEADER BLOCK --- - -require 'rails_helper' - -describe LendingPeriod do - - before(:all) do - class Foo < ActiveFedora::Base - include LendingPeriod - - property :lending_period, predicate: ::RDF::Vocab::SCHEMA.eligibleDuration, multiple: false do |index| - index.as :stored_sortable - end - - attr_accessor :collection_id - end - end - - after(:all) { Object.send(:remove_const, :Foo) } - - subject { Foo.new } - - let(:co) { FactoryBot.create(:collection) } - - before { subject.collection_id = co.id } - - describe 'set_lending_period' do - context 'a custom lending period has not been set' do - it 'is equal to the default period in the settings.yml' do - subject.set_lending_period - expect(subject.lending_period).to eq ActiveSupport::Duration.parse(Settings.controlled_digital_lending.default_lending_period).to_i - end - end - context 'a plain text custom lending period has been set' do - let(:media_object) { FactoryBot.create(:media_object, lending_period: "1 day") } - let(:complex_date) { FactoryBot.create(:collection, default_lending_period: "6 days 4 hours")} - it 'is equal to the custom lending period measured in seconds' do - media_object.set_lending_period - expect(media_object.lending_period).to eq 86400 - end - it 'accepts strings containing day and hour' do - expect { complex_date.set_lending_period }.not_to raise_error - end - end - context 'an ISO8601 duration format custom lending period has been set' do - let(:collection) { FactoryBot.create(:collection, default_lending_period: "P1D") } - let(:year_month) { FactoryBot.create(:media_object, lending_period: "P1Y2M") } - let(:day_hr_min_sec) { FactoryBot.create(:collection, default_lending_period: "P4DT6H3M30S")} - let(:sec) { FactoryBot.create(:media_object, lending_period: "PT3650.015S")} - it 'is equal to the custom lending period measured in seconds' do - collection.set_lending_period - expect(collection.default_lending_period).to eq 86400 - end - it 'accepts any ISO8601 duration' do - expect { year_month.set_lending_period }.not_to raise_error - expect { day_hr_min_sec.set_lending_period }.not_to raise_error - expect { sec.set_lending_period }.not_to raise_error - end - end - end -end From b649f1c32c75cf145e82df5500c97e23fedca2c6 Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Mon, 18 Jul 2022 17:15:48 -0400 Subject: [PATCH 054/230] Update and fix tests --- app/controllers/admin/collections_controller.rb | 2 +- spec/jobs/bulk_action_job_spec.rb | 4 ++-- spec/lib/human_readable_duration_spec.rb | 8 ++++---- spec/models/media_object_spec.rb | 2 +- spec/requests/checkouts_spec.rb | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/app/controllers/admin/collections_controller.rb b/app/controllers/admin/collections_controller.rb index 2cfec5525f..5e91641832 100644 --- a/app/controllers/admin/collections_controller.rb +++ b/app/controllers/admin/collections_controller.rb @@ -304,7 +304,7 @@ def update_access(collection, params) def build_default_lending_period iso_duration = "P#{params["add_default_lending_period_days"]}DT#{params["add_default_lending_period_hours"]}H" - int_duration = ActiveSupport::Duration.parse(iso_duration).to_i + int_duration = iso_duration.is_a?(Integer) ? ActiveSupport::Duration.parse(iso_duration).to_i : Settings.controlled_digital_lending.default_lending_period.to_i end def apply_access(collection, params) diff --git a/spec/jobs/bulk_action_job_spec.rb b/spec/jobs/bulk_action_job_spec.rb index 3fa810d980..db8af16bc4 100644 --- a/spec/jobs/bulk_action_job_spec.rb +++ b/spec/jobs/bulk_action_job_spec.rb @@ -79,7 +79,7 @@ def check_push(result) co.default_read_groups = ["co_group"] co.default_hidden = true co.default_visibility = 'public' - co.lending_period = 129600 + co.default_lending_period = 129600 co.save! mo.read_users = ["mo_user"] @@ -100,7 +100,7 @@ def check_push(result) it "changes item lending period" do BulkActionJobs::ApplyCollectionAccessControl.perform_now co.id, true mo.reload - expect(mo.lending_period).to eq(co.lending_period) + expect(mo.lending_period).to eq(co.default_lending_period) end context "overwrite is true" do diff --git a/spec/lib/human_readable_duration_spec.rb b/spec/lib/human_readable_duration_spec.rb index 98a63e636c..1abf66eb81 100644 --- a/spec/lib/human_readable_duration_spec.rb +++ b/spec/lib/human_readable_duration_spec.rb @@ -24,10 +24,10 @@ end end context 'when lending period is measured in hours' do - let(:collection) { instance_double("Admin::Collection", lending_period: 7200) } + let(:collection) { instance_double("Admin::Collection", default_lending_period: 7200) } it 'returns the lending period as a human readable string' do - expect(ActiveSupport::Duration.build(collection.lending_period).to_human_readable_s).to eq("2 hours") + expect(ActiveSupport::Duration.build(collection.default_lending_period).to_human_readable_s).to eq("2 hours") end end context 'when lending period is measured in days and hours' do @@ -40,12 +40,12 @@ context 'when lending period includes 1 day and/or 1 hour' do let(:day) { instance_double("MediaObject", lending_period: 86400) } let(:hour) { instance_double("MediaObject", lending_period: 3600) } - let(:day_hour) { instance_double("Admin::Collection", lending_period: 90000) } + let(:day_hour) { instance_double("Admin::Collection", default_lending_period: 90000) } it 'returns the lending period as a human readable string with singular day and/or hour' do expect(ActiveSupport::Duration.build(day.lending_period).to_human_readable_s).to eq("1 day") expect(ActiveSupport::Duration.build(hour.lending_period).to_human_readable_s).to eq("1 hour") - expect(ActiveSupport::Duration.build(day_hour.lending_period).to_human_readable_s).to eq("1 day 1 hour") + expect(ActiveSupport::Duration.build(day_hour.default_lending_period).to_human_readable_s).to eq("1 day 1 hour") end end end diff --git a/spec/models/media_object_spec.rb b/spec/models/media_object_spec.rb index 964c75396d..7505ad620c 100644 --- a/spec/models/media_object_spec.rb +++ b/spec/models/media_object_spec.rb @@ -770,7 +770,7 @@ describe '#collection=' do let(:new_media_object) { MediaObject.new } - let(:collection) { FactoryBot.create(:collection, default_hidden: true, default_read_users: ['archivist1@example.com'], default_read_groups: ['TestGroup', 'public'], lending_period: 86400)} + let(:collection) { FactoryBot.create(:collection, default_hidden: true, default_read_users: ['archivist1@example.com'], default_read_groups: ['TestGroup', 'public'], default_lending_period: 86400)} it 'sets hidden based upon collection for new media objects' do expect {new_media_object.collection = collection}.to change {new_media_object.hidden?}.to(true).from(false) diff --git a/spec/requests/checkouts_spec.rb b/spec/requests/checkouts_spec.rb index a280f457f4..1c1cc71ab4 100644 --- a/spec/requests/checkouts_spec.rb +++ b/spec/requests/checkouts_spec.rb @@ -152,7 +152,7 @@ end context "non-default lending period" do - let(:media_object) { FactoryBot.create(:published_media_object, lending_period: "1 day", visibility: 'public') } + let(:media_object) { FactoryBot.create(:published_media_object, lending_period: 86400, visibility: 'public') } before { freeze_time } after { travel_back } it "creates a new checkout" do From 831e2c9b12aadc1a150c7a09b81f0d821d856609 Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Tue, 19 Jul 2022 13:51:22 -0400 Subject: [PATCH 055/230] Add additional tests and fix lending period setters --- app/models/admin/collection.rb | 3 +-- app/models/media_object.rb | 1 - spec/features/media_object_spec.rb | 7 +++++++ spec/models/collection_spec.rb | 18 +++++++++++++++++- spec/models/media_object_spec.rb | 24 ++++++++++++++++++++++++ 5 files changed, 49 insertions(+), 4 deletions(-) diff --git a/app/models/admin/collection.rb b/app/models/admin/collection.rb index a000188cfb..b929591a7b 100644 --- a/app/models/admin/collection.rb +++ b/app/models/admin/collection.rb @@ -22,7 +22,6 @@ class Admin::Collection < ActiveFedora::Base include ActiveFedora::Associations include Identifier include MigrationTarget - # include LendingPeriod has_many :media_objects, class_name: 'MediaObject', predicate: ActiveFedora::RDF::Fcrepo::RelsExt.isMemberOfCollection @@ -271,7 +270,7 @@ def default_visibility_changed? end def set_default_lending_period - self.default_lending_period ||= Settings.controlled_digital_lending.default_lending_period.to_i + self.default_lending_period ||= ActiveSupport::Duration.parse(Settings.controlled_digital_lending.default_lending_period).to_i end private diff --git a/app/models/media_object.rb b/app/models/media_object.rb index 39f7356354..eaad3cafc4 100644 --- a/app/models/media_object.rb +++ b/app/models/media_object.rb @@ -25,7 +25,6 @@ class MediaObject < ActiveFedora::Base include SpeedyAF::OrderedAggregationIndex include MediaObjectIntercom include SupplementalFileBehavior - # include LendingPeriod require 'avalon/controlled_vocabulary' include Kaminari::ActiveFedoraModelExtension diff --git a/spec/features/media_object_spec.rb b/spec/features/media_object_spec.rb index fd4d9b86c4..798cd6302a 100644 --- a/spec/features/media_object_spec.rb +++ b/spec/features/media_object_spec.rb @@ -64,5 +64,12 @@ visit media_object_path(media_object) expect(page.has_content?(contributor)).to be_truthy end + it 'displays the lending period properly' do + lending_period = 90000 + media_object.lending_period = lending_period + media_object.save + visit media_object_path(media_object) + expect(page.has_content?('1 day 1 hour')).to be_truthy + end end end diff --git a/spec/models/collection_spec.rb b/spec/models/collection_spec.rb index 494f08744a..f6bc53e373 100644 --- a/spec/models/collection_spec.rb +++ b/spec/models/collection_spec.rb @@ -587,7 +587,7 @@ Settings.dropbox.path = "s3://#{bucket}/dropbox" end - it "should be able to handle special S3 avoidable characters and create object" do + it "should be able to handle special S3 avoidable characters and create object" do remote_object = double(key: corrected_collection_name, bucket_name: bucket, exists?: false) allow(Aws::S3::Client).to receive(:new).and_return(my_client) allow(Aws::S3::Object).to receive(:new).and_return(remote_object) @@ -601,4 +601,20 @@ Settings.dropbox.path = old_path end end + + describe 'set_default_lending_period' do + context 'a custom lending period has not been set' do + it 'sets the lending period equal to the system default' do + collection.set_default_lending_period + expect(collection.default_lending_period).to eq ActiveSupport::Duration.parse(Settings.controlled_digital_lending.default_lending_period).to_i + end + end + context 'a custom lending period has been set' do + let(:collection) { FactoryBot.create(:collection, default_lending_period: 86400) } + it 'leaves the lending period equal to the custom value' do + collection.set_default_lending_period + expect(collection.default_lending_period).to eq 86400 + end + end + end end diff --git a/spec/models/media_object_spec.rb b/spec/models/media_object_spec.rb index 7505ad620c..333c3c5a91 100644 --- a/spec/models/media_object_spec.rb +++ b/spec/models/media_object_spec.rb @@ -1024,4 +1024,28 @@ end end end + + describe 'set_lending_period' do + context 'there is not a custom lending period' do + it 'sets the lending period to the system default' do + media_object.set_lending_period + expect(media_object.lending_period).to eq ActiveSupport::Duration.parse(Settings.controlled_digital_lending.default_lending_period).to_i + end + end + context 'the parent collection has a custom lending period' do + let(:collection) { FactoryBot.create(:collection, default_lending_period: 86400) } + let(:media_object) { FactoryBot.create(:media_object, collection_id: collection.id) } + it "sets the lending period to equal the collection's default lending period" do + media_object.set_lending_period + expect(media_object.lending_period).to eq collection.default_lending_period + end + context 'the media object has a custom lending period' do + let(:media_object) { FactoryBot.create(:media_object, collection_id: collection.id, lending_period: 172800)} + it "leaves the lending period equal to the custom value" do + media_object.set_lending_period + expect(media_object.lending_period).to eq 172800 + end + end + end + end end From 398755f714045ffbba9ed0843a05afef0488b959 Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Tue, 19 Jul 2022 16:58:58 -0400 Subject: [PATCH 056/230] Change lending period tool tip --- config/locales/en.yml | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/config/locales/en.yml b/config/locales/en.yml index b5c5532cd2..2e2dbb73a8 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -171,12 +171,7 @@ en: or ffaa:aaff:bbcc:ddee:1122:3344:5566:7777/ffff:ffff:ffff:ffff:ffff:ffff:ffff:ff00 Start and end dates are not required; if included, access for the specified IP Address(es) will start at the beginning of the start date and end at the beginning of the end date. Otherwise, access will be open ended for the specified IP Address(es). lending_period: | - Lending Period is the length of time that an item can be checked out for. - Accepted formats are ISO8601 duration (ex. P1DT12H) or a human-readable - string using numerals without punctuation or conjunctions (ex. 1 day 12 hours). - Values entered as strings may only contain days and/or hours and must use whole numbers. - For example, 1 month would be entered as '30 days' and 1.5 days would - be entered as '1 day 12 hours' or '36 hours'. + Lending Period is the length of time that an item may be checked out for. contact: title: 'Contact Us - %{application_name}' From d80c2a777adc7c505c89a120aaf053c560dc236d Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Wed, 20 Jul 2022 11:32:31 -0400 Subject: [PATCH 057/230] Refactor Co-authored-by: Chris Colvard --- app/controllers/admin/collections_controller.rb | 7 ++++--- app/models/checkout.rb | 4 +--- app/models/media_object.rb | 2 +- app/views/admin/collections/show.html.erb | 2 +- .../media_objects/_access_control.html.erb | 5 ++++- .../media_objects/_metadata_display.html.erb | 2 +- app/views/modules/_access_control.html.erb | 17 ++++++----------- config/initializers/avalon.rb | 2 +- ..._readable_duration.rb => day_hour_string.rb} | 6 +++--- ...duration_spec.rb => day_hour_string_spec.rb} | 12 ++++++------ 10 files changed, 28 insertions(+), 31 deletions(-) rename lib/{human_readable_duration.rb => day_hour_string.rb} (89%) rename spec/lib/{human_readable_duration_spec.rb => day_hour_string_spec.rb} (88%) diff --git a/app/controllers/admin/collections_controller.rb b/app/controllers/admin/collections_controller.rb index 5e91641832..f928b97579 100644 --- a/app/controllers/admin/collections_controller.rb +++ b/app/controllers/admin/collections_controller.rb @@ -299,12 +299,13 @@ def update_access(collection, params) collection.default_visibility = params[:visibility] unless params[:visibility].blank? collection.default_hidden = params[:hidden] == "1" - collection.default_lending_period = build_default_lending_period + collection.default_lending_period = build_default_lending_period.to_i end def build_default_lending_period - iso_duration = "P#{params["add_default_lending_period_days"]}DT#{params["add_default_lending_period_hours"]}H" - int_duration = iso_duration.is_a?(Integer) ? ActiveSupport::Duration.parse(iso_duration).to_i : Settings.controlled_digital_lending.default_lending_period.to_i + params["add_lending_period_days"].days + params["add_lending_period_hours"].hours + rescue + Settings.controlled_digital_lending.default_lending_period end def apply_access(collection, params) diff --git a/app/models/checkout.rb b/app/models/checkout.rb index 0ff8752095..dd7388ea53 100644 --- a/app/models/checkout.rb +++ b/app/models/checkout.rb @@ -21,9 +21,7 @@ def set_checkout_return_times! end def duration - if !media_object_id.nil? - duration = media_object.lending_period - end + duration = media_object.lending_period if media_object_id.present? duration ||= ActiveSupport::Duration.parse(Settings.controlled_digital_lending.default_lending_period) duration end diff --git a/app/models/media_object.rb b/app/models/media_object.rb index eaad3cafc4..19b9fa9699 100644 --- a/app/models/media_object.rb +++ b/app/models/media_object.rb @@ -378,7 +378,7 @@ def lending_status end def set_lending_period - self.lending_period ||= Admin::Collection.find(self.collection_id).default_lending_period + self.lending_period ||= collection.default_lending_period end def current_checkout(user_id) diff --git a/app/views/admin/collections/show.html.erb b/app/views/admin/collections/show.html.erb index d7c1c10ee9..f2722f361f 100644 --- a/app/views/admin/collections/show.html.erb +++ b/app/views/admin/collections/show.html.erb @@ -128,7 +128,7 @@ Unless required by applicable law or agreed to in writing, software distributed <%= render "modules/access_control", { object: @collection, visibility: @collection.default_visibility, hidden: @collection.default_hidden, - default_lending_period: @collection.default_lending_period, + lending_period: @collection.default_lending_period, modal: { partial: "apply_access_control", title: "Apply current Default Access settings to all existing Items" } } %> <% if can? :update_access_control, @collection %> diff --git a/app/views/media_objects/_access_control.html.erb b/app/views/media_objects/_access_control.html.erb index b69bf7ec65..57ab2e21f3 100644 --- a/app/views/media_objects/_access_control.html.erb +++ b/app/views/media_objects/_access_control.html.erb @@ -14,5 +14,8 @@ Unless required by applicable law or agreed to in writing, software distributed --- END LICENSE_HEADER BLOCK --- %> - <%= render 'modules/access_control', {object: @media_object, visibility: @media_object.visibility, hidden: @media_object.hidden?} %> + <%= render 'modules/access_control', {object: @media_object, + visibility: @media_object.visibility, + hidden: @media_object.hidden?, + lending_period: @media_object.lending_period} %> <%= render 'workflow_buttons', form: 'access_control_form' %> diff --git a/app/views/media_objects/_metadata_display.html.erb b/app/views/media_objects/_metadata_display.html.erb index d124841aca..84a39fa0a1 100644 --- a/app/views/media_objects/_metadata_display.html.erb +++ b/app/views/media_objects/_metadata_display.html.erb @@ -73,7 +73,7 @@ Unless required by applicable law or agreed to in writing, software distributed
      <%= @media_object.access_text %>
      - Lending Period: <%= ActiveSupport::Duration.build(@media_object.lending_period).to_human_readable_s %> + Lending Period: <%= ActiveSupport::Duration.build(@media_object.lending_period).to_day_hour_s %>
      diff --git a/app/views/modules/_access_control.html.erb b/app/views/modules/_access_control.html.erb index a7aeb27cf2..fa90c387d7 100644 --- a/app/views/modules/_access_control.html.erb +++ b/app/views/modules/_access_control.html.erb @@ -70,25 +70,20 @@ Unless required by applicable law or agreed to in writing, software distributed
      - <% field = object.is_a?(Admin::Collection) ? :default_lending_period : :lending_period %> - <%= render partial: "modules/tooltip", locals: { form: vid, field: field, tooltip: t("access_control.#{:lending_period}"), options: {display_label: (t("access_control.#{:lending_period}label")+'*').html_safe} } %>
      - <% if object.is_a? Admin::Collection %> - <% d, h = (object.default_lending_period/3600).divmod(24) %> - <% else %> - <% d, h = (object.lending_period/3600).divmod(24) %> - <% end %> + <%= render partial: "modules/tooltip", locals: { form: vid, field: :lending_period, tooltip: t("access_control.#{:lending_period}"), options: {display_label: (t("access_control.#{:lending_period}label")+'*').html_safe} } %>
      + <% d, h = (lending_period/3600).divmod(24) %>
      -
      -
      diff --git a/config/initializers/avalon.rb b/config/initializers/avalon.rb index 425f032d57..d565fffa9d 100644 --- a/config/initializers/avalon.rb +++ b/config/initializers/avalon.rb @@ -1,6 +1,6 @@ require 'string_additions' require 'avalon/errors' -require 'human_readable_duration' +require 'day_hour_string' # Loads configuration information from the YAML file and then sets up the # dropbox # diff --git a/lib/human_readable_duration.rb b/lib/day_hour_string.rb similarity index 89% rename from lib/human_readable_duration.rb rename to lib/day_hour_string.rb index 369c5be2c5..429c1f6a53 100644 --- a/lib/human_readable_duration.rb +++ b/lib/day_hour_string.rb @@ -13,8 +13,8 @@ # --- END LICENSE_HEADER BLOCK --- -module HumanReadableDuration - def to_human_readable_s +module DayHourString + def to_day_hour_s d, h = (self/3600).divmod(24) day_string = d > 0 ? d.to_s + ' day'.pluralize(d) : nil @@ -29,4 +29,4 @@ def to_human_readable_s end end end -ActiveSupport::Duration.prepend(HumanReadableDuration) +ActiveSupport::Duration.prepend(DayHourString) diff --git a/spec/lib/human_readable_duration_spec.rb b/spec/lib/day_hour_string_spec.rb similarity index 88% rename from spec/lib/human_readable_duration_spec.rb rename to spec/lib/day_hour_string_spec.rb index 1abf66eb81..f6856c99df 100644 --- a/spec/lib/human_readable_duration_spec.rb +++ b/spec/lib/day_hour_string_spec.rb @@ -20,21 +20,21 @@ let(:media_object) { instance_double("MediaObject", lending_period: 172800) } it 'returns the lending period as a human readable string' do - expect(ActiveSupport::Duration.build(media_object.lending_period).to_human_readable_s).to eq("2 days") + expect(ActiveSupport::Duration.build(media_object.lending_period).to_day_hour_s).to eq("2 days") end end context 'when lending period is measured in hours' do let(:collection) { instance_double("Admin::Collection", default_lending_period: 7200) } it 'returns the lending period as a human readable string' do - expect(ActiveSupport::Duration.build(collection.default_lending_period).to_human_readable_s).to eq("2 hours") + expect(ActiveSupport::Duration.build(collection.default_lending_period).to_day_hour_s).to eq("2 hours") end end context 'when lending period is measured in days and hours' do let(:media_object) { instance_double("MediaObject", lending_period: 129600) } it 'returns the lending period as a human readable string' do - expect(ActiveSupport::Duration.build(media_object.lending_period).to_human_readable_s).to eq("1 day 12 hours") + expect(ActiveSupport::Duration.build(media_object.lending_period).to_day_hour_s).to eq("1 day 12 hours") end end context 'when lending period includes 1 day and/or 1 hour' do @@ -43,9 +43,9 @@ let(:day_hour) { instance_double("Admin::Collection", default_lending_period: 90000) } it 'returns the lending period as a human readable string with singular day and/or hour' do - expect(ActiveSupport::Duration.build(day.lending_period).to_human_readable_s).to eq("1 day") - expect(ActiveSupport::Duration.build(hour.lending_period).to_human_readable_s).to eq("1 hour") - expect(ActiveSupport::Duration.build(day_hour.default_lending_period).to_human_readable_s).to eq("1 day 1 hour") + expect(ActiveSupport::Duration.build(day.lending_period).to_day_hour_s).to eq("1 day") + expect(ActiveSupport::Duration.build(hour.lending_period).to_day_hour_s).to eq("1 hour") + expect(ActiveSupport::Duration.build(day_hour.default_lending_period).to_day_hour_s).to eq("1 day 1 hour") end end end From 438b6f27f52501ab35887a51c90a8b9036345afb Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Wed, 20 Jul 2022 16:11:52 -0400 Subject: [PATCH 058/230] Refactor access control step and collection controller Co-authored-by: Chris Colvard --- app/controllers/admin/collections_controller.rb | 2 +- app/models/access_control_step.rb | 13 ++++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/app/controllers/admin/collections_controller.rb b/app/controllers/admin/collections_controller.rb index f928b97579..3233d06b5e 100644 --- a/app/controllers/admin/collections_controller.rb +++ b/app/controllers/admin/collections_controller.rb @@ -303,7 +303,7 @@ def update_access(collection, params) end def build_default_lending_period - params["add_lending_period_days"].days + params["add_lending_period_hours"].hours + params["add_lending_period_days"].to_i.days + params["add_lending_period_hours"].to_i.hours rescue Settings.controlled_digital_lending.default_lending_period end diff --git a/app/models/access_control_step.rb b/app/models/access_control_step.rb index 78acc9a12a..84d18d08ae 100644 --- a/app/models/access_control_step.rb +++ b/app/models/access_control_step.rb @@ -95,11 +95,11 @@ def execute context lease.destroy end unless limited_access_submit - iso_duration = "P#{context[:add_lending_period_days]}DT#{context[:add_lending_period_hours]}H" - int_duration = ActiveSupport::Duration.parse(iso_duration).to_i media_object.visibility = context[:visibility] unless context[:visibility].blank? media_object.hidden = context[:hidden] == "1" - media_object.lending_period = int_duration + d = context['add_lending_period_days'] + h = context['add_lending_period_hours'] + media_object.lending_period = build_lending_period(d, h).to_i end media_object.save! @@ -118,4 +118,11 @@ def execute context context[:lending_period] = media_object.lending_period context end + + private + def build_lending_period(d, h) + int_duration = d.to_i.days + h.to_i.hours + rescue + int_duration = Settings.controlled_digital_lending.default_lending_period + end end From c1f0b22cf70c788b4d6f7ce1d0ef3862b84c01a8 Mon Sep 17 00:00:00 2001 From: Mason Ballengee <68433277+masaball@users.noreply.github.com> Date: Thu, 21 Jul 2022 09:36:48 -0400 Subject: [PATCH 059/230] Update app/views/modules/_access_control.html.erb Co-authored-by: Chris Colvard --- app/views/modules/_access_control.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/modules/_access_control.html.erb b/app/views/modules/_access_control.html.erb index fa90c387d7..78b1b7d3e8 100644 --- a/app/views/modules/_access_control.html.erb +++ b/app/views/modules/_access_control.html.erb @@ -137,7 +137,7 @@ Unless required by applicable law or agreed to in writing, software distributed
      Item is available to be checked out for - <%= ActiveSupport::Duration.build(object.lending_period).to_human_readable_s %> + <%= ActiveSupport::Duration.build(object.lending_period).to_day_hour_s %>
      From 6752bcd915d844786a4f9f6dffa3d8a94242d144 Mon Sep 17 00:00:00 2001 From: Mason Ballengee <68433277+masaball@users.noreply.github.com> Date: Thu, 21 Jul 2022 10:34:07 -0400 Subject: [PATCH 060/230] Remove unnecessary object.lending_period call lending_period is set as part of the render partial call. Doing an object.lending_period call is unneeded and will cause an error for collections. --- app/views/modules/_access_control.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/modules/_access_control.html.erb b/app/views/modules/_access_control.html.erb index 78b1b7d3e8..de61bb4850 100644 --- a/app/views/modules/_access_control.html.erb +++ b/app/views/modules/_access_control.html.erb @@ -137,7 +137,7 @@ Unless required by applicable law or agreed to in writing, software distributed
      Item is available to be checked out for - <%= ActiveSupport::Duration.build(object.lending_period).to_day_hour_s %> + <%= ActiveSupport::Duration.build(lending_period).to_day_hour_s %>
      From a4f0ee2e47d94f3a9046e61cc767d447e9239780 Mon Sep 17 00:00:00 2001 From: Chris Colvard Date: Thu, 21 Jul 2022 11:43:17 -0400 Subject: [PATCH 061/230] Appease rubocop --- app/controllers/checkouts_controller.rb | 20 ++++++++------------ app/models/access_control_step.rb | 11 ++++++----- app/models/checkout.rb | 18 +++++++++--------- lib/day_hour_string.rb | 7 +++---- 4 files changed, 26 insertions(+), 30 deletions(-) diff --git a/app/controllers/checkouts_controller.rb b/app/controllers/checkouts_controller.rb index 010d8e2eb9..05f6026ec6 100644 --- a/app/controllers/checkouts_controller.rb +++ b/app/controllers/checkouts_controller.rb @@ -5,8 +5,6 @@ class CheckoutsController < ApplicationController # GET /checkouts or /checkouts.json def index - @checkouts - respond_to do |format| format.html { render :index } format.json do @@ -25,8 +23,7 @@ def index end # GET /checkouts/1.json - def show - end + def show; end # POST /checkouts or /checkouts.json def create @@ -35,7 +32,7 @@ def create respond_to do |format| # TODO: move this can? check into a checkout ability (can?(:create, @checkout)) if can?(:create, @checkout) && @checkout.save - format.html { redirect_to media_object_path(checkout_params[:media_object_id]), flash: { success: "Checkout was successfully created."} } + format.html { redirect_to media_object_path(checkout_params[:media_object_id]), flash: { success: "Checkout was successfully created." } } format.json { render :show, status: :created, location: @checkout } else format.json { render json: @checkout.errors, status: :unprocessable_entity } @@ -55,18 +52,17 @@ def update end end - #PATCH /checkouts/1/return + # PATCH /checkouts/1/return def return @checkout.update(return_time: DateTime.current) flash[:notice] = "Checkout was successfully returned." respond_to do |format| format.html { redirect_back fallback_location: checkouts_url, notice: flash[:notice] } - format.json { render json:flash[:notice] } + format.json { render json: flash[:notice] } end end - # PATCH /checkouts/return_all def return_all @checkouts.each { |c| c.update(return_time: DateTime.current) } @@ -83,7 +79,7 @@ def destroy flash[:notice] = "Checkout was successfully destroyed." respond_to do |format| format.html { redirect_to checkouts_url, notice: flash[:notice] } - format.json { render json:flash[:notice] } + format.json { render json: flash[:notice] } end end @@ -95,14 +91,14 @@ def set_checkout end def set_checkouts - unless params[:display_returned] == 'true' - @checkouts = set_active_checkouts - else + if params[:display_returned] == 'true' @checkouts = if current_ability.is_administrator? set_active_checkouts.or(Checkout.all.where("return_time <= now()")) else set_active_checkouts.or(Checkout.returned_for_user(current_user.id)) end + else + @checkouts = set_active_checkouts end end diff --git a/app/models/access_control_step.rb b/app/models/access_control_step.rb index 84d18d08ae..4f7117ff01 100644 --- a/app/models/access_control_step.rb +++ b/app/models/access_control_step.rb @@ -120,9 +120,10 @@ def execute context end private - def build_lending_period(d, h) - int_duration = d.to_i.days + h.to_i.hours - rescue - int_duration = Settings.controlled_digital_lending.default_lending_period - end + + def build_lending_period(d, h) + d.to_i.days + h.to_i.hours + rescue + Settings.controlled_digital_lending.default_lending_period + end end diff --git a/app/models/checkout.rb b/app/models/checkout.rb index dd7388ea53..234a41a0a4 100644 --- a/app/models/checkout.rb +++ b/app/models/checkout.rb @@ -15,14 +15,14 @@ def media_object private - def set_checkout_return_times! - self.checkout_time ||= DateTime.current - self.return_time ||= checkout_time + duration - end + def set_checkout_return_times! + self.checkout_time ||= DateTime.current + self.return_time ||= checkout_time + duration + end - def duration - duration = media_object.lending_period if media_object_id.present? - duration ||= ActiveSupport::Duration.parse(Settings.controlled_digital_lending.default_lending_period) - duration - end + def duration + duration = media_object.lending_period if media_object_id.present? + duration ||= ActiveSupport::Duration.parse(Settings.controlled_digital_lending.default_lending_period) + duration + end end diff --git a/lib/day_hour_string.rb b/lib/day_hour_string.rb index 429c1f6a53..2550144ab9 100644 --- a/lib/day_hour_string.rb +++ b/lib/day_hour_string.rb @@ -12,13 +12,12 @@ # specific language governing permissions and limitations under the License. # --- END LICENSE_HEADER BLOCK --- - module DayHourString def to_day_hour_s - d, h = (self/3600).divmod(24) + d, h = (self / 3600).divmod(24) - day_string = d > 0 ? d.to_s + ' day'.pluralize(d) : nil - hour_string = h > 0 ? h.to_s + ' hour'.pluralize(h) : nil + day_string = d.positive? ? d.to_s + ' day'.pluralize(d) : nil + hour_string = h.positive? ? h.to_s + ' hour'.pluralize(h) : nil if day_string.nil? hour_string From fe6b8e79bcef626ddc870f1ea3bebb59340af641 Mon Sep 17 00:00:00 2001 From: Chris Colvard Date: Thu, 21 Jul 2022 11:51:10 -0400 Subject: [PATCH 062/230] Add updated db/schema.rb --- db/schema.rb | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/db/schema.rb b/db/schema.rb index 1081f1f488..3aec8f700c 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2022_02_18_184645) do +ActiveRecord::Schema.define(version: 2022_05_26_185425) do create_table "active_encode_encode_records", force: :cascade do |t| t.string "global_id" @@ -115,6 +115,16 @@ t.index ["user_id"], name: "index_bookmarks_on_user_id" end + create_table "checkouts", force: :cascade do |t| + t.integer "user_id" + t.string "media_object_id" + t.datetime "checkout_time" + t.datetime "return_time" + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.index ["user_id"], name: "index_checkouts_on_user_id" + end + create_table "courses", force: :cascade do |t| t.string "context_id" t.string "title" @@ -269,4 +279,5 @@ end add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id" + add_foreign_key "checkouts", "users" end From ce7e81076f8e8d3294043ae0f42ee9aff8753cdf Mon Sep 17 00:00:00 2001 From: Chris Colvard Date: Thu, 21 Jul 2022 15:14:19 -0400 Subject: [PATCH 063/230] Update spec/lib/day_hour_string_spec.rb Co-authored-by: Mason Ballengee <68433277+masaball@users.noreply.github.com> --- spec/lib/day_hour_string_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/lib/day_hour_string_spec.rb b/spec/lib/day_hour_string_spec.rb index f6856c99df..fb2ed7c7d2 100644 --- a/spec/lib/day_hour_string_spec.rb +++ b/spec/lib/day_hour_string_spec.rb @@ -14,8 +14,8 @@ require 'rails_helper' -describe 'HumanReadableDuration' do - describe 'to_human_readable_s' do +describe 'DayHourString' do + describe 'to_day_hour_s' do context 'when lending period is measured in days' do let(:media_object) { instance_double("MediaObject", lending_period: 172800) } From 284c8b68e52e9105f2708d0ca60254974494504d Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Thu, 21 Jul 2022 16:46:58 -0400 Subject: [PATCH 064/230] Make deleted content error message clearer --- app/views/errors/deleted_pid.html.erb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/views/errors/deleted_pid.html.erb b/app/views/errors/deleted_pid.html.erb index 2684b78c2c..302ad15628 100644 --- a/app/views/errors/deleted_pid.html.erb +++ b/app/views/errors/deleted_pid.html.erb @@ -15,7 +15,8 @@ Unless required by applicable law or agreed to in writing, software distributed %>
      -

      Item(s) being deleted

      -

      The item(s) you requested are being deleted from the system. If this is not expected, contact your support staff.

      + <% item = request.original_url.include?("admin/collections") ? "collection" : "item" %> +

      <%= "#{item.titleize} Deleted" %>

      +

      <%= "The #{item} you requested has been deleted from the system. If this is not expected, contact your support staff." %>

      From 4ec91c968307caa37e2cd10b5db180ad2a8c0353 Mon Sep 17 00:00:00 2001 From: Chris Colvard Date: Thu, 21 Jul 2022 17:00:08 -0400 Subject: [PATCH 065/230] Override accessor to provide fallback; have controllers do validation --- .../admin/collections_controller.rb | 32 ++++++++++++++++--- app/models/access_control_step.rb | 18 ++++++++--- app/models/admin/collection.rb | 6 ++-- app/models/media_object.rb | 6 ++-- .../admin_collections_controller_spec.rb | 11 +++++++ .../media_objects_controller_spec.rb | 11 +++++++ spec/models/collection_spec.rb | 4 +-- spec/models/media_object_spec.rb | 5 +-- 8 files changed, 70 insertions(+), 23 deletions(-) diff --git a/app/controllers/admin/collections_controller.rb b/app/controllers/admin/collections_controller.rb index 3233d06b5e..b591a77e26 100644 --- a/app/controllers/admin/collections_controller.rb +++ b/app/controllers/admin/collections_controller.rb @@ -299,13 +299,35 @@ def update_access(collection, params) collection.default_visibility = params[:visibility] unless params[:visibility].blank? collection.default_hidden = params[:hidden] == "1" - collection.default_lending_period = build_default_lending_period.to_i + lending_period = build_default_lending_period(collection) + collection.default_lending_period = lending_period if lending_period.positive? end - def build_default_lending_period - params["add_lending_period_days"].to_i.days + params["add_lending_period_hours"].to_i.hours - rescue - Settings.controlled_digital_lending.default_lending_period + def build_default_lending_period(collection) + lending_period = 0 + + begin + if params["add_lending_period_days"].to_i.positive? + lending_period += params["add_lending_period_days"].to_i.days + else + collection.errors.add(:lending_period, "days needs to be a positive integer.") + end + rescue + collection.errors.add(:lending_period, "days needs to be a positive integer.") + end + + begin + if params["add_lending_period_hours"].to_i.positive? + lending_period += params["add_lending_period_hours"].to_i.hours + else + collection.errors.add(:lending_period, "hours needs to be a positive integer.") + end + rescue + collection.errors.add(:lending_period, "hours needs to be a positive integer.") + end + + flash[:notice] = collection.errors.full_messages.join if collection.errors.present? + lending_period.to_i end def apply_access(collection, params) diff --git a/app/models/access_control_step.rb b/app/models/access_control_step.rb index 4f7117ff01..f43a2329b7 100644 --- a/app/models/access_control_step.rb +++ b/app/models/access_control_step.rb @@ -94,13 +94,18 @@ def execute context media_object.governing_policies.delete( lease ) lease.destroy end + unless limited_access_submit media_object.visibility = context[:visibility] unless context[:visibility].blank? media_object.hidden = context[:hidden] == "1" - d = context['add_lending_period_days'] - h = context['add_lending_period_hours'] - media_object.lending_period = build_lending_period(d, h).to_i + lending_period = build_lending_period(context['add_lending_period_days'], context['add_lending_period_hours']) + if lending_period.positive? + media_object.lending_period = lending_period + else + context[:error] = "Lending period days and hours need to be positive integers." + end end + media_object.save! #Setup these values in the context because the edit partial is being rendered without running the controller's #edit (VOV-2978) @@ -122,8 +127,11 @@ def execute context private def build_lending_period(d, h) - d.to_i.days + h.to_i.hours + lending_period = 0 + lending_period += d.to_i.days if d.to_i.positive? + lending_period += h.to_i.hours if h.to_i.positive? + lending_period.to_i rescue - Settings.controlled_digital_lending.default_lending_period + 0 end end diff --git a/app/models/admin/collection.rb b/app/models/admin/collection.rb index b929591a7b..5bbb8a6a60 100644 --- a/app/models/admin/collection.rb +++ b/app/models/admin/collection.rb @@ -70,7 +70,6 @@ class Admin::Collection < ActiveFedora::Base has_subresource 'poster', class_name: 'IndexedFile' - before_save :set_default_lending_period around_save :reindex_members, if: Proc.new{ |c| c.name_changed? or c.unit_changed? } before_create :create_dropbox_directory! @@ -269,8 +268,9 @@ def default_visibility_changed? !!@default_visibility_will_change end - def set_default_lending_period - self.default_lending_period ||= ActiveSupport::Duration.parse(Settings.controlled_digital_lending.default_lending_period).to_i + alias_method :'_default_lending_period', :'default_lending_period' + def default_lending_period + self._default_lending_period || ActiveSupport::Duration.parse(Settings.controlled_digital_lending.default_lending_period).to_i end private diff --git a/app/models/media_object.rb b/app/models/media_object.rb index 19b9fa9699..4859522921 100644 --- a/app/models/media_object.rb +++ b/app/models/media_object.rb @@ -35,7 +35,6 @@ class MediaObject < ActiveFedora::Base before_save :update_dependent_properties!, prepend: true before_save :update_permalink, if: Proc.new { |mo| mo.persisted? && mo.published? }, prepend: true before_save :assign_id!, prepend: true - before_save :set_lending_period after_save :update_dependent_permalinks_job, if: Proc.new { |mo| mo.persisted? && mo.published? } after_save :remove_bookmarks @@ -377,8 +376,9 @@ def lending_status Checkout.active_for_media_object(id).any? ? "checked_out" : "available" end - def set_lending_period - self.lending_period ||= collection.default_lending_period + alias_method :'_lending_period', :'lending_period' + def lending_period + self._lending_period || collection&.default_lending_period end def current_checkout(user_id) diff --git a/spec/controllers/admin_collections_controller_spec.rb b/spec/controllers/admin_collections_controller_spec.rb index f032a5cd59..0e8cb0a1d4 100644 --- a/spec/controllers/admin_collections_controller_spec.rb +++ b/spec/controllers/admin_collections_controller_spec.rb @@ -426,6 +426,17 @@ end end + context "changing lending period" do + it "sets a custom lending period" do + expect { put 'update', params: { id: collection.id, save_access: "Save Access Settings", add_lending_period_days: 7, add_lending_period_hours: 8 } }.to change { collection.reload.default_lending_period }.to(633600) + end + + it "returns error if invalid" do + expect { put 'update', params: { id: collection.id, save_access: "Save Access Settings", add_lending_period_days: -1, add_lending_period_hours: -1 } }.not_to change { collection.reload.default_lending_period } + expect(response).to redirect_to(admin_collection_path(collection)) + expect(flash[:notice]).to be_present + end + end end describe "#apply_access" do diff --git a/spec/controllers/media_objects_controller_spec.rb b/spec/controllers/media_objects_controller_spec.rb index 67f8022764..6068db774c 100644 --- a/spec/controllers/media_objects_controller_spec.rb +++ b/spec/controllers/media_objects_controller_spec.rb @@ -1332,6 +1332,17 @@ }.not_to change{media_object.leases.count} end end + + context "lending period" do + it "sets a custom lending period" do + expect { put :update, params: { id: media_object.id, step: 'access-control', donot_advance: 'true', add_lending_period_days: 7, add_lending_period_hours: 8 } }.to change { media_object.reload.lending_period }.to(633600) + end + + it "returns error if invalid" do + expect { put :update, params: { id: media_object.id, step: 'access-control', donot_advance: 'true', add_lending_period_days: -1, add_lending_period_hours: -1 } }.not_to change { media_object.reload.lending_period } + expect(flash[:error]).to be_present + end + end end context 'resource description' do diff --git a/spec/models/collection_spec.rb b/spec/models/collection_spec.rb index f6bc53e373..79e4e0e93b 100644 --- a/spec/models/collection_spec.rb +++ b/spec/models/collection_spec.rb @@ -602,17 +602,15 @@ end end - describe 'set_default_lending_period' do + describe 'default_lending_period' do context 'a custom lending period has not been set' do it 'sets the lending period equal to the system default' do - collection.set_default_lending_period expect(collection.default_lending_period).to eq ActiveSupport::Duration.parse(Settings.controlled_digital_lending.default_lending_period).to_i end end context 'a custom lending period has been set' do let(:collection) { FactoryBot.create(:collection, default_lending_period: 86400) } it 'leaves the lending period equal to the custom value' do - collection.set_default_lending_period expect(collection.default_lending_period).to eq 86400 end end diff --git a/spec/models/media_object_spec.rb b/spec/models/media_object_spec.rb index 333c3c5a91..0dbc52cb0b 100644 --- a/spec/models/media_object_spec.rb +++ b/spec/models/media_object_spec.rb @@ -1025,10 +1025,9 @@ end end - describe 'set_lending_period' do + describe 'lending_period' do context 'there is not a custom lending period' do it 'sets the lending period to the system default' do - media_object.set_lending_period expect(media_object.lending_period).to eq ActiveSupport::Duration.parse(Settings.controlled_digital_lending.default_lending_period).to_i end end @@ -1036,13 +1035,11 @@ let(:collection) { FactoryBot.create(:collection, default_lending_period: 86400) } let(:media_object) { FactoryBot.create(:media_object, collection_id: collection.id) } it "sets the lending period to equal the collection's default lending period" do - media_object.set_lending_period expect(media_object.lending_period).to eq collection.default_lending_period end context 'the media object has a custom lending period' do let(:media_object) { FactoryBot.create(:media_object, collection_id: collection.id, lending_period: 172800)} it "leaves the lending period equal to the custom value" do - media_object.set_lending_period expect(media_object.lending_period).to eq 172800 end end From d96413f736e56edbeb7bf9f06420e6b5710ea4bd Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Wed, 27 Jul 2022 10:12:47 -0400 Subject: [PATCH 066/230] Add instance name to comments mailer --- app/controllers/comments_controller.rb | 7 +- app/models/comment.rb | 6 +- .../comments_mailer/contact_email.html.erb | 9 +- .../comments_mailer/contact_email.text.erb | 5 +- config/environments/development.rb | 2 + .../previews/comments_mailer_preview.rb | 13 +++ spec/requests/comments_spec.rb | 93 +++++++++++++++++++ 7 files changed, 125 insertions(+), 10 deletions(-) create mode 100644 spec/mailers/previews/comments_mailer_preview.rb create mode 100644 spec/requests/comments_spec.rb diff --git a/app/controllers/comments_controller.rb b/app/controllers/comments_controller.rb index 3b8b2fee76..06d0f75314 100644 --- a/app/controllers/comments_controller.rb +++ b/app/controllers/comments_controller.rb @@ -1,11 +1,11 @@ # Copyright 2011-2022, The Trustees of Indiana University and Northwestern # University. Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. -# +# # You may obtain a copy of the License at -# +# # http://www.apache.org/licenses/LICENSE-2.0 -# +# # Unless required by applicable law or agreed to in writing, software distributed # under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR # CONDITIONS OF ANY KIND, either express or implied. See the License for the @@ -30,6 +30,7 @@ def create @comment.comment = params[:comment][:comment] if @comment.valid? && (Settings.recaptcha.blank? || verify_recaptcha(model: @comment)) + @comment.subject = @comment.subject.prepend("#{Settings.name}: ") begin CommentsMailer.contact_email(@comment.to_h).deliver_later rescue Errno::ECONNRESET => e diff --git a/app/models/comment.rb b/app/models/comment.rb index 5c5268c147..2d0a731828 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -1,11 +1,11 @@ # Copyright 2011-2022, The Trustees of Indiana University and Northwestern # University. Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. -# +# # You may obtain a copy of the License at -# +# # http://www.apache.org/licenses/LICENSE-2.0 -# +# # Unless required by applicable law or agreed to in writing, software distributed # under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR # CONDITIONS OF ANY KIND, either express or implied. See the License for the diff --git a/app/views/comments_mailer/contact_email.html.erb b/app/views/comments_mailer/contact_email.html.erb index c4f30251d8..0faf0dee6b 100644 --- a/app/views/comments_mailer/contact_email.html.erb +++ b/app/views/comments_mailer/contact_email.html.erb @@ -18,14 +18,17 @@ Unless required by applicable law or agreed to in writing, software distributed
      From
      -
      <%= @comment.name %> (<<%= @comment.email %>>)
      +
      <%= @comment.name %> (<<%= @comment.email %>>)
      + +
      Site
      +
      <%= Settings.name %>
      Subject
      -
      <%= @comment.subject %>
      +
      <%= @comment.subject.gsub(/#{Settings.name}:\s/, '') %>
      Comment
      <%= @comment.comment %>
      - +
      Received
      <%= DateTime.now %>
      diff --git a/app/views/comments_mailer/contact_email.text.erb b/app/views/comments_mailer/contact_email.text.erb index 8be8a25a7a..9bf4306c9f 100644 --- a/app/views/comments_mailer/contact_email.text.erb +++ b/app/views/comments_mailer/contact_email.text.erb @@ -20,8 +20,11 @@ A @comment has been received from the system FROM <%= @comment.name %> (<<%= @comment.email %>>) +SITE +<%= Settings.name %> + SUBJECT -<%= @comment.subject %> +<%= @comment.subject.gsub(/#{Settings.name}:\s/, '') %> COMMENT <%= @comment.comment %> diff --git a/config/environments/development.rb b/config/environments/development.rb index 66df51f6fc..a82e51cd7d 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -36,6 +36,8 @@ config.action_mailer.perform_caching = false + config.action_mailer.show_previews = true + # Print deprecation notices to the Rails logger. config.active_support.deprecation = :log diff --git a/spec/mailers/previews/comments_mailer_preview.rb b/spec/mailers/previews/comments_mailer_preview.rb new file mode 100644 index 0000000000..34883c7bdc --- /dev/null +++ b/spec/mailers/previews/comments_mailer_preview.rb @@ -0,0 +1,13 @@ +class CommentsMailerPreview < ActionMailer::Preview + def contact_email + @comment = Comment.new + @comment.name = 'Eddie Munson' + @comment.nickname = '' + @comment.email = 'emunson@archive.edu' + @comment.email_confirmation = 'emunson@archive.edu' + @comment.subject = "#{Settings.name}: General feedback" + @comment.comment = 'Testing, testing, testing' + + CommentsMailer.contact_email(@comment.to_h) + end +end diff --git a/spec/requests/comments_spec.rb b/spec/requests/comments_spec.rb new file mode 100644 index 0000000000..5e74f56fd3 --- /dev/null +++ b/spec/requests/comments_spec.rb @@ -0,0 +1,93 @@ +# Copyright 2011-2022, The Trustees of Indiana University and Northwestern +# University. Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed +# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +# CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. +# --- END LICENSE_HEADER BLOCK --- + +require 'rails_helper' + +describe CommentsController do + + describe "GET /index" do + before { get comments_url } + it 'renders a successful response' do + expect(response).to be_successful + end + it 'renders the index partial' do + expect(response).to render_template(:index) + end + end + + describe "POST /create" do + let(:email) { Faker::Internet.email } + let(:attributes) { + { comment: + { name: Faker::Name.name, + subject: 'General feedback', + email: email, + email_confirmation: email, + nickname: '', + comment: Faker::Lorem.sentence + } + } + } + context 'all fields have been properly filled in' do + it 'renders a successful response' do + post comments_url, params: attributes + expect(response).to be_successful + end + it 'queues the comment email for delivery' do + post comments_url, params: attributes + assert_enqueued_jobs(1) + end + it 'produces an email with proper contents' do + expect { post comments_url, params: attributes }.to( + have_enqueued_mail(CommentsMailer, :contact_email).with( + name: attributes[:comment][:name], subject: Settings.name + ': ' + attributes[:comment][:subject], + email: email, nickname: '', comment: attributes[:comment][:comment] + ) + ) + end + end + context 'not all fields have been filled in' do + before { attributes[:comment][:comment] = nil } + it 'does not queue an email for delivery' do + post comments_url, params: attributes + assert_enqueued_jobs(0) + end + it 'returns a flash message informing the user that information is missing' do + post comments_url, params: attributes + expect(flash[:error]).to eq('Your comment was missing required information. Please complete all fields and resubmit.') + end + it 'renders the index partial' do + post comments_url, params: attributes + expect(response).to render_template(:index) + end + end + context 'the connection is interrupted' do + it 'returns a flash message asking the user to report the problem' do + allow_any_instance_of(ActionMailer::MessageDelivery).to receive(:deliver_later).and_raise(Errno::ECONNRESET) + post comments_url, params:attributes + expect(flash[:notice]).to eq("The message could not be sent in a timely fashion. Contact us at #{Settings.email.support} to report the problem.") + end + it 'does not enqueue an email for delivery' do + allow_any_instance_of(ActionMailer::MessageDelivery).to receive(:deliver_later).and_raise(Errno::ECONNRESET) + post comments_url, params:attributes + assert_enqueued_jobs(0) + end + it 'renders the index partial' do + allow_any_instance_of(ActionMailer::MessageDelivery).to receive(:deliver_later).and_raise(Errno::ECONNRESET) + post comments_url, params:attributes + expect(response).to render_template(:index) + end + end + end +end From 79f332066eec29af746a123fe3fcba2cf2148b1f Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Wed, 27 Jul 2022 14:03:17 -0400 Subject: [PATCH 067/230] Refactor subject handling and add tests --- app/controllers/comments_controller.rb | 1 - app/mailers/comments_mailer.rb | 8 +-- .../comments_mailer/contact_email.html.erb | 2 +- .../comments_mailer/contact_email.text.erb | 2 +- spec/mailers/comments_mailer_spec.rb | 64 +++++++++++++++++++ .../previews/comments_mailer_preview.rb | 2 +- spec/requests/comments_spec.rb | 2 +- 7 files changed, 72 insertions(+), 9 deletions(-) create mode 100644 spec/mailers/comments_mailer_spec.rb diff --git a/app/controllers/comments_controller.rb b/app/controllers/comments_controller.rb index 06d0f75314..7bd7b380e1 100644 --- a/app/controllers/comments_controller.rb +++ b/app/controllers/comments_controller.rb @@ -30,7 +30,6 @@ def create @comment.comment = params[:comment][:comment] if @comment.valid? && (Settings.recaptcha.blank? || verify_recaptcha(model: @comment)) - @comment.subject = @comment.subject.prepend("#{Settings.name}: ") begin CommentsMailer.contact_email(@comment.to_h).deliver_later rescue Errno::ECONNRESET => e diff --git a/app/mailers/comments_mailer.rb b/app/mailers/comments_mailer.rb index 1087897b06..f6fe545923 100644 --- a/app/mailers/comments_mailer.rb +++ b/app/mailers/comments_mailer.rb @@ -1,11 +1,11 @@ # Copyright 2011-2022, The Trustees of Indiana University and Northwestern # University. Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. -# +# # You may obtain a copy of the License at -# +# # http://www.apache.org/licenses/LICENSE-2.0 -# +# # Unless required by applicable law or agreed to in writing, software distributed # under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR # CONDITIONS OF ANY KIND, either express or implied. See the License for the @@ -17,6 +17,6 @@ class CommentsMailer < ActionMailer::Base def contact_email(comment) @comment = OpenStruct.new(comment) - mail(from: Settings.email.comments, reply_to: @comment.email, subject: @comment.subject) + mail(from: Settings.email.comments, reply_to: @comment.email, subject: "#{Settings.name}: #{@comment.subject}") end end diff --git a/app/views/comments_mailer/contact_email.html.erb b/app/views/comments_mailer/contact_email.html.erb index 0faf0dee6b..1d688b8833 100644 --- a/app/views/comments_mailer/contact_email.html.erb +++ b/app/views/comments_mailer/contact_email.html.erb @@ -24,7 +24,7 @@ Unless required by applicable law or agreed to in writing, software distributed
      <%= Settings.name %>
      Subject
      -
      <%= @comment.subject.gsub(/#{Settings.name}:\s/, '') %>
      +
      <%= @comment.subject %>
      Comment
      <%= @comment.comment %>
      diff --git a/app/views/comments_mailer/contact_email.text.erb b/app/views/comments_mailer/contact_email.text.erb index 9bf4306c9f..b87e63f0ce 100644 --- a/app/views/comments_mailer/contact_email.text.erb +++ b/app/views/comments_mailer/contact_email.text.erb @@ -24,7 +24,7 @@ SITE <%= Settings.name %> SUBJECT -<%= @comment.subject.gsub(/#{Settings.name}:\s/, '') %> +<%= @comment.subject %> COMMENT <%= @comment.comment %> diff --git a/spec/mailers/comments_mailer_spec.rb b/spec/mailers/comments_mailer_spec.rb new file mode 100644 index 0000000000..3540364ffa --- /dev/null +++ b/spec/mailers/comments_mailer_spec.rb @@ -0,0 +1,64 @@ +# Copyright 2011-2022, The Trustees of Indiana University and Northwestern +# University. Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed +# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +# CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. +# --- END LICENSE_HEADER BLOCK --- + +require 'rails_helper' + +describe 'CommentsMailer' do + + include EmailSpec::Helpers + include EmailSpec::Matchers + + describe 'contact_email' do + let(:email) { Faker::Internet.email } + let(:comment) { + { + name: Faker::Name.name, + subject: 'General feedback', + email: email, + email_confirmation: email, + nickname: '', + comment: Faker::Lorem.sentence + } + } + + let(:mail) { CommentsMailer.contact_email(comment.to_h) } + + it 'has correct e-mail address' do + expect(mail).to deliver_to(Settings.email.comments) + end + + it 'has correct subject' do + expect(mail).to have_subject("#{Settings.name}: #{comment[:subject]}") + end + + context 'body' do + it 'has commenter\'s information' do + expect(mail).to have_body_text(comment[:name]) + expect(mail).to have_body_text(comment[:email]) + end + it 'has instance information' do + expect(mail).to have_body_text(Settings.name) + end + it 'has the subject' do + expect(mail).to have_body_text(comment[:subject]) + end + it 'has the comment' do + expect(mail).to have_body_text(comment[:comment]) + end + it 'has the date received' do + expect(mail).to have_body_text(DateTime.now.to_s) + end + end + end +end diff --git a/spec/mailers/previews/comments_mailer_preview.rb b/spec/mailers/previews/comments_mailer_preview.rb index 34883c7bdc..6295d4f42d 100644 --- a/spec/mailers/previews/comments_mailer_preview.rb +++ b/spec/mailers/previews/comments_mailer_preview.rb @@ -5,7 +5,7 @@ def contact_email @comment.nickname = '' @comment.email = 'emunson@archive.edu' @comment.email_confirmation = 'emunson@archive.edu' - @comment.subject = "#{Settings.name}: General feedback" + @comment.subject = "General feedback" @comment.comment = 'Testing, testing, testing' CommentsMailer.contact_email(@comment.to_h) diff --git a/spec/requests/comments_spec.rb b/spec/requests/comments_spec.rb index 5e74f56fd3..ab3f1299e9 100644 --- a/spec/requests/comments_spec.rb +++ b/spec/requests/comments_spec.rb @@ -51,7 +51,7 @@ it 'produces an email with proper contents' do expect { post comments_url, params: attributes }.to( have_enqueued_mail(CommentsMailer, :contact_email).with( - name: attributes[:comment][:name], subject: Settings.name + ': ' + attributes[:comment][:subject], + name: attributes[:comment][:name], subject: attributes[:comment][:subject], email: email, nickname: '', comment: attributes[:comment][:comment] ) ) From 83dca7f503bbc26a7bec53279fd7105069b36c56 Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Thu, 28 Jul 2022 15:08:32 -0400 Subject: [PATCH 068/230] Remove useless item/collection check --- app/views/errors/deleted_pid.html.erb | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/views/errors/deleted_pid.html.erb b/app/views/errors/deleted_pid.html.erb index 302ad15628..7b72e6e428 100644 --- a/app/views/errors/deleted_pid.html.erb +++ b/app/views/errors/deleted_pid.html.erb @@ -15,8 +15,7 @@ Unless required by applicable law or agreed to in writing, software distributed %>
      - <% item = request.original_url.include?("admin/collections") ? "collection" : "item" %> -

      <%= "#{item.titleize} Deleted" %>

      -

      <%= "The #{item} you requested has been deleted from the system. If this is not expected, contact your support staff." %>

      +

      <%= "Item Deleted" %>

      +

      <%= "The item you requested has been deleted from the system. If this is not expected, contact your support staff." %>

      From 71333717131568d9671144cb491c366563ef481d Mon Sep 17 00:00:00 2001 From: Mason Ballengee <68433277+masaball@users.noreply.github.com> Date: Thu, 28 Jul 2022 15:17:23 -0400 Subject: [PATCH 069/230] Update deleted_pid.html.erb When removing the item/collection test, the <%= %> tags became unneeded since the error message is just strings. --- app/views/errors/deleted_pid.html.erb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/errors/deleted_pid.html.erb b/app/views/errors/deleted_pid.html.erb index 7b72e6e428..f0bf32c733 100644 --- a/app/views/errors/deleted_pid.html.erb +++ b/app/views/errors/deleted_pid.html.erb @@ -15,7 +15,7 @@ Unless required by applicable law or agreed to in writing, software distributed %>
      -

      <%= "Item Deleted" %>

      -

      <%= "The item you requested has been deleted from the system. If this is not expected, contact your support staff." %>

      +

      Item Deleted

      +

      The item you requested has been deleted from the system. If this is not expected, contact your support staff.

      From 6276c0c75337491e70d2da1cf621ebad89087801 Mon Sep 17 00:00:00 2001 From: dananji Date: Fri, 29 Jul 2022 14:37:32 -0400 Subject: [PATCH 070/230] Render mediaobject without masterfile properly when CDL is enabled --- app/views/media_objects/_embed_checkout.html.erb | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/app/views/media_objects/_embed_checkout.html.erb b/app/views/media_objects/_embed_checkout.html.erb index d17393a7d8..599e7b2c64 100644 --- a/app/views/media_objects/_embed_checkout.html.erb +++ b/app/views/media_objects/_embed_checkout.html.erb @@ -13,7 +13,8 @@ Unless required by applicable law or agreed to in writing, software distributed specific language governing permissions and limitations under the License. --- END LICENSE_HEADER BLOCK --- %> - <% master_file=@media_object.master_files.first if @media_object.master_files.size > 0%> +<% if !@masterFiles.blank? %> + <% master_file=@media_object.master_files.first %>
      <% if @media_object.lending_status == "available" %> <%= t('media_object.cdl.checkout_message').html_safe %> @@ -21,4 +22,9 @@ Unless required by applicable law or agreed to in writing, software distributed <% else %> <%= t('media_object.cdl.not_available_message').html_safe %> <% end %> -
      \ No newline at end of file + +<% else %> +
      +

      No media is associated with this item

      +
      +<% end %> From da69a808dca0989b090461a1ca44d18459baf898 Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Mon, 1 Aug 2022 13:59:41 -0400 Subject: [PATCH 071/230] Update controlled vocab API to accept key/value pairs --- app/controllers/vocabulary_controller.rb | 24 +++++++++++--- .../controllers/vocabulary_controller_spec.rb | 32 +++++++++++++++---- 2 files changed, 46 insertions(+), 10 deletions(-) diff --git a/app/controllers/vocabulary_controller.rb b/app/controllers/vocabulary_controller.rb index e74f22faff..6a02526044 100644 --- a/app/controllers/vocabulary_controller.rb +++ b/app/controllers/vocabulary_controller.rb @@ -1,11 +1,11 @@ # Copyright 2011-2022, The Trustees of Indiana University and Northwestern # University. Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. -# +# # You may obtain a copy of the License at -# +# # http://www.apache.org/licenses/LICENSE-2.0 -# +# # Unless required by applicable law or agreed to in writing, software distributed # under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR # CONDITIONS OF ANY KIND, either express or implied. See the License for the @@ -33,7 +33,20 @@ def update end v = Avalon::ControlledVocabulary.vocabulary - v[params[:id].to_sym] |= Array(params[:entry]) + if vocabulary_params[:entry].is_a?(ActionController::Parameters) + begin + new_entry = vocabulary_params[:entry].to_hash + v[params[:id].to_sym].merge!(new_entry) + rescue NoMethodError + render json: {errors: ["#{params[:id]} entries must not be a key/value pair."]}, status: 422 and return + end + else + begin + v[params[:id].to_sym] |= Array(params[:entry]) + rescue NoMethodError + render json: {errors: ["#{params[:id]} entries must be a key/value pair."]}, status: 422 and return + end + end result = Avalon::ControlledVocabulary.vocabulary = v if result head :ok, content_type: 'application/json' @@ -50,4 +63,7 @@ def verify_vocabulary_exists end end + def vocabulary_params + params.permit(:entry => {}) + end end diff --git a/spec/controllers/vocabulary_controller_spec.rb b/spec/controllers/vocabulary_controller_spec.rb index f7a14afbd4..168b11f7d0 100644 --- a/spec/controllers/vocabulary_controller_spec.rb +++ b/spec/controllers/vocabulary_controller_spec.rb @@ -1,11 +1,11 @@ # Copyright 2011-2022, The Trustees of Indiana University and Northwestern # University. Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. -# +# # You may obtain a copy of the License at -# +# # http://www.apache.org/licenses/LICENSE-2.0 -# +# # Unless required by applicable law or agreed to in writing, software distributed # under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR # CONDITIONS OF ANY KIND, either express or implied. See the License for the @@ -87,9 +87,17 @@ end end describe "#update" do - it "should add unit to controlled vocabulary" do - put 'update', params: { format:'json', id: :units, entry: 'New Unit' } - expect(Avalon::ControlledVocabulary.vocabulary[:units]).to include("New Unit") + context "new unit is a string" do + it "should add unit to controlled vocabulary" do + put 'update', params: { format:'json', id: :units, entry: 'New Unit' } + expect(Avalon::ControlledVocabulary.vocabulary[:units]).to include("New Unit") + end + end + context "new unit is a key value pair" do + it "should add unit to controlled vocabulary" do + put 'update', params: { format:'json', id: :identifier_types, entry: { key: 'value' } } + expect(Avalon::ControlledVocabulary.vocabulary[:identifier_types]).to include({ 'key' => 'value' }) + end end it "should return 404 if requested vocabulary not present" do put 'update', params: { format:'json', id: :doesnt_exist, entry: 'test' } @@ -110,6 +118,18 @@ expect(JSON.parse(response.body)["errors"].class).to eq Array expect(JSON.parse(response.body)["errors"].first.class).to eq String end + it "should return 422 if update is a key/value when the vocab uses single values" do + put 'update', params: { format:'json', id: :units, entry: { key: 'value' } } + expect(response.status).to eq(422) + expect(JSON.parse(response.body)["errors"].class).to eq Array + expect(JSON.parse(response.body)["errors"].first.class).to eq String + end + it "should return 422 if update is a value when the vocab uses key/value pairs" do + put 'update', params: { format:'json', id: :identifier_types, entry: "New Unit" } + expect(response.status).to eq(422) + expect(JSON.parse(response.body)["errors"].class).to eq Array + expect(JSON.parse(response.body)["errors"].first.class).to eq String + end end end From 308bf80215c6e2769a52a49d5925be3cafa08bfd Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Mon, 1 Aug 2022 14:47:39 -0400 Subject: [PATCH 072/230] Refactor for codeclimate --- app/controllers/vocabulary_controller.rb | 32 ++++++++++++------------ 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/app/controllers/vocabulary_controller.rb b/app/controllers/vocabulary_controller.rb index 6a02526044..e6d75a6874 100644 --- a/app/controllers/vocabulary_controller.rb +++ b/app/controllers/vocabulary_controller.rb @@ -32,22 +32,13 @@ def update render json: {errors: ["No update value sent"]}, status: 422 and return end - v = Avalon::ControlledVocabulary.vocabulary - if vocabulary_params[:entry].is_a?(ActionController::Parameters) - begin - new_entry = vocabulary_params[:entry].to_hash - v[params[:id].to_sym].merge!(new_entry) - rescue NoMethodError - render json: {errors: ["#{params[:id]} entries must not be a key/value pair."]}, status: 422 and return - end - else - begin - v[params[:id].to_sym] |= Array(params[:entry]) - rescue NoMethodError - render json: {errors: ["#{params[:id]} entries must be a key/value pair."]}, status: 422 and return - end + @v = Avalon::ControlledVocabulary.vocabulary + begin + build_update(@v) + rescue NoMethodError + render json: {errors: ["Update failed. Ensure that the new entry is in the proper form for the intended vocabulary."]}, status: 422 and return end - result = Avalon::ControlledVocabulary.vocabulary = v + result = Avalon::ControlledVocabulary.vocabulary = @v if result head :ok, content_type: 'application/json' else @@ -63,7 +54,16 @@ def verify_vocabulary_exists end end + def build_update(vocabulary) + if vocabulary_params[:entry].is_a?(ActionController::Parameters) + new_entry = vocabulary_params[:entry].to_hash + @v[params[:id].to_sym].merge!(new_entry) + else + @v[params[:id].to_sym] |= Array(params[:entry]) + end + end + def vocabulary_params - params.permit(:entry => {}) + params.permit(entry: {}) end end From 8e451263781267a36aba84ddf5f414c70b2b0320 Mon Sep 17 00:00:00 2001 From: Mason Ballengee <68433277+masaball@users.noreply.github.com> Date: Mon, 1 Aug 2022 15:15:50 -0400 Subject: [PATCH 073/230] Update spec/controllers/vocabulary_controller_spec.rb Co-authored-by: Chris Colvard --- spec/controllers/vocabulary_controller_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/controllers/vocabulary_controller_spec.rb b/spec/controllers/vocabulary_controller_spec.rb index 168b11f7d0..df0c6f8ad8 100644 --- a/spec/controllers/vocabulary_controller_spec.rb +++ b/spec/controllers/vocabulary_controller_spec.rb @@ -93,8 +93,8 @@ expect(Avalon::ControlledVocabulary.vocabulary[:units]).to include("New Unit") end end - context "new unit is a key value pair" do - it "should add unit to controlled vocabulary" do + context "new entry is a key value pair" do + it "should add to controlled vocabulary" do put 'update', params: { format:'json', id: :identifier_types, entry: { key: 'value' } } expect(Avalon::ControlledVocabulary.vocabulary[:identifier_types]).to include({ 'key' => 'value' }) end From 8ac1c7891b6c0c5838b6c4d38a8c2bdc3587a99c Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Tue, 2 Aug 2022 10:43:46 -0400 Subject: [PATCH 074/230] Allow managers to GET collection/items endpoint --- app/models/ability.rb | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/models/ability.rb b/app/models/ability.rb index 871c0c1a17..3a10b6d57f 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -1,11 +1,11 @@ # Copyright 2011-2022, The Trustees of Indiana University and Northwestern # University. Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. -# +# # You may obtain a copy of the License at -# +# # http://www.apache.org/licenses/LICENSE-2.0 -# +# # Unless required by applicable law or agreed to in writing, software distributed # under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR # CONDITIONS OF ANY KIND, either express or implied. See the License for the @@ -57,6 +57,7 @@ def create_permissions(user=nil, session=nil) if @user_groups.include? "manager" can :create, Admin::Collection + can :items, Admin::Collection end end end From c913f8d7804791565149dcdf529512a3808231c5 Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Tue, 2 Aug 2022 14:37:21 -0400 Subject: [PATCH 075/230] Limit items endpoint to users who have read access --- app/models/ability.rb | 3 +- .../admin_collections_controller_spec.rb | 34 +++++++++++++++++-- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/app/models/ability.rb b/app/models/ability.rb index 3a10b6d57f..c0711e6dc4 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -57,7 +57,6 @@ def create_permissions(user=nil, session=nil) if @user_groups.include? "manager" can :create, Admin::Collection - can :items, Admin::Collection end end end @@ -80,7 +79,7 @@ def custom_permissions(user=nil, session=nil) cannot :read, Admin::Collection unless (full_login? || is_api_request?) if full_login? || is_api_request? - can :read, Admin::Collection do |collection| + can [:read, :items], Admin::Collection do |collection| is_member_of?(collection) end diff --git a/spec/controllers/admin_collections_controller_spec.rb b/spec/controllers/admin_collections_controller_spec.rb index 0e8cb0a1d4..409766b46e 100644 --- a/spec/controllers/admin_collections_controller_spec.rb +++ b/spec/controllers/admin_collections_controller_spec.rb @@ -1,11 +1,11 @@ # Copyright 2011-2022, The Trustees of Indiana University and Northwestern # University. Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. -# +# # You may obtain a copy of the License at -# +# # http://www.apache.org/licenses/LICENSE-2.0 -# +# # Unless required by applicable law or agreed to in writing, software distributed # under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR # CONDITIONS OF ANY KIND, either express or implied. See the License for the @@ -264,6 +264,34 @@ #TODO add check that mediaobject is serialized to json properly end + context 'user is a collection manager' do + let(:manager) { FactoryBot.create(:manager) } + before(:each) do + ApiToken.create token: 'manager_token', username: manager.username, email: manager.email + request.headers['Avalon-Api-Key'] = 'manager_token' + end + context 'user does not manage this collection' do + it "should return a 401 response code" do + get 'items', params: { id: collection.id, format: 'json' } + expect(response.status).to eq(401) + end + it 'should not return json for specific collection\'s media_objects' do + get 'items', params: { id: collection.id, format: 'json' } + expect(response.body).to be_empty + end + end + context 'user manages this collection' do + let!(:collection) { FactoryBot.create(:collection, items: 2, managers: [manager.username]) } + it "should not return a 401 response code" do + get 'items', params: { id: collection.id, format: 'json' } + expect(response.status).not_to eq(401) + end + it "should return json for specific collection's media objects" do + get 'items', params: { id: collection.id, format: 'json' } + expect(JSON.parse(response.body)).to include(collection.media_objects[0].id,collection.media_objects[1].id) + end + end + end end describe "#create" do From 3e08f37a657bddc0c76ed16277bccb8822643270 Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Wed, 3 Aug 2022 14:11:21 -0400 Subject: [PATCH 076/230] Update checkouts page --- app/controllers/checkouts_controller.rb | 8 ++++---- app/models/checkout.rb | 4 ++++ app/views/checkouts/index.html.erb | 6 +++--- spec/models/checkout_spec.rb | 14 ++++++++++++++ 4 files changed, 25 insertions(+), 7 deletions(-) diff --git a/app/controllers/checkouts_controller.rb b/app/controllers/checkouts_controller.rb index 05f6026ec6..d3763415e5 100644 --- a/app/controllers/checkouts_controller.rb +++ b/app/controllers/checkouts_controller.rb @@ -117,8 +117,8 @@ def admin_array(checkout) def user_array(checkout) [ view_context.link_to(checkout.media_object.title, main_app.media_object_url(checkout.media_object)), - checkout.checkout_time.to_s(:long_ordinal), - checkout.return_time.to_s(:long_ordinal), + Checkout.date_parser(checkout.checkout_time), + Checkout.date_parser(checkout.return_time), time_remaining(checkout), checkout_actions(checkout) ] @@ -134,9 +134,9 @@ def time_remaining(checkout) def checkout_actions(checkout) if checkout.return_time > DateTime.current - view_context.link_to('Return', return_checkout_url(checkout), class: 'btn btn-danger btn-xs', method: :patch, data: { confirm: "Are you sure you want to return this item?" }) + view_context.link_to('Return', return_checkout_url(checkout), class: 'btn btn-outline btn-xs', method: :patch) elsif checkout.return_time < DateTime.current && checkout.media_object.lending_status == 'available' - view_context.link_to('Checkout', checkouts_url(params: { checkout: { media_object_id: checkout.media_object_id }, format: :json }), class: 'btn btn-primary btn-xs', method: :post) + view_context.link_to('Checkout', checkouts_url(params: { checkout: { media_object_id: checkout.media_object_id } }), class: 'btn btn-primary btn-xs', method: :post) else 'Item Unavailable' end diff --git a/app/models/checkout.rb b/app/models/checkout.rb index 234a41a0a4..7792f64d69 100644 --- a/app/models/checkout.rb +++ b/app/models/checkout.rb @@ -13,6 +13,10 @@ def media_object MediaObject.find(media_object_id) end + def self.date_parser(date_time) + date_time.strftime("%B #{date_time.strftime("%-d").to_i.ordinalize}, %Y %l:%M %p") + end + private def set_checkout_return_times! diff --git a/app/views/checkouts/index.html.erb b/app/views/checkouts/index.html.erb index 406be8812b..8b73264db0 100644 --- a/app/views/checkouts/index.html.erb +++ b/app/views/checkouts/index.html.erb @@ -32,12 +32,12 @@
      <%= checkout.user.user_key %><%= link_to checkout.media_object.title, main_app.media_object_url(checkout.media_object) %><%= checkout.checkout_time.to_s(:long_ordinal) %><%= checkout.return_time.to_s(:long_ordinal) %><%= Checkout.date_parser(checkout.checkout_time) %><%= Checkout.date_parser(checkout.return_time) %> <%= distance_of_time_in_words(checkout.return_time - DateTime.current) %> <% if checkout.return_time > DateTime.current %> - <%= link_to 'Return', return_checkout_url(checkout), class: 'btn btn-danger btn-xs', method: :patch, data: { confirm: "Are you sure you want to return this item?" } %> + <%= link_to 'Return', return_checkout_url(checkout), class: 'btn btn-outline btn-xs', method: :patch %> <% elsif checkout.return_time < DateTime.current && checkout.media_object.lending_status == 'available' %> <%= link_to 'Checkout', checkouts_url(params: { checkout: { media_object_id: checkout.media_object_id } }), class: 'btn btn-primary btn-xs', method: :post %> <% else %> diff --git a/spec/models/checkout_spec.rb b/spec/models/checkout_spec.rb index 28c04c500a..a6c281f348 100644 --- a/spec/models/checkout_spec.rb +++ b/spec/models/checkout_spec.rb @@ -2,6 +2,8 @@ require 'cancan/matchers' RSpec.describe Checkout, type: :model do + include ActiveSupport::Testing::TimeHelpers + let(:checkout) { FactoryBot.create(:checkout) } describe 'validations' do @@ -88,4 +90,16 @@ expect(checkout.media_object).to eq media_object end end + + describe 'date_parser' do + before { Time.stub(:now) { Time.new(2000,01,13,12,00) } } + let(:checkout) { FactoryBot.create(:checkout, checkout_time: Time.now, return_time: Time.now + 1.day) } + it 'parses DateTime values into strings'do + expect(Checkout.date_parser(checkout.checkout_time)).to be_a(String) + expect(Checkout.date_parser(checkout.return_time)).to be_a(String) + end + it 'parses the dates to be human readable' do + expect(Checkout.date_parser(checkout.checkout_time)).to eq("January 13th, 2000 12:00 PM") + end + end end From a8fb335fdad4d00b3d5222e1b1b66f2ac06b28a8 Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Wed, 3 Aug 2022 16:18:23 -0400 Subject: [PATCH 077/230] Refactor update of checkouts page --- app/controllers/checkouts_controller.rb | 4 ++-- app/models/checkout.rb | 4 ---- app/views/checkouts/index.html.erb | 4 ++-- config/initializers/time_formats.rb | 1 + spec/models/checkout_spec.rb | 14 -------------- spec/views/checkouts/index.html.erb_spec.rb | 9 ++++++++- 6 files changed, 13 insertions(+), 23 deletions(-) create mode 100644 config/initializers/time_formats.rb diff --git a/app/controllers/checkouts_controller.rb b/app/controllers/checkouts_controller.rb index d3763415e5..58af00212f 100644 --- a/app/controllers/checkouts_controller.rb +++ b/app/controllers/checkouts_controller.rb @@ -117,8 +117,8 @@ def admin_array(checkout) def user_array(checkout) [ view_context.link_to(checkout.media_object.title, main_app.media_object_url(checkout.media_object)), - Checkout.date_parser(checkout.checkout_time), - Checkout.date_parser(checkout.return_time), + checkout.checkout_time.to_s(:long_ordinal_12h), + checkout.return_time.to_s(:long_ordinal_12h), time_remaining(checkout), checkout_actions(checkout) ] diff --git a/app/models/checkout.rb b/app/models/checkout.rb index 7792f64d69..234a41a0a4 100644 --- a/app/models/checkout.rb +++ b/app/models/checkout.rb @@ -13,10 +13,6 @@ def media_object MediaObject.find(media_object_id) end - def self.date_parser(date_time) - date_time.strftime("%B #{date_time.strftime("%-d").to_i.ordinalize}, %Y %l:%M %p") - end - private def set_checkout_return_times! diff --git a/app/views/checkouts/index.html.erb b/app/views/checkouts/index.html.erb index 8b73264db0..9ae7ee9f0e 100644 --- a/app/views/checkouts/index.html.erb +++ b/app/views/checkouts/index.html.erb @@ -32,8 +32,8 @@ <%= checkout.user.user_key %><%= link_to checkout.media_object.title, main_app.media_object_url(checkout.media_object) %><%= Checkout.date_parser(checkout.checkout_time) %><%= Checkout.date_parser(checkout.return_time) %><%= checkout.checkout_time.to_s(:long_ordinal_12h) %><%= checkout.return_time.to_s(:long_ordinal_12h) %> <%= distance_of_time_in_words(checkout.return_time - DateTime.current) %> <% if checkout.return_time > DateTime.current %> diff --git a/config/initializers/time_formats.rb b/config/initializers/time_formats.rb new file mode 100644 index 0000000000..b3c37fc91e --- /dev/null +++ b/config/initializers/time_formats.rb @@ -0,0 +1 @@ +Time::DATE_FORMATS[:long_ordinal_12h] = lambda { |time| time.strftime("%B #{time.day.ordinalize}, %Y %l:%M %p") } diff --git a/spec/models/checkout_spec.rb b/spec/models/checkout_spec.rb index a6c281f348..28c04c500a 100644 --- a/spec/models/checkout_spec.rb +++ b/spec/models/checkout_spec.rb @@ -2,8 +2,6 @@ require 'cancan/matchers' RSpec.describe Checkout, type: :model do - include ActiveSupport::Testing::TimeHelpers - let(:checkout) { FactoryBot.create(:checkout) } describe 'validations' do @@ -90,16 +88,4 @@ expect(checkout.media_object).to eq media_object end end - - describe 'date_parser' do - before { Time.stub(:now) { Time.new(2000,01,13,12,00) } } - let(:checkout) { FactoryBot.create(:checkout, checkout_time: Time.now, return_time: Time.now + 1.day) } - it 'parses DateTime values into strings'do - expect(Checkout.date_parser(checkout.checkout_time)).to be_a(String) - expect(Checkout.date_parser(checkout.return_time)).to be_a(String) - end - it 'parses the dates to be human readable' do - expect(Checkout.date_parser(checkout.checkout_time)).to eq("January 13th, 2000 12:00 PM") - end - end end diff --git a/spec/views/checkouts/index.html.erb_spec.rb b/spec/views/checkouts/index.html.erb_spec.rb index a2d6198b9f..a8e7f692fe 100644 --- a/spec/views/checkouts/index.html.erb_spec.rb +++ b/spec/views/checkouts/index.html.erb_spec.rb @@ -3,7 +3,7 @@ RSpec.describe "checkouts/index", type: :view do let(:user) { FactoryBot.create(:user) } let(:ability) { Ability.new(user) } - let(:checkouts) { [FactoryBot.create(:checkout), FactoryBot.create(:checkout)] } + let(:checkouts) { [FactoryBot.create(:checkout, checkout_time: Time.new(2000, 01, 13, 12, 0, 0)), FactoryBot.create(:checkout)] } before(:each) do assign(:checkouts, checkouts) allow(view).to receive(:current_user).and_return(user) @@ -51,4 +51,11 @@ assert_select "tr>td", html: "#{checkouts.first.media_object.title}" end end + describe 'checkout and return time' do + it 'renders a human readable string' do + render + assert_select "tr>td", text: checkouts.first.checkout_time.to_s(:long_ordinal_12h) + expect(rendered).to include('January 13th, 2000 12:00 PM') + end + end end From a409b642133665ea0036689ded32dca27a6933f4 Mon Sep 17 00:00:00 2001 From: dananji Date: Thu, 4 Aug 2022 17:06:32 -0400 Subject: [PATCH 078/230] Updates to item page CDL controls --- app/assets/stylesheets/avalon.scss | 25 ++++++ app/assets/stylesheets/avalon/_buttons.scss | 11 +++ app/views/media_objects/_checkout.html.erb | 28 ++++++- .../media_objects/_destroy_checkout.html.erb | 81 +++++-------------- .../media_objects/_embed_checkout.html.erb | 14 ++-- .../media_objects/_metadata_display.html.erb | 2 +- config/locales/en.yml | 4 +- 7 files changed, 95 insertions(+), 70 deletions(-) diff --git a/app/assets/stylesheets/avalon.scss b/app/assets/stylesheets/avalon.scss index 543e33d0cf..70d02de718 100644 --- a/app/assets/stylesheets/avalon.scss +++ b/app/assets/stylesheets/avalon.scss @@ -1204,6 +1204,31 @@ td { .checkout { background-color: $dark; color: $white; + + p { + position: relative; + text-align: center; + } + + form { + position: relative; + left: 25%; + } + + .centered { + margin: auto; + position: absolute; + width: 50%; + left: 25%; + } + + .centered.video { + top: -50%; + left: 0; + right: 0; + bottom: 0; + height: 4rem; + } } .checkout.audio { diff --git a/app/assets/stylesheets/avalon/_buttons.scss b/app/assets/stylesheets/avalon/_buttons.scss index 9ff8d7c324..accb68fbe3 100644 --- a/app/assets/stylesheets/avalon/_buttons.scss +++ b/app/assets/stylesheets/avalon/_buttons.scss @@ -38,3 +38,14 @@ button.close { text-decoration: none; } } + +.check-out-btn { + // transition: transform 2s; + backface-visibility: hidden; +} + +.check-out-btn:hover { + transform:scale(1.1); + -webkit-transform:scale(1.1); + -moz-transform:scale(1.1); +} diff --git a/app/views/media_objects/_checkout.html.erb b/app/views/media_objects/_checkout.html.erb index e259457855..3b2c12af04 100644 --- a/app/views/media_objects/_checkout.html.erb +++ b/app/views/media_objects/_checkout.html.erb @@ -17,5 +17,29 @@ Unless required by applicable law or agreed to in writing, software distributed <%= form_for(Checkout.new, html: { style: "display: inline;" }) do |f| %> <%= hidden_field_tag "authenticity_token", form_authenticity_token %> <%= hidden_field_tag "checkout[media_object_id]", media_object_id %> - <%= f.submit "Check Out", class: "btn btn-secondary" %> -<% end %> \ No newline at end of file + <%= f.submit "Check Out", class: "btn btn-info check-out-btn", data: { lending_period: @media_object.lending_period } %> +<% end %> + +<% content_for :page_scripts do %> + +<% end %> diff --git a/app/views/media_objects/_destroy_checkout.html.erb b/app/views/media_objects/_destroy_checkout.html.erb index 4bfa2a5e24..8ee3992748 100644 --- a/app/views/media_objects/_destroy_checkout.html.erb +++ b/app/views/media_objects/_destroy_checkout.html.erb @@ -14,35 +14,38 @@ Unless required by applicable law or agreed to in writing, software distributed --- END LICENSE_HEADER BLOCK --- %> <% if can? :read, @media_object %> + <% current_checkout=@media_object.current_checkout(current_user.id) %>
      - <% current_checkout=@media_object.current_checkout(current_user.id) %> <% if @media_object.lending_status == "checked_out" && !current_checkout.nil? %> - <%= button_tag "Return Item", id: "return-btn", rel: "popover", class: "btn btn-danger", - data: { checkout_id: current_checkout.id, checkout_returntime: current_checkout.return_time } %> + <%= link_to 'Return now', return_checkout_url(current_checkout), class: 'btn btn-danger', method: :patch, + id: "return-btn", data: { checkout_returntime: current_checkout.return_time } %>

      Time
      remaining:

      0
      days
      00:00:00
      hh:mm:ss
      + <% end %>
      <% end %> - <% content_for :page_scripts do %> -<% end %> \ No newline at end of file +<% end %> diff --git a/app/views/media_objects/_embed_checkout.html.erb b/app/views/media_objects/_embed_checkout.html.erb index 599e7b2c64..f123a1d7ac 100644 --- a/app/views/media_objects/_embed_checkout.html.erb +++ b/app/views/media_objects/_embed_checkout.html.erb @@ -16,12 +16,14 @@ Unless required by applicable law or agreed to in writing, software distributed <% if !@masterFiles.blank? %> <% master_file=@media_object.master_files.first %>
      - <% if @media_object.lending_status == "available" %> - <%= t('media_object.cdl.checkout_message').html_safe %> - <%= render "checkout" %> - <% else %> - <%= t('media_object.cdl.not_available_message').html_safe %> - <% end %> +
      + <% if @media_object.lending_status == "available" %> + <%= t('media_object.cdl.checkout_message').html_safe %> + <%= render "checkout" %> + <% else %> + <%= t('media_object.cdl.not_available_message').html_safe %> + <% end %> +
      <% else %>
      diff --git a/app/views/media_objects/_metadata_display.html.erb b/app/views/media_objects/_metadata_display.html.erb index 84a39fa0a1..46b6e53021 100644 --- a/app/views/media_objects/_metadata_display.html.erb +++ b/app/views/media_objects/_metadata_display.html.erb @@ -73,7 +73,7 @@ Unless required by applicable law or agreed to in writing, software distributed
      <%= @media_object.access_text %>
      - Lending Period: <%= ActiveSupport::Duration.build(@media_object.lending_period).to_day_hour_s %> + Lending Period: <%= ActiveSupport::Duration.build(@media_object.lending_period).to_day_hour_s %>
      diff --git a/config/locales/en.yml b/config/locales/en.yml index 2e2dbb73a8..bb58c40d5b 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -23,10 +23,10 @@ en: empty_share_link_notice: "After processing has started the embedded link will be available." empty_share_section_permalink_notice: "After processing has started the section link will be available." cdl: - checkout_message: "

      To enable streaming please check out resource. This content is available to be checked out.

      " + checkout_message: "

      Borrow this item to access media resources.

      " not_available_message: "

      This resource is not available to be checked out at the moment. Please check again later.

      " expire: - heading: "Check Out Expired" + heading: "Check Out Expired!" message: "Your lending period has been expired. Please return the item, and check for availability later." metadata_tip: abstract: | From 6346b17d92e7d1eaede046912baca79f8918a308 Mon Sep 17 00:00:00 2001 From: dananji Date: Fri, 5 Aug 2022 10:00:31 -0400 Subject: [PATCH 079/230] Cleaner formatting of lending period string using ActiveSupport --- app/assets/stylesheets/avalon/_buttons.scss | 5 ----- app/views/media_objects/_checkout.html.erb | 16 +++------------- 2 files changed, 3 insertions(+), 18 deletions(-) diff --git a/app/assets/stylesheets/avalon/_buttons.scss b/app/assets/stylesheets/avalon/_buttons.scss index accb68fbe3..7383eab877 100644 --- a/app/assets/stylesheets/avalon/_buttons.scss +++ b/app/assets/stylesheets/avalon/_buttons.scss @@ -39,11 +39,6 @@ button.close { } } -.check-out-btn { - // transition: transform 2s; - backface-visibility: hidden; -} - .check-out-btn:hover { transform:scale(1.1); -webkit-transform:scale(1.1); diff --git a/app/views/media_objects/_checkout.html.erb b/app/views/media_objects/_checkout.html.erb index 3b2c12af04..6e727ac3ed 100644 --- a/app/views/media_objects/_checkout.html.erb +++ b/app/views/media_objects/_checkout.html.erb @@ -17,7 +17,8 @@ Unless required by applicable law or agreed to in writing, software distributed <%= form_for(Checkout.new, html: { style: "display: inline;" }) do |f| %> <%= hidden_field_tag "authenticity_token", form_authenticity_token %> <%= hidden_field_tag "checkout[media_object_id]", media_object_id %> - <%= f.submit "Check Out", class: "btn btn-info check-out-btn", data: { lending_period: @media_object.lending_period } %> + <%= f.submit "Check Out", class: "btn btn-info check-out-btn", + data: { lending_period: ActiveSupport::Duration.build(@media_object.lending_period).to_day_hour_s } %> <% end %> <% content_for :page_scripts do %> @@ -25,18 +26,7 @@ Unless required by applicable law or agreed to in writing, software distributed $(".check-out-btn").hover( function() { var lending_period = $(this).data().lendingPeriod; - var lending_period_str = ''; - if(lending_period > 0) { - var days = Math.floor(lending_period / (3600 * 24)); - lending_period -= days * (3600 * 24); - if(days > 0) { lending_period_str += `${days} days`}; - - var hours = Math.floor((lending_period / (60 * 60)) % 24); - lending_period -= hours * (60 * 60); - if(hours > 0) { lending_period_str += ` ${hours} hours`}; - - $(this).val(`Borrow for ${lending_period_str}`); - } + $(this).val(`Borrow for ${lending_period}`); }, function() { $(this).val('Check Out'); } From 3d16c67a34bdc01ca02ad3cbcb7cb1c7df1e438b Mon Sep 17 00:00:00 2001 From: dananji Date: Mon, 1 Aug 2022 14:21:08 -0400 Subject: [PATCH 080/230] Display remaining time for non-admin users --- .../_administrative_links.html.erb | 70 ++++++------------- .../media_objects/_destroy_checkout.html.erb | 12 ++-- app/views/media_objects/show.html.erb | 9 ++- 3 files changed, 37 insertions(+), 54 deletions(-) diff --git a/app/views/media_objects/_administrative_links.html.erb b/app/views/media_objects/_administrative_links.html.erb index 4492de71f8..fadea8d98a 100644 --- a/app/views/media_objects/_administrative_links.html.erb +++ b/app/views/media_objects/_administrative_links.html.erb @@ -14,52 +14,28 @@ Unless required by applicable law or agreed to in writing, software distributed --- END LICENSE_HEADER BLOCK --- %> <% if can? :update, @media_object %> -
      -

      -

      -

      - -
      -
      - <%= link_to 'Edit', edit_media_object_path(@media_object), class: 'btn btn-primary' %> - - <% if @media_object.published? %> - <% if can?(:unpublish, @media_object) %> - <%= link_to 'Unpublish', update_status_media_object_path(@media_object, status:'unpublish'), method: :put, class: 'btn btn-outline' %> - <% end %> - <% else %> - <%= link_to 'Publish', update_status_media_object_path(@media_object, status:'publish'), method: :put, class: 'btn btn-outline' %> - <% end %> - - <%# This might not be the best approach because it makes accidental - deletion possible just by following a link. Need to revisit when - extra cycles are available %> - - <% if can? :destroy, @media_object %> - <%= link_to 'Delete', confirm_remove_media_object_path(@media_object), class: 'btn btn-link' %> - <% end %> - - <% if Settings.intercom.present? and can? :intercom_push, @media_object %> - <%= button_tag(Settings.intercom['default']['push_label'], class: 'btn btn-outline', data: {toggle:"modal", target:"#intercom_push"}) %> - <%= render "intercom_push_modal" %> - <% end %> -
      -
      - <%= render 'destroy_checkout' %> -
      +
      + <%= link_to 'Edit', edit_media_object_path(@media_object), class: 'btn btn-primary' %> + + <% if @media_object.published? %> + <% if can?(:unpublish, @media_object) %> + <%= link_to 'Unpublish', update_status_media_object_path(@media_object, status:'unpublish'), method: :put, class: 'btn btn-outline' %> + <% end %> + <% else %> + <%= link_to 'Publish', update_status_media_object_path(@media_object, status:'publish'), method: :put, class: 'btn btn-outline' %> + <% end %> + + <%# This might not be the best approach because it makes accidental + deletion possible just by following a link. Need to revisit when + extra cycles are available %> + + <% if can? :destroy, @media_object %> + <%= link_to 'Delete', confirm_remove_media_object_path(@media_object), class: 'btn btn-link' %> + <% end %> + + <% if Settings.intercom.present? and can? :intercom_push, @media_object %> + <%= button_tag(Settings.intercom['default']['push_label'], class: 'btn btn-outline', data: {toggle:"modal", target:"#intercom_push"}) %> + <%= render "intercom_push_modal" %> + <% end %>
      -
      <% end %> diff --git a/app/views/media_objects/_destroy_checkout.html.erb b/app/views/media_objects/_destroy_checkout.html.erb index 8ee3992748..199cf531c7 100644 --- a/app/views/media_objects/_destroy_checkout.html.erb +++ b/app/views/media_objects/_destroy_checkout.html.erb @@ -13,12 +13,12 @@ Unless required by applicable law or agreed to in writing, software distributed specific language governing permissions and limitations under the License. --- END LICENSE_HEADER BLOCK --- %> -<% if can? :read, @media_object %> - <% current_checkout=@media_object.current_checkout(current_user.id) %> -
      - <% if @media_object.lending_status == "checked_out" && !current_checkout.nil? %> +<% current_checkout=@media_object.current_checkout(current_user.id) %> +<% if (can? :read, @media_object) && !current_checkout.nil? %> +
      > +
      <%= link_to 'Return now', return_checkout_url(current_checkout), class: 'btn btn-danger', method: :patch, - id: "return-btn", data: { checkout_returntime: current_checkout.return_time } %> + id: "return-btn", data: { checkout_returntime: current_checkout.return_time } %>

      Time
      remaining:

      0
      days
      @@ -42,7 +42,7 @@ Unless required by applicable law or agreed to in writing, software distributed
      - <% end %> +
      <% end %> diff --git a/app/views/media_objects/show.html.erb b/app/views/media_objects/show.html.erb index 09efc2de56..43baf4843c 100644 --- a/app/views/media_objects/show.html.erb +++ b/app/views/media_objects/show.html.erb @@ -26,5 +26,12 @@ Unless required by applicable law or agreed to in writing, software distributed -<%= render 'administrative_links' %> +
      + <% if can? :update, @media_object %> +
      + <%= render 'administrative_links' %> +
      + <% end %> + <%= render 'destroy_checkout' %> +
      <%= render 'item_view' %> From 34497996801bc98f5828e49e707f7473597a6d9a Mon Sep 17 00:00:00 2001 From: dananji Date: Mon, 1 Aug 2022 15:09:51 -0400 Subject: [PATCH 081/230] Fix for failing test --- app/views/media_objects/_destroy_checkout.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/media_objects/_destroy_checkout.html.erb b/app/views/media_objects/_destroy_checkout.html.erb index 199cf531c7..82e82c1a7f 100644 --- a/app/views/media_objects/_destroy_checkout.html.erb +++ b/app/views/media_objects/_destroy_checkout.html.erb @@ -13,7 +13,7 @@ Unless required by applicable law or agreed to in writing, software distributed specific language governing permissions and limitations under the License. --- END LICENSE_HEADER BLOCK --- %> -<% current_checkout=@media_object.current_checkout(current_user.id) %> +<% current_checkout=@media_object.current_checkout(current_user.id) if current_user %> <% if (can? :read, @media_object) && !current_checkout.nil? %>
      >
      From f3a5a6c9a9c39185dde5867f5f204e4d6f8c7791 Mon Sep 17 00:00:00 2001 From: dananji Date: Fri, 5 Aug 2022 11:15:01 -0400 Subject: [PATCH 082/230] Add back in hidden content on view page --- .../media_objects/_administrative_links.html.erb | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/app/views/media_objects/_administrative_links.html.erb b/app/views/media_objects/_administrative_links.html.erb index fadea8d98a..43731f8244 100644 --- a/app/views/media_objects/_administrative_links.html.erb +++ b/app/views/media_objects/_administrative_links.html.erb @@ -14,6 +14,22 @@ Unless required by applicable law or agreed to in writing, software distributed --- END LICENSE_HEADER BLOCK --- %> <% if can? :update, @media_object %> +

      +

      +

      <%= link_to 'Edit', edit_media_object_path(@media_object), class: 'btn btn-primary' %> From 1aa31a59c25ec02bb2b21ab6c6a9672de9714b6d Mon Sep 17 00:00:00 2001 From: dananji Date: Fri, 5 Aug 2022 14:41:57 -0400 Subject: [PATCH 083/230] Lending period countdown changes --- app/assets/stylesheets/avalon.scss | 13 +++++++------ .../media_objects/_destroy_checkout.html.erb | 18 +++++++++--------- config/locales/en.yml | 1 + 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/app/assets/stylesheets/avalon.scss b/app/assets/stylesheets/avalon.scss index 70d02de718..98084607cb 100644 --- a/app/assets/stylesheets/avalon.scss +++ b/app/assets/stylesheets/avalon.scss @@ -1171,7 +1171,7 @@ td { /* CDL controls on view page styles */ .cdl-controls { display: inline-flex; - margin-bottom: 10px; + margin-bottom: 1rem; height: 2.5rem; float: right; @@ -1187,17 +1187,18 @@ td { .remaining-time p { line-height: 1rem; - margin-left: 1rem; + margin: 0.25rem; text-align: left; } .remaining-time span{ color: #fff; - margin-left: 3px; - padding: 2px 5px; - border-radius: 3px; + margin-left: 0.25rem; + padding: 0.15rem 0.25rem; + border-radius: 0.15rem; background: $primary; - font-size: x-small; + font-size: small; + line-height: initial; } } diff --git a/app/views/media_objects/_destroy_checkout.html.erb b/app/views/media_objects/_destroy_checkout.html.erb index 8ee3992748..8f8da695ea 100644 --- a/app/views/media_objects/_destroy_checkout.html.erb +++ b/app/views/media_objects/_destroy_checkout.html.erb @@ -20,9 +20,9 @@ Unless required by applicable law or agreed to in writing, software distributed <%= link_to 'Return now', return_checkout_url(current_checkout), class: 'btn btn-danger', method: :patch, id: "return-btn", data: { checkout_returntime: current_checkout.return_time } %>
      -

      Time
      remaining:

      + <%= t('media_object.cdl.time_remaining').html_safe %> 0
      days
      - 00:00:00
      hh:mm:ss
      + 00:00
      hh:mm
      - <% if can? :update, @media_object %> -
      - <%= render 'administrative_links' %> -
      - <% end %> + <%= render 'administrative_links' %> <%= render 'destroy_checkout' %>
      <%= render 'item_view' %> From 726030b5ee9db867dcd4d988ee9389758816ab76 Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Wed, 10 Aug 2022 09:59:09 -0400 Subject: [PATCH 086/230] Fix validation check on collection page --- .../admin/collections_controller.rb | 44 +++++++------------ app/models/access_control_step.rb | 8 ++-- .../admin_collections_controller_spec.rb | 12 ++++- 3 files changed, 32 insertions(+), 32 deletions(-) diff --git a/app/controllers/admin/collections_controller.rb b/app/controllers/admin/collections_controller.rb index b591a77e26..cd2b30441e 100644 --- a/app/controllers/admin/collections_controller.rb +++ b/app/controllers/admin/collections_controller.rb @@ -1,11 +1,11 @@ # Copyright 2011-2022, The Trustees of Indiana University and Northwestern # University. Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. -# +# # You may obtain a copy of the License at -# +# # http://www.apache.org/licenses/LICENSE-2.0 -# +# # Unless required by applicable law or agreed to in writing, software distributed # under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR # CONDITIONS OF ANY KIND, either express or implied. See the License for the @@ -299,35 +299,23 @@ def update_access(collection, params) collection.default_visibility = params[:visibility] unless params[:visibility].blank? collection.default_hidden = params[:hidden] == "1" - lending_period = build_default_lending_period(collection) - collection.default_lending_period = lending_period if lending_period.positive? + lending_period = build_default_lending_period(params[:add_lending_period_days], params[:add_lending_period_hours]) + if lending_period.positive? + collection.default_lending_period = lending_period + elsif lending_period == 0 + flash[:error] = "Lending period must be greater than 0." + else + flash[:error] = "Lending period days and hours need to be positive integers." + end end - def build_default_lending_period(collection) + def build_default_lending_period(d, h) lending_period = 0 - - begin - if params["add_lending_period_days"].to_i.positive? - lending_period += params["add_lending_period_days"].to_i.days - else - collection.errors.add(:lending_period, "days needs to be a positive integer.") - end - rescue - collection.errors.add(:lending_period, "days needs to be a positive integer.") - end - - begin - if params["add_lending_period_hours"].to_i.positive? - lending_period += params["add_lending_period_hours"].to_i.hours - else - collection.errors.add(:lending_period, "hours needs to be a positive integer.") - end - rescue - collection.errors.add(:lending_period, "hours needs to be a positive integer.") - end - - flash[:notice] = collection.errors.full_messages.join if collection.errors.present? + lending_period += d.to_i.days if d.to_i.positive? + lending_period += h.to_i.hours if h.to_i.positive? lending_period.to_i + rescue + 0 end def apply_access(collection, params) diff --git a/app/models/access_control_step.rb b/app/models/access_control_step.rb index f43a2329b7..f6ed82f104 100644 --- a/app/models/access_control_step.rb +++ b/app/models/access_control_step.rb @@ -1,11 +1,11 @@ # Copyright 2011-2022, The Trustees of Indiana University and Northwestern # University. Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. -# +# # You may obtain a copy of the License at -# +# # http://www.apache.org/licenses/LICENSE-2.0 -# +# # Unless required by applicable law or agreed to in writing, software distributed # under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR # CONDITIONS OF ANY KIND, either express or implied. See the License for the @@ -101,6 +101,8 @@ def execute context lending_period = build_lending_period(context['add_lending_period_days'], context['add_lending_period_hours']) if lending_period.positive? media_object.lending_period = lending_period + elsif + context[:error] = "Lending_period must be greater than 0." else context[:error] = "Lending period days and hours need to be positive integers." end diff --git a/spec/controllers/admin_collections_controller_spec.rb b/spec/controllers/admin_collections_controller_spec.rb index 409766b46e..193a41a7a6 100644 --- a/spec/controllers/admin_collections_controller_spec.rb +++ b/spec/controllers/admin_collections_controller_spec.rb @@ -462,7 +462,17 @@ it "returns error if invalid" do expect { put 'update', params: { id: collection.id, save_access: "Save Access Settings", add_lending_period_days: -1, add_lending_period_hours: -1 } }.not_to change { collection.reload.default_lending_period } expect(response).to redirect_to(admin_collection_path(collection)) - expect(flash[:notice]).to be_present + expect(flash[:error]).to be_present + expect { put 'update', params: { id: collection.id, save_access: "Save Access Settings", add_lending_period_days: 0, add_lending_period_hours: 0 } }.not_to change { collection.reload.default_lending_period } + expect(response).to redirect_to(admin_collection_path(collection)) + expect(flash[:error]).to be_present + end + + it "accepts 0 as a valid day or hour value" do + expect { put 'update', params: { id: collection.id, save_access: "Save Access Settings", add_lending_period_days: 0, add_lending_period_hours: 1 } }.to change { collection.reload.default_lending_period }.to(3600) + expect(flash[:error]).not_to be_present + expect { put 'update', params: { id: collection.id, save_access: "Save Access Settings", add_lending_period_days: 1, add_lending_period_hours: 0 } }.to change { collection.reload.default_lending_period }.to(86400) + expect(flash[:error]).not_to be_present end end end From 30f4d894839932c5db6c88096558661b6e619a8f Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Wed, 10 Aug 2022 11:33:50 -0400 Subject: [PATCH 087/230] Fix issues for codeclimate --- app/controllers/admin/collections_controller.rb | 2 +- app/models/access_control_step.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/admin/collections_controller.rb b/app/controllers/admin/collections_controller.rb index cd2b30441e..7d12efed97 100644 --- a/app/controllers/admin/collections_controller.rb +++ b/app/controllers/admin/collections_controller.rb @@ -302,7 +302,7 @@ def update_access(collection, params) lending_period = build_default_lending_period(params[:add_lending_period_days], params[:add_lending_period_hours]) if lending_period.positive? collection.default_lending_period = lending_period - elsif lending_period == 0 + elsif lending_period.zero? flash[:error] = "Lending period must be greater than 0." else flash[:error] = "Lending period days and hours need to be positive integers." diff --git a/app/models/access_control_step.rb b/app/models/access_control_step.rb index f6ed82f104..402fe35f5e 100644 --- a/app/models/access_control_step.rb +++ b/app/models/access_control_step.rb @@ -101,7 +101,7 @@ def execute context lending_period = build_lending_period(context['add_lending_period_days'], context['add_lending_period_hours']) if lending_period.positive? media_object.lending_period = lending_period - elsif + elsif lending_period.zero? context[:error] = "Lending_period must be greater than 0." else context[:error] = "Lending period days and hours need to be positive integers." From 6fa1e584abf5c6dc64ab62967e9d00a4e2a480d3 Mon Sep 17 00:00:00 2001 From: Mason Ballengee <68433277+masaball@users.noreply.github.com> Date: Wed, 10 Aug 2022 11:36:29 -0400 Subject: [PATCH 088/230] Fix typo --- app/models/access_control_step.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/access_control_step.rb b/app/models/access_control_step.rb index 402fe35f5e..f4d42f75a0 100644 --- a/app/models/access_control_step.rb +++ b/app/models/access_control_step.rb @@ -102,7 +102,7 @@ def execute context if lending_period.positive? media_object.lending_period = lending_period elsif lending_period.zero? - context[:error] = "Lending_period must be greater than 0." + context[:error] = "Lending period must be greater than 0." else context[:error] = "Lending period days and hours need to be positive integers." end From 0fe8ed0286ae5b136f73359709eb0cbf250821cb Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Thu, 11 Aug 2022 14:46:24 -0400 Subject: [PATCH 089/230] Refactor error generation --- app/controllers/admin/collections_controller.rb | 14 ++++++++------ app/models/access_control_step.rb | 15 +++++++++------ .../admin_collections_controller_spec.rb | 10 ++++++++++ .../controllers/media_objects_controller_spec.rb | 16 +++++++++++++--- 4 files changed, 40 insertions(+), 15 deletions(-) diff --git a/app/controllers/admin/collections_controller.rb b/app/controllers/admin/collections_controller.rb index 7d12efed97..27cc89c353 100644 --- a/app/controllers/admin/collections_controller.rb +++ b/app/controllers/admin/collections_controller.rb @@ -299,20 +299,22 @@ def update_access(collection, params) collection.default_visibility = params[:visibility] unless params[:visibility].blank? collection.default_hidden = params[:hidden] == "1" - lending_period = build_default_lending_period(params[:add_lending_period_days], params[:add_lending_period_hours]) + lending_period = build_default_lending_period(collection) if lending_period.positive? collection.default_lending_period = lending_period elsif lending_period.zero? flash[:error] = "Lending period must be greater than 0." - else - flash[:error] = "Lending period days and hours need to be positive integers." end end - def build_default_lending_period(d, h) + def build_default_lending_period(collection) lending_period = 0 - lending_period += d.to_i.days if d.to_i.positive? - lending_period += h.to_i.hours if h.to_i.positive? + d = params["add_lending_period_days"].to_i + h = params["add_lending_period_hours"].to_i + d.negative? ? collection.errors.add(:lending_period, "days needs to be a positive integer.") : lending_period += d.days + h.negative? ? collection.errors.add(:lending_period, "hours needs to be a positive integer.") : lending_period += h.hours + + flash[:error] = collection.errors.full_messages.join if collection.errors.present? lending_period.to_i rescue 0 diff --git a/app/models/access_control_step.rb b/app/models/access_control_step.rb index f4d42f75a0..a822ae6bc0 100644 --- a/app/models/access_control_step.rb +++ b/app/models/access_control_step.rb @@ -98,13 +98,11 @@ def execute context unless limited_access_submit media_object.visibility = context[:visibility] unless context[:visibility].blank? media_object.hidden = context[:hidden] == "1" - lending_period = build_lending_period(context['add_lending_period_days'], context['add_lending_period_hours']) + lending_period = build_lending_period(context) if lending_period.positive? media_object.lending_period = lending_period elsif lending_period.zero? context[:error] = "Lending period must be greater than 0." - else - context[:error] = "Lending period days and hours need to be positive integers." end end @@ -128,10 +126,15 @@ def execute context private - def build_lending_period(d, h) + def build_lending_period(context) lending_period = 0 - lending_period += d.to_i.days if d.to_i.positive? - lending_period += h.to_i.hours if h.to_i.positive? + errors = [] + d = context["add_lending_period_days"].to_i + h = context["add_lending_period_hours"].to_i + d.negative? ? errors.append("Lending period days needs to be a positive integer.") : lending_period += d.days + h.negative? ? errors.append("Lending period hours needs to be a positive integer.") : lending_period += h.hours + + context[:error] = errors.join if errors.present? lending_period.to_i rescue 0 diff --git a/spec/controllers/admin_collections_controller_spec.rb b/spec/controllers/admin_collections_controller_spec.rb index 193a41a7a6..9acbfed472 100644 --- a/spec/controllers/admin_collections_controller_spec.rb +++ b/spec/controllers/admin_collections_controller_spec.rb @@ -463,9 +463,19 @@ expect { put 'update', params: { id: collection.id, save_access: "Save Access Settings", add_lending_period_days: -1, add_lending_period_hours: -1 } }.not_to change { collection.reload.default_lending_period } expect(response).to redirect_to(admin_collection_path(collection)) expect(flash[:error]).to be_present + expect(flash[:error]).to eq("Lending period must be greater than 0.") + put 'update', params: { id: collection.id, save_access: "Save Access Settings", add_lending_period_days: -1, add_lending_period_hours: 1 } + expect(response).to redirect_to(admin_collection_path(collection)) + expect(flash[:error]).to be_present + expect(flash[:error]).to eq("Lending period days needs to be a positive integer.") + put 'update', params: { id: collection.id, save_access: "Save Access Settings", add_lending_period_days: 1, add_lending_period_hours: -1 } + expect(response).to redirect_to(admin_collection_path(collection)) + expect(flash[:error]).to be_present + expect(flash[:error]).to eq("Lending period hours needs to be a positive integer.") expect { put 'update', params: { id: collection.id, save_access: "Save Access Settings", add_lending_period_days: 0, add_lending_period_hours: 0 } }.not_to change { collection.reload.default_lending_period } expect(response).to redirect_to(admin_collection_path(collection)) expect(flash[:error]).to be_present + expect(flash[:error]).to eq("Lending period must be greater than 0.") end it "accepts 0 as a valid day or hour value" do diff --git a/spec/controllers/media_objects_controller_spec.rb b/spec/controllers/media_objects_controller_spec.rb index 6068db774c..73e0065aeb 100644 --- a/spec/controllers/media_objects_controller_spec.rb +++ b/spec/controllers/media_objects_controller_spec.rb @@ -1,11 +1,11 @@ # Copyright 2011-2022, The Trustees of Indiana University and Northwestern # University. Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. -# +# # You may obtain a copy of the License at -# +# # http://www.apache.org/licenses/LICENSE-2.0 -# +# # Unless required by applicable law or agreed to in writing, software distributed # under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR # CONDITIONS OF ANY KIND, either express or implied. See the License for the @@ -1341,6 +1341,16 @@ it "returns error if invalid" do expect { put :update, params: { id: media_object.id, step: 'access-control', donot_advance: 'true', add_lending_period_days: -1, add_lending_period_hours: -1 } }.not_to change { media_object.reload.lending_period } expect(flash[:error]).to be_present + expect(flash[:error]).to eq("Lending period must be greater than 0.") + put :update, params: { id: media_object.id, step: 'access-control', donot_advance: 'true', add_lending_period_days: 1, add_lending_period_hours: -1 } + expect(flash[:error]).to be_present + expect(flash[:error]).to eq("Lending period hours needs to be a positive integer.") + put :update, params: { id: media_object.id, step: 'access-control', donot_advance: 'true', add_lending_period_days: -1, add_lending_period_hours: 1 } + expect(flash[:error]).to be_present + expect(flash[:error]).to eq("Lending period days needs to be a positive integer.") + expect { put :update, params: { id: media_object.id, step: 'access-control', donot_advance: 'true', add_lending_period_days: 0, add_lending_period_hours: 0 } }.not_to change { media_object.reload.lending_period } + expect(flash[:error]).to be_present + expect(flash[:error]).to eq("Lending period must be greater than 0.") end end end From c198677b0cb3475c68d84d3e83b621e018d2f040 Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Mon, 8 Aug 2022 15:51:17 -0400 Subject: [PATCH 090/230] Add CDL conditional logic Controlled digital lending needs to be able to be turned on and off. This adds conditional checks to routes, controllers, views, etc. to enable on/off functionality. --- app/jobs/bulk_action_jobs.rb | 10 +- app/views/_user_util_links.html.erb | 14 +- app/views/media_objects/_item_view.html.erb | 6 +- .../media_objects/_metadata_display.html.erb | 4 +- app/views/modules/_access_control.html.erb | 64 ++--- config/locales/en.yml | 2 +- config/routes.rb | 2 +- config/settings.yml | 2 +- .../admin_collections_controller_spec.rb | 7 + .../media_objects_controller_spec.rb | 243 ++++++++++++------ spec/features/media_object_spec.rb | 33 ++- spec/jobs/bulk_action_job_spec.rb | 26 +- spec/requests/checkouts_spec.rb | 2 + spec/routing/checkouts_routing_spec.rb | 82 ++++-- 14 files changed, 336 insertions(+), 161 deletions(-) diff --git a/app/jobs/bulk_action_jobs.rb b/app/jobs/bulk_action_jobs.rb index cb735cedb0..cf58d67bba 100644 --- a/app/jobs/bulk_action_jobs.rb +++ b/app/jobs/bulk_action_jobs.rb @@ -1,11 +1,11 @@ # Copyright 2011-2022, The Trustees of Indiana University and Northwestern # University. Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. -# +# # You may obtain a copy of the License at -# +# # http://www.apache.org/licenses/LICENSE-2.0 -# +# # Unless required by applicable law or agreed to in writing, software distributed # under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR # CONDITIONS OF ANY KIND, either express or implied. See the License for the @@ -205,7 +205,9 @@ def perform(collection_id, overwrite = true) media_object = MediaObject.find(id) media_object.hidden = collection.default_hidden media_object.visibility = collection.default_visibility - media_object.lending_period = collection.default_lending_period + if Settings.controlled_digital_lending.enable + media_object.lending_period = collection.default_lending_period + end # Special access if overwrite diff --git a/app/views/_user_util_links.html.erb b/app/views/_user_util_links.html.erb index 6869765d1e..08ed0340cc 100644 --- a/app/views/_user_util_links.html.erb +++ b/app/views/_user_util_links.html.erb @@ -32,13 +32,15 @@ Unless required by applicable law or agreed to in writing, software distributed <%= link_to 'Timelines', main_app.timelines_path, id:'timelines_nav', class: 'nav-link' %> <% end %> - <% if current_ability.can? :create, Checkout %> - <% end %> - <% end %> <% if render_bookmarks_control? %>
      -
      -
      -

      Item lending period

      -
      -
      -
      - <%= render partial: "modules/tooltip", locals: { form: vid, field: :lending_period, tooltip: t("access_control.#{:lending_period}"), options: {display_label: (t("access_control.#{:lending_period}label")+'*').html_safe} } %>
      - <% d, h = (lending_period/3600).divmod(24) %> -
      -
      - - <%= text_field_tag "add_lending_period_days", d ? d : 0, class: 'form-control' %> -
      -
      - - <%= text_field_tag "add_lending_period_hours", h ? h : 0, class: 'form-control' %> +<% if Settings.controlled_digital_lending.enable %> +
      +
      +

      Item lending period

      +
      +
      +
      + <%= render partial: "modules/tooltip", locals: { form: vid, field: :lending_period, tooltip: t("access_control.#{:lending_period}"), options: {display_label: (t("access_control.#{:lending_period}label")+'*').html_safe} } %>
      + <% d, h = (lending_period/3600).divmod(24) %> +
      +
      + + <%= text_field_tag "add_lending_period_days", d ? d : 0, class: 'form-control' %> +
      +
      + + <%= text_field_tag "add_lending_period_hours", h ? h : 0, class: 'form-control' %> +
      -
      +<% end %> <%= render modal[:partial], modal_title: modal[:title] if defined? modal %> @@ -130,17 +132,19 @@ Unless required by applicable law or agreed to in writing, software distributed
      -
      -
      -

      Item lending period

      -
      -
      -
      - Item is available to be checked out for - <%= ActiveSupport::Duration.build(lending_period).to_day_hour_s %> +<% if Settings.controlled_digital_lending.enable %> +
      +
      +

      Item lending period

      +
      +
      +
      + Item is available to be checked out for + <%= ActiveSupport::Duration.build(lending_period).to_day_hour_s %> +
      -
      +<% end %> <% end #form_for %> diff --git a/config/locales/en.yml b/config/locales/en.yml index 3541320c8d..68ea889412 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -172,7 +172,7 @@ en: or ffaa:aaff:bbcc:ddee:1122:3344:5566:7777/ffff:ffff:ffff:ffff:ffff:ffff:ffff:ff00 Start and end dates are not required; if included, access for the specified IP Address(es) will start at the beginning of the start date and end at the beginning of the end date. Otherwise, access will be open ended for the specified IP Address(es). lending_period: | - Lending Period is the length of time that an item may be checked out for. + Lending Period is the length of time that an item may be checked out for. Day and Hour values must be positive integers. contact: title: 'Contact Us - %{application_name}' diff --git a/config/routes.rb b/config/routes.rb index 3a4a6596c1..0d7162d25f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -26,7 +26,7 @@ end end - resources :checkouts, only: [:index, :create, :show, :update, :destroy] do + resources :checkouts, only: [:index, :create, :show, :update, :destroy], :constraints => lambda { |request| Settings.controlled_digital_lending.enable } do collection do patch :return_all end diff --git a/config/settings.yml b/config/settings.yml index 23b67d36fa..f0da422f0b 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -96,6 +96,6 @@ active_storage: service: local #bucket: supplementalfiles controlled_digital_lending: - enable: true + enable: false default_lending_period: 'P14D' # ISO8601 duration format: P14D == 14.days, PT8H == 8.hours, etc. max_checkouts_per_user: 25 diff --git a/spec/controllers/admin_collections_controller_spec.rb b/spec/controllers/admin_collections_controller_spec.rb index 9acbfed472..006c83903e 100644 --- a/spec/controllers/admin_collections_controller_spec.rb +++ b/spec/controllers/admin_collections_controller_spec.rb @@ -455,6 +455,7 @@ end context "changing lending period" do + before { allow(Settings.controlled_digital_lending).to receive(:enable).and_return(true) } it "sets a custom lending period" do expect { put 'update', params: { id: collection.id, save_access: "Save Access Settings", add_lending_period_days: 7, add_lending_period_hours: 8 } }.to change { collection.reload.default_lending_period }.to(633600) end @@ -484,6 +485,12 @@ expect { put 'update', params: { id: collection.id, save_access: "Save Access Settings", add_lending_period_days: 1, add_lending_period_hours: 0 } }.to change { collection.reload.default_lending_period }.to(86400) expect(flash[:error]).not_to be_present end + + it "returns error if both day and hour are 0" do + expect { put 'update', params: { id: collection.id, save_access: "Save Access Settings", add_lending_period_days: 0, add_lending_period_hours: 0 } }.not_to change { collection.reload.default_lending_period } + expect(response).to redirect_to(admin_collection_path(collection)) + expect(flash[:notice]).to be_present + end end end diff --git a/spec/controllers/media_objects_controller_spec.rb b/spec/controllers/media_objects_controller_spec.rb index 73e0065aeb..5f0f73fbe6 100644 --- a/spec/controllers/media_objects_controller_spec.rb +++ b/spec/controllers/media_objects_controller_spec.rb @@ -754,7 +754,173 @@ context "Conditional Share partials should be rendered" do let!(:media_object) { FactoryBot.create(:published_media_object, :with_master_file, visibility: 'public') } - context "With check out" do + context 'With cdl enabled' do + before { allow(Settings.controlled_digital_lending).to receive(:enable).and_return(true) } + context "With check out" do + context "Normal login" do + it "administrators: should include lti, embed, and share" do + login_as(:administrator) + FactoryBot.create(:checkout, media_object_id: media_object.id, user_id: controller.current_user.id) + get :show, params: { id: media_object.id } + expect(response).to render_template(:_share_resource) + expect(response).to render_template(:_embed_resource) + expect(response).to render_template(:_lti_url) + end + it "managers: should include lti, embed, and share" do + login_user media_object.collection.managers.first + FactoryBot.create(:checkout, media_object_id: media_object.id, user_id: controller.current_user.id) + get :show, params: { id: media_object.id } + expect(response).to render_template(:_share_resource) + expect(response).to render_template(:_embed_resource) + expect(response).to render_template(:_lti_url) + end + it "editors: should include lti, embed, and share" do + login_user media_object.collection.editors.first + FactoryBot.create(:checkout, media_object_id: media_object.id, user_id: controller.current_user.id) + get :show, params: { id: media_object.id } + expect(response).to render_template(:_share_resource) + expect(response).to render_template(:_embed_resource) + expect(response).to render_template(:_lti_url) + end + it "others: should include embed and share and NOT lti" do + login_as(:user) + FactoryBot.create(:checkout, media_object_id: media_object.id, user_id: controller.current_user.id) + get :show, params: { id: media_object.id } + expect(response).to render_template(:_share_resource) + expect(response).to render_template(:_embed_resource) + expect(response).to_not render_template(:_lti_url) + end + end + context "LTI login" do + it "administrators/managers/editors: should include lti, embed, and share" do + login_lti 'administrator' + lti_group = @controller.user_session[:virtual_groups].first + FactoryBot.create(:published_media_object, visibility: 'private', read_groups: [lti_group]) + FactoryBot.create(:checkout, media_object_id: media_object.id, user_id: controller.current_user.id) + get :show, params: { id: media_object.id } + expect(response).to render_template(:_share_resource) + expect(response).to render_template(:_embed_resource) + expect(response).to render_template(:_lti_url) + end + it "others: should include only lti" do + login_lti 'student' + lti_group = @controller.user_session[:virtual_groups].first + FactoryBot.create(:published_media_object, visibility: 'private', read_groups: [lti_group]) + FactoryBot.create(:checkout, media_object_id: media_object.id, user_id: controller.current_user.id) + get :show, params: { id: media_object.id } + expect(response).to_not render_template(:_share_resource) + expect(response).to_not render_template(:_embed_resource) + expect(response).to render_template(:_lti_url) + end + end + context "No share tabs rendered" do + before do + @original_conditional_partials = controller.class.conditional_partials.deep_dup + controller.class.conditional_partials[:share].each {|partial_name, conditions| conditions[:if] = false } + end + after do + controller.class.conditional_partials = @original_conditional_partials + end + it "should not render Share button" do + # allow(@controller).to receive(:evaluate_if_unless_configuration).and_return false + # allow(@controller).to receive(:is_editor_or_not_lti).and_return false + expect(response).to_not render_template(:_share) + end + end + context "No LTI configuration" do + around do |example| + providers = Avalon::Authentication::Providers + Avalon::Authentication::Providers = Avalon::Authentication::Providers.reject{|p| p[:provider] == :lti} + example.run + Avalon::Authentication::Providers = providers + end + it "should not include lti" do + login_as(:administrator) + FactoryBot.create(:checkout, media_object_id: media_object.id, user_id: controller.current_user.id) + get :show, params: { id: media_object.id } + expect(response).to render_template(:_share_resource) + expect(response).to render_template(:_embed_resource) + expect(response).to_not render_template(:_lti_url) + end + end + end + context "Without check out" do + context "Normal login" do + it "administrators: should include lti, embed, and share" do + login_as(:administrator) + get :show, params: { id: media_object.id } + expect(response).not_to render_template(:_share_resource) + expect(response).to render_template(:_embed_checkout) + end + it "managers: should include lti, embed, and share" do + login_user media_object.collection.managers.first + get :show, params: { id: media_object.id } + expect(response).not_to render_template(:_share_resource) + expect(response).to render_template(:_embed_checkout) + end + it "editors: should include lti, embed, and share" do + login_user media_object.collection.editors.first + get :show, params: { id: media_object.id } + expect(response).not_to render_template(:_share_resource) + expect(response).to render_template(:_embed_checkout) + end + it "others: should include embed and share and NOT lti" do + login_as(:user) + get :show, params: { id: media_object.id } + expect(response).not_to render_template(:_share_resource) + expect(response).to render_template(:_embed_checkout) + end + end + context "LTI login" do + it "administrators/managers/editors: should include lti, embed, and share" do + login_lti 'administrator' + lti_group = @controller.user_session[:virtual_groups].first + FactoryBot.create(:published_media_object, visibility: 'private', read_groups: [lti_group]) + get :show, params: { id: media_object.id } + expect(response).not_to render_template(:_share_resource) + expect(response).to render_template(:_embed_checkout) + end + it "others: should include only lti" do + login_lti 'student' + lti_group = @controller.user_session[:virtual_groups].first + FactoryBot.create(:published_media_object, visibility: 'private', read_groups: [lti_group]) + get :show, params: { id: media_object.id } + expect(response).not_to render_template(:_share_resource) + expect(response).to render_template(:_embed_checkout) + end + end + context "No share tabs rendered" do + before do + @original_conditional_partials = controller.class.conditional_partials.deep_dup + controller.class.conditional_partials[:share].each {|partial_name, conditions| conditions[:if] = false } + end + after do + controller.class.conditional_partials = @original_conditional_partials + end + it "should not render Share button" do + # allow(@controller).to receive(:evaluate_if_unless_configuration).and_return false + # allow(@controller).to receive(:is_editor_or_not_lti).and_return false + expect(response).to_not render_template(:_share) + end + end + context "No LTI configuration" do + around do |example| + providers = Avalon::Authentication::Providers + Avalon::Authentication::Providers = Avalon::Authentication::Providers.reject{|p| p[:provider] == :lti} + example.run + Avalon::Authentication::Providers = providers + end + it "should not include lti" do + login_as(:administrator) + get :show, params: { id: media_object.id } + expect(response).not_to render_template(:_share_resource) + expect(response).to render_template(:_embed_checkout) + end + end + end + end + context "With cdl disabled" do + before { allow(Settings.controlled_digital_lending).to receive(:enable).and_return(false) } context "Normal login" do it "administrators: should include lti, embed, and share" do login_as(:administrator) @@ -842,80 +1008,6 @@ end end end - context "Without check out" do - context "Normal login" do - it "administrators: should include lti, embed, and share" do - login_as(:administrator) - get :show, params: { id: media_object.id } - expect(response).not_to render_template(:_share_resource) - expect(response).to render_template(:_embed_checkout) - end - it "managers: should include lti, embed, and share" do - login_user media_object.collection.managers.first - get :show, params: { id: media_object.id } - expect(response).not_to render_template(:_share_resource) - expect(response).to render_template(:_embed_checkout) - end - it "editors: should include lti, embed, and share" do - login_user media_object.collection.editors.first - get :show, params: { id: media_object.id } - expect(response).not_to render_template(:_share_resource) - expect(response).to render_template(:_embed_checkout) - end - it "others: should include embed and share and NOT lti" do - login_as(:user) - get :show, params: { id: media_object.id } - expect(response).not_to render_template(:_share_resource) - expect(response).to render_template(:_embed_checkout) - end - end - context "LTI login" do - it "administrators/managers/editors: should include lti, embed, and share" do - login_lti 'administrator' - lti_group = @controller.user_session[:virtual_groups].first - FactoryBot.create(:published_media_object, visibility: 'private', read_groups: [lti_group]) - get :show, params: { id: media_object.id } - expect(response).not_to render_template(:_share_resource) - expect(response).to render_template(:_embed_checkout) - end - it "others: should include only lti" do - login_lti 'student' - lti_group = @controller.user_session[:virtual_groups].first - FactoryBot.create(:published_media_object, visibility: 'private', read_groups: [lti_group]) - get :show, params: { id: media_object.id } - expect(response).not_to render_template(:_share_resource) - expect(response).to render_template(:_embed_checkout) - end - end - context "No share tabs rendered" do - before do - @original_conditional_partials = controller.class.conditional_partials.deep_dup - controller.class.conditional_partials[:share].each {|partial_name, conditions| conditions[:if] = false } - end - after do - controller.class.conditional_partials = @original_conditional_partials - end - it "should not render Share button" do - # allow(@controller).to receive(:evaluate_if_unless_configuration).and_return false - # allow(@controller).to receive(:is_editor_or_not_lti).and_return false - expect(response).to_not render_template(:_share) - end - end - context "No LTI configuration" do - around do |example| - providers = Avalon::Authentication::Providers - Avalon::Authentication::Providers = Avalon::Authentication::Providers.reject{|p| p[:provider] == :lti} - example.run - Avalon::Authentication::Providers = providers - end - it "should not include lti" do - login_as(:administrator) - get :show, params: { id: media_object.id } - expect(response).not_to render_template(:_share_resource) - expect(response).to render_template(:_embed_checkout) - end - end - end end context "correctly handle unfound streams/sections" do @@ -1334,6 +1426,7 @@ end context "lending period" do + before { allow(Settings.controlled_digital_lending).to receive(:enable).and_return(true) } it "sets a custom lending period" do expect { put :update, params: { id: media_object.id, step: 'access-control', donot_advance: 'true', add_lending_period_days: 7, add_lending_period_hours: 8 } }.to change { media_object.reload.lending_period }.to(633600) end diff --git a/spec/features/media_object_spec.rb b/spec/features/media_object_spec.rb index 798cd6302a..8a81c29388 100644 --- a/spec/features/media_object_spec.rb +++ b/spec/features/media_object_spec.rb @@ -1,11 +1,11 @@ # Copyright 2011-2022, The Trustees of Indiana University and Northwestern # University. Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. -# +# # You may obtain a copy of the License at -# +# # http://www.apache.org/licenses/LICENSE-2.0 -# +# # Unless required by applicable law or agreed to in writing, software distributed # under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR # CONDITIONS OF ANY KIND, either express or implied. See the License for the @@ -57,19 +57,32 @@ visit media_object_path(media_object) expect(page.has_content?(summary)).to be_truthy end - it 'displays the contriburs properly' do + it 'displays the contributors properly' do contributor = 'Jamie Lannister' media_object.contributor = [contributor] media_object.save visit media_object_path(media_object) expect(page.has_content?(contributor)).to be_truthy end - it 'displays the lending period properly' do - lending_period = 90000 - media_object.lending_period = lending_period - media_object.save - visit media_object_path(media_object) - expect(page.has_content?('1 day 1 hour')).to be_truthy + context 'cdl is enabled' do + before { allow(Settings.controlled_digital_lending).to receive(:enable).and_return(true) } + it 'displays the lending period properly' do + lending_period = 90000 + media_object.lending_period = lending_period + media_object.save + visit media_object_path(media_object) + expect(page.has_content?('1 day 1 hour')).to be_truthy + end + end + context 'cdl is disabled' do + before { allow(Settings.controlled_digital_lending).to receive(:enable).and_return(false) } + it 'does not display the lending period' do + lending_period = 90000 + media_object.lending_period = lending_period + media_object.save + visit media_object_path(media_object) + expect(page.has_content?('1 day 1 hour')).to be_falsey + end end end end diff --git a/spec/jobs/bulk_action_job_spec.rb b/spec/jobs/bulk_action_job_spec.rb index db8af16bc4..6f834cb57c 100644 --- a/spec/jobs/bulk_action_job_spec.rb +++ b/spec/jobs/bulk_action_job_spec.rb @@ -1,11 +1,11 @@ # Copyright 2011-2022, The Trustees of Indiana University and Northwestern # University. Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. -# +# # You may obtain a copy of the License at -# +# # http://www.apache.org/licenses/LICENSE-2.0 -# +# # Unless required by applicable law or agreed to in writing, software distributed # under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR # CONDITIONS OF ANY KIND, either express or implied. See the License for the @@ -97,10 +97,22 @@ def check_push(result) expect(mo.visibility).to eq('public') end - it "changes item lending period" do - BulkActionJobs::ApplyCollectionAccessControl.perform_now co.id, true - mo.reload - expect(mo.lending_period).to eq(co.default_lending_period) + context "with cdl enabled" do + before { allow(Settings.controlled_digital_lending).to receive(:enable).and_return(true) } + it "changes item lending period" do + BulkActionJobs::ApplyCollectionAccessControl.perform_now co.id, true + mo.reload + expect(mo.lending_period).to eq(co.default_lending_period) + end + end + + context "with cdl disabled" do + before { allow(Settings.controlled_digital_lending).to receive(:enable).and_return(false) } + it "does not change item lending period" do + BulkActionJobs::ApplyCollectionAccessControl.perform_now co.id, true + mo.reload + expect(mo.lending_period).not_to eq(co.default_lending_period) + end end context "overwrite is true" do diff --git a/spec/requests/checkouts_spec.rb b/spec/requests/checkouts_spec.rb index 1c1cc71ab4..68d1e856e9 100644 --- a/spec/requests/checkouts_spec.rb +++ b/spec/requests/checkouts_spec.rb @@ -32,6 +32,8 @@ before { sign_in(user) } + before { allow(Settings.controlled_digital_lending).to receive(:enable).and_return(true) } + describe "GET /index" do before { checkout } diff --git a/spec/routing/checkouts_routing_spec.rb b/spec/routing/checkouts_routing_spec.rb index 31860952cb..e10731a7c3 100644 --- a/spec/routing/checkouts_routing_spec.rb +++ b/spec/routing/checkouts_routing_spec.rb @@ -2,36 +2,74 @@ RSpec.describe CheckoutsController, type: :routing do describe "routing" do - it "routes to #index" do - expect(get: "/checkouts").to route_to("checkouts#index") - end + context "controlled digital lending is enabled" do + before { allow(Settings.controlled_digital_lending).to receive(:enable).and_return(true) } + it "routes to #index" do + expect(get: "/checkouts").to route_to("checkouts#index") + end - it "routes to #create" do - expect(post: "/checkouts").to route_to("checkouts#create") - end + it "routes to #create" do + expect(post: "/checkouts").to route_to("checkouts#create") + end - it "routes to #show via GET" do - expect(get: "/checkouts/1").to route_to("checkouts#show", id: "1") - end + it "routes to #show via GET" do + expect(get: "/checkouts/1").to route_to("checkouts#show", id: "1") + end - it "routes to #update via PUT" do - expect(put: "/checkouts/1").to route_to("checkouts#update", id: "1") - end + it "routes to #update via PUT" do + expect(put: "/checkouts/1").to route_to("checkouts#update", id: "1") + end - it "routes to #update via PATCH" do - expect(patch: "/checkouts/1").to route_to("checkouts#update", id: "1") - end + it "routes to #update via PATCH" do + expect(patch: "/checkouts/1").to route_to("checkouts#update", id: "1") + end - it "routes to #destroy" do - expect(delete: "/checkouts/1").to route_to("checkouts#destroy", id: "1") - end + it "routes to #destroy" do + expect(delete: "/checkouts/1").to route_to("checkouts#destroy", id: "1") + end + + it "routes to #return_all" do + expect(patch: "/checkouts/return_all").to route_to("checkouts#return_all") + end - it "routes to #return_all" do - expect(patch: "/checkouts/return_all").to route_to("checkouts#return_all") + it "routes to #return" do + expect(patch: "/checkouts/1/return").to route_to("checkouts#return", id: "1") + end end - it "routes to #return" do - expect(patch: "/checkouts/1/return").to route_to("checkouts#return", id: "1") + context "controlled digital lending is disabled" do + before { allow(Settings.controlled_digital_lending).to receive(:enable).and_return(false) } + it "does not route to #index" do + expect(get: "/checkouts").not_to route_to("checkouts#index") + end + + it "does not route to #create" do + expect(post: "/checkouts").not_to route_to("checkouts#create") + end + + it "does not route to to #show via GET" do + expect(get: "/checkouts/1").not_to route_to("checkouts#show", id: "1") + end + + it "does not route to #update via PUT" do + expect(put: "/checkouts/1").not_to route_to("checkouts#update", id: "1") + end + + it "does not route to #update via PATCH" do + expect(patch: "/checkouts/1").not_to route_to("checkouts#update", id: "1") + end + + it "does not route to #destroy" do + expect(delete: "/checkouts/1").not_to route_to("checkouts#destroy", id: "1") + end + + it "does not route to #return_all" do + expect(patch: "/checkouts/return_all").not_to route_to("checkouts#return_all") + end + + it "does not route to #return" do + expect(patch: "/checkouts/1/return").not_to route_to("checkouts#return", id: "1") + end end end end From 10b4d0589a66562efdcec6d4b35955023c8c115d Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Wed, 10 Aug 2022 17:08:02 -0400 Subject: [PATCH 091/230] Add convenience method for checking cdl settings Co-authored-by: Chris Colvard --- app/controllers/admin/collections_controller.rb | 8 +++----- app/jobs/bulk_action_jobs.rb | 2 +- app/models/access_control_step.rb | 12 +++++++----- app/views/_user_util_links.html.erb | 2 +- app/views/media_objects/_item_view.html.erb | 2 +- app/views/media_objects/_metadata_display.html.erb | 2 +- app/views/modules/_access_control.html.erb | 4 ++-- config/locales/en.yml | 4 +++- config/routes.rb | 2 +- lib/avalon/configuration.rb | 11 ++++++++--- 10 files changed, 28 insertions(+), 21 deletions(-) diff --git a/app/controllers/admin/collections_controller.rb b/app/controllers/admin/collections_controller.rb index 27cc89c353..f49321b335 100644 --- a/app/controllers/admin/collections_controller.rb +++ b/app/controllers/admin/collections_controller.rb @@ -299,11 +299,9 @@ def update_access(collection, params) collection.default_visibility = params[:visibility] unless params[:visibility].blank? collection.default_hidden = params[:hidden] == "1" - lending_period = build_default_lending_period(collection) - if lending_period.positive? - collection.default_lending_period = lending_period - elsif lending_period.zero? - flash[:error] = "Lending period must be greater than 0." + if Avalon::Configuration.controlled_digital_lending_enabled? + lending_period = build_default_lending_period(collection) + collection.default_lending_period = lending_period if lending_period.positive? end end diff --git a/app/jobs/bulk_action_jobs.rb b/app/jobs/bulk_action_jobs.rb index cf58d67bba..98bca410e1 100644 --- a/app/jobs/bulk_action_jobs.rb +++ b/app/jobs/bulk_action_jobs.rb @@ -205,7 +205,7 @@ def perform(collection_id, overwrite = true) media_object = MediaObject.find(id) media_object.hidden = collection.default_hidden media_object.visibility = collection.default_visibility - if Settings.controlled_digital_lending.enable + if Avalon::Configuration.controlled_digital_lending_enabled? media_object.lending_period = collection.default_lending_period end diff --git a/app/models/access_control_step.rb b/app/models/access_control_step.rb index a822ae6bc0..2c90181f3c 100644 --- a/app/models/access_control_step.rb +++ b/app/models/access_control_step.rb @@ -98,11 +98,13 @@ def execute context unless limited_access_submit media_object.visibility = context[:visibility] unless context[:visibility].blank? media_object.hidden = context[:hidden] == "1" - lending_period = build_lending_period(context) - if lending_period.positive? - media_object.lending_period = lending_period - elsif lending_period.zero? - context[:error] = "Lending period must be greater than 0." + if Avalon::Configuration.controlled_digital_lending_enabled? + lending_period = build_lending_period(context['add_lending_period_days'], context['add_lending_period_hours']) + if lending_period.positive? + media_object.lending_period = lending_period + else + context[:error] = "Lending period days and hours need to be positive integers." + end end end diff --git a/app/views/_user_util_links.html.erb b/app/views/_user_util_links.html.erb index 08ed0340cc..b22abeaf2f 100644 --- a/app/views/_user_util_links.html.erb +++ b/app/views/_user_util_links.html.erb @@ -32,7 +32,7 @@ Unless required by applicable law or agreed to in writing, software distributed <%= link_to 'Timelines', main_app.timelines_path, id:'timelines_nav', class: 'nav-link' %> <% end %> - <% if Settings.controlled_digital_lending.enable %> + <% if Avalon::Configuration.controlled_digital_lending_enabled? %> <% if current_ability.can? :create, Checkout %>
      -<% if Settings.controlled_digital_lending.enable %> +<% if Avalon::Configuration.controlled_digital_lending_enabled? %>

      Item lending period

      @@ -132,7 +132,7 @@ Unless required by applicable law or agreed to in writing, software distributed
      -<% if Settings.controlled_digital_lending.enable %> +<% if Avalon::Configuration.controlled_digital_lending_enabled? %>

      Item lending period

      diff --git a/config/locales/en.yml b/config/locales/en.yml index 68ea889412..4d23a54ff3 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -172,7 +172,9 @@ en: or ffaa:aaff:bbcc:ddee:1122:3344:5566:7777/ffff:ffff:ffff:ffff:ffff:ffff:ffff:ff00 Start and end dates are not required; if included, access for the specified IP Address(es) will start at the beginning of the start date and end at the beginning of the end date. Otherwise, access will be open ended for the specified IP Address(es). lending_period: | - Lending Period is the length of time that an item may be checked out for. Day and Hour values must be positive integers. + Lending Period is the length of time that an item may be checked out for. + Lending Period must be greater than 0. + Day and Hour values must be positive integers. contact: title: 'Contact Us - %{application_name}' diff --git a/config/routes.rb b/config/routes.rb index 0d7162d25f..ed7e9fda16 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -26,7 +26,7 @@ end end - resources :checkouts, only: [:index, :create, :show, :update, :destroy], :constraints => lambda { |request| Settings.controlled_digital_lending.enable } do + resources :checkouts, only: [:index, :create, :show, :update, :destroy], :constraints => lambda { |request| Avalon::Configuration.controlled_digital_lending_enabled? } do collection do patch :return_all end diff --git a/lib/avalon/configuration.rb b/lib/avalon/configuration.rb index 9e6d22d106..cc4a386dc9 100644 --- a/lib/avalon/configuration.rb +++ b/lib/avalon/configuration.rb @@ -1,11 +1,11 @@ # Copyright 2011-2022, The Trustees of Indiana University and Northwestern # University. Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. -# +# # You may obtain a copy of the License at -# +# # http://www.apache.org/licenses/LICENSE-2.0 -# +# # Unless required by applicable law or agreed to in writing, software distributed # under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR # CONDITIONS OF ANY KIND, either express or implied. See the License for the @@ -35,6 +35,11 @@ def sanitize_filename end end + # To be called as Avalon::Configuration.controlled_digital_lending_enabled? + def controlled_digital_lending_enabled? + !!Settings.controlled_digital_lending&.enable + end + private class << self def coerce(value, method) From f2ec65fed78f3a4485c1a77ae3763a100e4e17c9 Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Fri, 12 Aug 2022 14:30:38 -0400 Subject: [PATCH 092/230] Fix rebase conflicts --- app/controllers/admin/collections_controller.rb | 8 ++++++-- app/models/access_control_step.rb | 8 ++++---- config/locales/en.yml | 1 - spec/controllers/admin_collections_controller_spec.rb | 2 +- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/app/controllers/admin/collections_controller.rb b/app/controllers/admin/collections_controller.rb index f49321b335..511c794400 100644 --- a/app/controllers/admin/collections_controller.rb +++ b/app/controllers/admin/collections_controller.rb @@ -301,7 +301,11 @@ def update_access(collection, params) collection.default_hidden = params[:hidden] == "1" if Avalon::Configuration.controlled_digital_lending_enabled? lending_period = build_default_lending_period(collection) - collection.default_lending_period = lending_period if lending_period.positive? + if lending_period.positive? + collection.default_lending_period = lending_period + elsif lending_period.zero? + flash[:error] = "Lending period must be greater than 0." + end end end @@ -312,7 +316,7 @@ def build_default_lending_period(collection) d.negative? ? collection.errors.add(:lending_period, "days needs to be a positive integer.") : lending_period += d.days h.negative? ? collection.errors.add(:lending_period, "hours needs to be a positive integer.") : lending_period += h.hours - flash[:error] = collection.errors.full_messages.join if collection.errors.present? + flash[:error] = collection.errors.full_messages.join(' ') if collection.errors.present? lending_period.to_i rescue 0 diff --git a/app/models/access_control_step.rb b/app/models/access_control_step.rb index 2c90181f3c..e0e42590ac 100644 --- a/app/models/access_control_step.rb +++ b/app/models/access_control_step.rb @@ -99,11 +99,11 @@ def execute context media_object.visibility = context[:visibility] unless context[:visibility].blank? media_object.hidden = context[:hidden] == "1" if Avalon::Configuration.controlled_digital_lending_enabled? - lending_period = build_lending_period(context['add_lending_period_days'], context['add_lending_period_hours']) + lending_period = build_lending_period(context) if lending_period.positive? media_object.lending_period = lending_period - else - context[:error] = "Lending period days and hours need to be positive integers." + elsif lending_period.zero? + context[:error] = "Lending period must be greater than 0." end end end @@ -136,7 +136,7 @@ def build_lending_period(context) d.negative? ? errors.append("Lending period days needs to be a positive integer.") : lending_period += d.days h.negative? ? errors.append("Lending period hours needs to be a positive integer.") : lending_period += h.hours - context[:error] = errors.join if errors.present? + context[:error] = errors.join(' ') if errors.present? lending_period.to_i rescue 0 diff --git a/config/locales/en.yml b/config/locales/en.yml index 4d23a54ff3..ae46722a66 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -173,7 +173,6 @@ en: Start and end dates are not required; if included, access for the specified IP Address(es) will start at the beginning of the start date and end at the beginning of the end date. Otherwise, access will be open ended for the specified IP Address(es). lending_period: | Lending Period is the length of time that an item may be checked out for. - Lending Period must be greater than 0. Day and Hour values must be positive integers. contact: diff --git a/spec/controllers/admin_collections_controller_spec.rb b/spec/controllers/admin_collections_controller_spec.rb index 006c83903e..37c1cb163d 100644 --- a/spec/controllers/admin_collections_controller_spec.rb +++ b/spec/controllers/admin_collections_controller_spec.rb @@ -489,7 +489,7 @@ it "returns error if both day and hour are 0" do expect { put 'update', params: { id: collection.id, save_access: "Save Access Settings", add_lending_period_days: 0, add_lending_period_hours: 0 } }.not_to change { collection.reload.default_lending_period } expect(response).to redirect_to(admin_collection_path(collection)) - expect(flash[:notice]).to be_present + expect(flash[:error]).to be_present end end end From 6ae7b0f4b5552502e3c4866de6ba5b48c3f5a0ab Mon Sep 17 00:00:00 2001 From: Chris Colvard Date: Fri, 12 Aug 2022 15:06:23 -0400 Subject: [PATCH 093/230] Stay in modal when links clicked inside modal Without this change If you click the `more>>` link to open a facet modal then click `Next` or `A-Z sort` it would cause a page reload and the modal is the page contents. This change makes the ajax request update the currently open modal without a page reload. --- app/views/catalog/_facet_pagination.html.erb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/views/catalog/_facet_pagination.html.erb b/app/views/catalog/_facet_pagination.html.erb index 6444448f9d..af4ae9e9e3 100644 --- a/app/views/catalog/_facet_pagination.html.erb +++ b/app/views/catalog/_facet_pagination.html.erb @@ -14,11 +14,11 @@ Unless required by applicable law or agreed to in writing, software distributed --- END LICENSE_HEADER BLOCK --- %> @@ -26,9 +26,9 @@ Unless required by applicable law or agreed to in writing, software distributed
      <% if @pagination.sort == 'index' -%> <%= t('blacklight.search.facets.sort.index') %> - <%= link_to(t('blacklight.search.facets.sort.count'), @pagination.params_for_resort_url('count', search_state.to_h), class: "sort_change numeric btn btn-outline", data: { ajax_modal: "preserve" }) %> + <%= link_to(t('blacklight.search.facets.sort.count'), @pagination.params_for_resort_url('count', search_state.to_h), class: "sort_change numeric btn btn-outline", data: { blacklight_modal: "preserve" }) %> <% elsif @pagination.sort == 'count' -%> - <%= link_to(t('blacklight.search.facets.sort.index'), @pagination.params_for_resort_url('index', search_state.to_h), class: "sort_change az btn btn-outline", data: { ajax_modal: "preserve" }) %> + <%= link_to(t('blacklight.search.facets.sort.index'), @pagination.params_for_resort_url('index', search_state.to_h), class: "sort_change az btn btn-outline", data: { blacklight_modal: "preserve" }) %> <%= t('blacklight.search.facets.sort.count') %> <% end -%>
      From c061cd5d430208c733da07367ff800bd2879e046 Mon Sep 17 00:00:00 2001 From: Chris Colvard Date: Wed, 17 Aug 2022 14:26:48 -0400 Subject: [PATCH 094/230] Styling changes for collection search cards --- .../components/collections/Collection.scss | 14 +++++++++++++- .../collections/landing/SearchResultsCard.js | 4 ++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/app/javascript/components/collections/Collection.scss b/app/javascript/components/collections/Collection.scss index 481b716589..10b8f1c314 100644 --- a/app/javascript/components/collections/Collection.scss +++ b/app/javascript/components/collections/Collection.scss @@ -72,6 +72,8 @@ .card-body { padding-top: 5px; padding-bottom: 0px; + padding-right: 1rem; + padding-left: 1rem; } } @@ -83,6 +85,16 @@ } } +.card-text { + font-size: 14px; + + dt, dd { + width: 100%; + padding-right: 8px; + padding-left: 8px; + } +} + .search-within-facets { margin-left: 16px; } @@ -205,4 +217,4 @@ display: block; width: fit-content; margin-top: 10px; -} \ No newline at end of file +} diff --git a/app/javascript/components/collections/landing/SearchResultsCard.js b/app/javascript/components/collections/landing/SearchResultsCard.js index 0fde8c633f..1ad2a43190 100644 --- a/app/javascript/components/collections/landing/SearchResultsCard.js +++ b/app/javascript/components/collections/landing/SearchResultsCard.js @@ -35,8 +35,8 @@ const CardMetaData = ({ doc, fieldLabel, fieldName }) => { if (doc.attributes[fieldName]) { return ( -
      {fieldLabel}
      -
      +
      {fieldLabel}
      +
      ); } From 9c618403e88e965574a09a02be03077c93bdcbcf Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Fri, 19 Aug 2022 10:06:52 -0400 Subject: [PATCH 095/230] Fix duplication of notices on checkouts page --- app/views/checkouts/index.html.erb | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/views/checkouts/index.html.erb b/app/views/checkouts/index.html.erb index 9ae7ee9f0e..c3a03516a3 100644 --- a/app/views/checkouts/index.html.erb +++ b/app/views/checkouts/index.html.erb @@ -1,5 +1,3 @@ -

      <%= notice %>

      -

      Checkouts

      From 76b3107b105a38eac1d52143a410cf2d05398299 Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Fri, 19 Aug 2022 10:39:15 -0400 Subject: [PATCH 096/230] Remove flash messages from checkout actions --- app/controllers/checkouts_controller.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/controllers/checkouts_controller.rb b/app/controllers/checkouts_controller.rb index 58af00212f..7cf778e062 100644 --- a/app/controllers/checkouts_controller.rb +++ b/app/controllers/checkouts_controller.rb @@ -32,7 +32,7 @@ def create respond_to do |format| # TODO: move this can? check into a checkout ability (can?(:create, @checkout)) if can?(:create, @checkout) && @checkout.save - format.html { redirect_to media_object_path(checkout_params[:media_object_id]), flash: { success: "Checkout was successfully created." } } + format.html { redirect_to media_object_path(checkout_params[:media_object_id]) } format.json { render :show, status: :created, location: @checkout } else format.json { render json: @checkout.errors, status: :unprocessable_entity } @@ -56,10 +56,9 @@ def update def return @checkout.update(return_time: DateTime.current) - flash[:notice] = "Checkout was successfully returned." respond_to do |format| - format.html { redirect_back fallback_location: checkouts_url, notice: flash[:notice] } - format.json { render json: flash[:notice] } + format.html { redirect_back fallback_location: checkouts_url } + format.json { head :no_content } end end @@ -68,7 +67,7 @@ def return_all @checkouts.each { |c| c.update(return_time: DateTime.current) } respond_to do |format| - format.html { redirect_to checkouts_url, notice: "All checkouts were successfully returned." } + format.html { redirect_to checkouts_url } format.json { head :no_content } end end @@ -77,6 +76,7 @@ def return_all def destroy @checkout.destroy flash[:notice] = "Checkout was successfully destroyed." + respond_to do |format| format.html { redirect_to checkouts_url, notice: flash[:notice] } format.json { render json: flash[:notice] } From bb0b891e3984b5c279d6bb1c9e0b96051806fc48 Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Fri, 19 Aug 2022 14:58:04 -0400 Subject: [PATCH 097/230] Fix deletion of users with checkouts This commit also fixes a deprecation warning related to running Rails 6. --- app/controllers/samvera/persona/users_controller.rb | 8 ++++---- app/models/user.rb | 7 ++++--- .../samvera/persona/user_controller_spec.rb | 12 +++++++++--- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/app/controllers/samvera/persona/users_controller.rb b/app/controllers/samvera/persona/users_controller.rb index 3ad5d7e1cd..e2dcd70a4c 100644 --- a/app/controllers/samvera/persona/users_controller.rb +++ b/app/controllers/samvera/persona/users_controller.rb @@ -1,11 +1,11 @@ # Copyright 2011-2022, The Trustees of Indiana University and Northwestern # University. Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. -# +# # You may obtain a copy of the License at -# +# # http://www.apache.org/licenses/LICENSE-2.0 -# +# # Unless required by applicable law or agreed to in writing, software distributed # under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR # CONDITIONS OF ANY KIND, either express or implied. See the License for the @@ -191,7 +191,7 @@ def load_user def app_view_path my_engine_root = Samvera::Persona::Engine.root.to_s - prepend_view_path "#{my_engine_root}/app/views/#{Rails.application.class.parent_name.downcase}" + prepend_view_path "#{my_engine_root}/app/views/#{Rails.application.class.module_parent_name.downcase}" prepend_view_path Rails.root.join('app', 'views') end diff --git a/app/models/user.rb b/app/models/user.rb index e0d5f1a838..4e29420d9f 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,11 +1,11 @@ # Copyright 2011-2022, The Trustees of Indiana University and Northwestern # University. Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. -# +# # You may obtain a copy of the License at -# +# # http://www.apache.org/licenses/LICENSE-2.0 -# +# # Unless required by applicable law or agreed to in writing, software distributed # under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR # CONDITIONS OF ANY KIND, either express or implied. See the License for the @@ -13,6 +13,7 @@ # --- END LICENSE_HEADER BLOCK --- class User < ActiveRecord::Base + has_many :checkouts, dependent: :destroy attr_writer :login # Connects this user object to Hydra behaviors. include Hydra::User diff --git a/spec/controllers/samvera/persona/user_controller_spec.rb b/spec/controllers/samvera/persona/user_controller_spec.rb index c338976886..8b959e48b8 100644 --- a/spec/controllers/samvera/persona/user_controller_spec.rb +++ b/spec/controllers/samvera/persona/user_controller_spec.rb @@ -1,11 +1,11 @@ # Copyright 2011-2022, The Trustees of Indiana University and Northwestern # University. Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. -# +# # You may obtain a copy of the License at -# +# # http://www.apache.org/licenses/LICENSE-2.0 -# +# # Unless required by applicable law or agreed to in writing, software distributed # under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR # CONDITIONS OF ANY KIND, either express or implied. See the License for the @@ -156,6 +156,8 @@ describe 'DELETE #destroy' do before :each do + FactoryBot.create(:checkout, user_id: user.id) + FactoryBot.create(:checkout, user_id: user.id, return_time: DateTime.current - 1.day) new_hash = {"administrator"=>[user.username], "group_manager"=>[user.username, "alice.archivist@example.edu"], "registered"=>["bob.user@example.edu"]} RoleMap.replace_with!(new_hash) end @@ -179,6 +181,10 @@ expect(RoleMap.all.find_by(entry: 'alice.archivist@example.edu').entry).to eq 'alice.archivist@example.edu' expect(RoleMap.all.find_by(entry: 'bob.user@example.edu').entry).to eq 'bob.user@example.edu' end + + it 'deletes the user\'s checkouts' do + expect(Checkout.find_by(user_id: user.id)).to be(nil) + end end end From 76959141cc86a9e2975f5665be926f8cac724e3d Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Fri, 19 Aug 2022 17:06:34 -0400 Subject: [PATCH 098/230] Move checkout destroy test to model spec --- .../samvera/persona/user_controller_spec.rb | 6 ------ spec/models/user_spec.rb | 12 +++++++++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/spec/controllers/samvera/persona/user_controller_spec.rb b/spec/controllers/samvera/persona/user_controller_spec.rb index 8b959e48b8..bcce0d4e80 100644 --- a/spec/controllers/samvera/persona/user_controller_spec.rb +++ b/spec/controllers/samvera/persona/user_controller_spec.rb @@ -156,8 +156,6 @@ describe 'DELETE #destroy' do before :each do - FactoryBot.create(:checkout, user_id: user.id) - FactoryBot.create(:checkout, user_id: user.id, return_time: DateTime.current - 1.day) new_hash = {"administrator"=>[user.username], "group_manager"=>[user.username, "alice.archivist@example.edu"], "registered"=>["bob.user@example.edu"]} RoleMap.replace_with!(new_hash) end @@ -181,10 +179,6 @@ expect(RoleMap.all.find_by(entry: 'alice.archivist@example.edu').entry).to eq 'alice.archivist@example.edu' expect(RoleMap.all.find_by(entry: 'bob.user@example.edu').entry).to eq 'bob.user@example.edu' end - - it 'deletes the user\'s checkouts' do - expect(Checkout.find_by(user_id: user.id)).to be(nil) - end end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 5523ec5c37..1d84d5a60c 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -1,11 +1,11 @@ # Copyright 2011-2022, The Trustees of Indiana University and Northwestern # University. Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. -# +# # You may obtain a copy of the License at -# +# # http://www.apache.org/licenses/LICENSE-2.0 -# +# # Unless required by applicable law or agreed to in writing, software distributed # under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR # CONDITIONS OF ANY KIND, either express or implied. See the License for the @@ -136,6 +136,12 @@ bookmark = Bookmark.create(document_id: Faker::Number.digit, user: user) expect { user.destroy }.to change { Bookmark.exists? bookmark.id }.from( true ).to( false ) end + it 'removes checkouts for user' do + user = FactoryBot.create(:public) + active_checkout = FactoryBot.create(:checkout, user_id: user.id) + returned_checkout = FactoryBot.create(:checkout, user_id: user.id, return_time: DateTime.current - 1.day) + expect { user.destroy }.to change { Checkout.all.count }.from(2).to(0) + end end describe '#timeline_tags' do From 0124abc9b166e445c6e5ff160f5c6af21b997e85 Mon Sep 17 00:00:00 2001 From: dananji Date: Mon, 22 Aug 2022 10:27:42 -0700 Subject: [PATCH 099/230] New SME build with keydown event handling fixed --- yarn.lock | 90 ++++++++++++++++++++++++++++++------------------------- 1 file changed, 50 insertions(+), 40 deletions(-) diff --git a/yarn.lock b/yarn.lock index acf87bf262..44905afc07 100644 --- a/yarn.lock +++ b/yarn.lock @@ -894,9 +894,9 @@ "@babel/plugin-transform-react-pure-annotations" "^7.14.5" "@babel/runtime@^7.1.2", "@babel/runtime@^7.15.4", "@babel/runtime@^7.2.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.8.3", "@babel/runtime@^7.9.2": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.18.6.tgz#6a1ef59f838debd670421f8c7f2cbb8da9751580" - integrity sha512-t9wi7/AW6XtKahAe20Yw0/mMljKq0B1r2fPdvaAdV/KPDZewFXdaaa6K7lxmZBZ8FBNpCiAT6iHPmd6QO9bKfQ== + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.18.9.tgz#b4fcfce55db3d2e5e080d2490f608a3b9f407f4a" + integrity sha512-lkqXDcvlFT5rvEjiu6+QYO+1GXrEHRo2LOtS7E4GtX5ESIZOgepqsZBVIj6Pv+a6zqsya9VCgiK1KAK4BvJDAw== dependencies: regenerator-runtime "^0.13.4" @@ -3841,9 +3841,9 @@ hex-color-regex@^1.1.0: integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ== hls.js@^1.1.2: - version "1.1.5" - resolved "https://registry.yarnpkg.com/hls.js/-/hls.js-1.1.5.tgz#923a8a8cfdf09542578696d47c8ae435da981ffd" - integrity sha512-mQX5TSNtJEzGo5HPpvcQgCu+BWoKDQM6YYtg/KbgWkmVAcqOCvSTi0SuqG2ZJLXxIzdnFcKU2z7Mrw/YQWhPOA== + version "1.2.1" + resolved "https://registry.yarnpkg.com/hls.js/-/hls.js-1.2.1.tgz#09b0207c60fcb3340a88e8d3d1523799fe5fbedf" + integrity sha512-+m/5+ikSpmQQvb6FmVWZUZfzvTJMn/QVfiCGP1Oq9WW4RKrAvxlExkhhbcVGgGqLNPFk1kdFkVQur//wKu3JVw== "hls.js@https://github.com/avalonmediasystem/hls.js#stricter_ts_probing": version "0.13.1" @@ -4542,69 +4542,69 @@ json5@^2.1.2: minimist "^1.2.5" jss-plugin-camel-case@^10.5.1: - version "10.9.0" - resolved "https://registry.yarnpkg.com/jss-plugin-camel-case/-/jss-plugin-camel-case-10.9.0.tgz#4921b568b38d893f39736ee8c4c5f1c64670aaf7" - integrity sha512-UH6uPpnDk413/r/2Olmw4+y54yEF2lRIV8XIZyuYpgPYTITLlPOsq6XB9qeqv+75SQSg3KLocq5jUBXW8qWWww== + version "10.9.2" + resolved "https://registry.yarnpkg.com/jss-plugin-camel-case/-/jss-plugin-camel-case-10.9.2.tgz#76dddfa32f9e62d17daa4e3504991fd0933b89e1" + integrity sha512-wgBPlL3WS0WDJ1lPJcgjux/SHnDuu7opmgQKSraKs4z8dCCyYMx9IDPFKBXQ8Q5dVYij1FFV0WdxyhuOOAXuTg== dependencies: "@babel/runtime" "^7.3.1" hyphenate-style-name "^1.0.3" - jss "10.9.0" + jss "10.9.2" jss-plugin-default-unit@^10.5.1: - version "10.9.0" - resolved "https://registry.yarnpkg.com/jss-plugin-default-unit/-/jss-plugin-default-unit-10.9.0.tgz#bb23a48f075bc0ce852b4b4d3f7582bc002df991" - integrity sha512-7Ju4Q9wJ/MZPsxfu4T84mzdn7pLHWeqoGd/D8O3eDNNJ93Xc8PxnLmV8s8ZPNRYkLdxZqKtm1nPQ0BM4JRlq2w== + version "10.9.2" + resolved "https://registry.yarnpkg.com/jss-plugin-default-unit/-/jss-plugin-default-unit-10.9.2.tgz#3e7f4a1506b18d8fe231554fd982439feb2a9c53" + integrity sha512-pYg0QX3bBEFtTnmeSI3l7ad1vtHU42YEEpgW7pmIh+9pkWNWb5dwS/4onSfAaI0kq+dOZHzz4dWe+8vWnanoSg== dependencies: "@babel/runtime" "^7.3.1" - jss "10.9.0" + jss "10.9.2" jss-plugin-global@^10.5.1: - version "10.9.0" - resolved "https://registry.yarnpkg.com/jss-plugin-global/-/jss-plugin-global-10.9.0.tgz#fc07a0086ac97aca174e37edb480b69277f3931f" - integrity sha512-4G8PHNJ0x6nwAFsEzcuVDiBlyMsj2y3VjmFAx/uHk/R/gzJV+yRHICjT4MKGGu1cJq2hfowFWCyrr/Gg37FbgQ== + version "10.9.2" + resolved "https://registry.yarnpkg.com/jss-plugin-global/-/jss-plugin-global-10.9.2.tgz#e7f2ad4a5e8e674fb703b04b57a570b8c3e5c2c2" + integrity sha512-GcX0aE8Ef6AtlasVrafg1DItlL/tWHoC4cGir4r3gegbWwF5ZOBYhx04gurPvWHC8F873aEGqge7C17xpwmp2g== dependencies: "@babel/runtime" "^7.3.1" - jss "10.9.0" + jss "10.9.2" jss-plugin-nested@^10.5.1: - version "10.9.0" - resolved "https://registry.yarnpkg.com/jss-plugin-nested/-/jss-plugin-nested-10.9.0.tgz#cc1c7d63ad542c3ccc6e2c66c8328c6b6b00f4b3" - integrity sha512-2UJnDrfCZpMYcpPYR16oZB7VAC6b/1QLsRiAutOt7wJaaqwCBvNsosLEu/fUyKNQNGdvg2PPJFDO5AX7dwxtoA== + version "10.9.2" + resolved "https://registry.yarnpkg.com/jss-plugin-nested/-/jss-plugin-nested-10.9.2.tgz#3aa2502816089ecf3981e1a07c49b276d67dca63" + integrity sha512-VgiOWIC6bvgDaAL97XCxGD0BxOKM0K0zeB/ECyNaVF6FqvdGB9KBBWRdy2STYAss4VVA7i5TbxFZN+WSX1kfQA== dependencies: "@babel/runtime" "^7.3.1" - jss "10.9.0" + jss "10.9.2" tiny-warning "^1.0.2" jss-plugin-props-sort@^10.5.1: - version "10.9.0" - resolved "https://registry.yarnpkg.com/jss-plugin-props-sort/-/jss-plugin-props-sort-10.9.0.tgz#30e9567ef9479043feb6e5e59db09b4de687c47d" - integrity sha512-7A76HI8bzwqrsMOJTWKx/uD5v+U8piLnp5bvru7g/3ZEQOu1+PjHvv7bFdNO3DwNPC9oM0a//KwIJsIcDCjDzw== + version "10.9.2" + resolved "https://registry.yarnpkg.com/jss-plugin-props-sort/-/jss-plugin-props-sort-10.9.2.tgz#645f6c8f179309667b3e6212f66b59a32fb3f01f" + integrity sha512-AP1AyUTbi2szylgr+O0OB7gkIxEGzySLITZ2GpsaoX72YMCGI2jYAc+WUhPfvUnZYiauF4zTnN4V4TGuvFjJlw== dependencies: "@babel/runtime" "^7.3.1" - jss "10.9.0" + jss "10.9.2" jss-plugin-rule-value-function@^10.5.1: - version "10.9.0" - resolved "https://registry.yarnpkg.com/jss-plugin-rule-value-function/-/jss-plugin-rule-value-function-10.9.0.tgz#379fd2732c0746fe45168011fe25544c1a295d67" - integrity sha512-IHJv6YrEf8pRzkY207cPmdbBstBaE+z8pazhPShfz0tZSDtRdQua5jjg6NMz3IbTasVx9FdnmptxPqSWL5tyJg== + version "10.9.2" + resolved "https://registry.yarnpkg.com/jss-plugin-rule-value-function/-/jss-plugin-rule-value-function-10.9.2.tgz#9afe07596e477123cbf11120776be6a64494541f" + integrity sha512-vf5ms8zvLFMub6swbNxvzsurHfUZ5Shy5aJB2gIpY6WNA3uLinEcxYyraQXItRHi5ivXGqYciFDRM2ZoVoRZ4Q== dependencies: "@babel/runtime" "^7.3.1" - jss "10.9.0" + jss "10.9.2" tiny-warning "^1.0.2" jss-plugin-vendor-prefixer@^10.5.1: - version "10.9.0" - resolved "https://registry.yarnpkg.com/jss-plugin-vendor-prefixer/-/jss-plugin-vendor-prefixer-10.9.0.tgz#aa9df98abfb3f75f7ed59a3ec50a5452461a206a" - integrity sha512-MbvsaXP7iiVdYVSEoi+blrW+AYnTDvHTW6I6zqi7JcwXdc6I9Kbm234nEblayhF38EftoenbM+5218pidmC5gA== + version "10.9.2" + resolved "https://registry.yarnpkg.com/jss-plugin-vendor-prefixer/-/jss-plugin-vendor-prefixer-10.9.2.tgz#410a0f3b9f8dbbfba58f4d329134df4849aa1237" + integrity sha512-SxcEoH+Rttf9fEv6KkiPzLdXRmI6waOTcMkbbEFgdZLDYNIP9UKNHFy6thhbRKqv0XMQZdrEsbDyV464zE/dUA== dependencies: "@babel/runtime" "^7.3.1" css-vendor "^2.0.8" - jss "10.9.0" + jss "10.9.2" -jss@10.9.0, jss@^10.5.1: - version "10.9.0" - resolved "https://registry.yarnpkg.com/jss/-/jss-10.9.0.tgz#7583ee2cdc904a83c872ba695d1baab4b59c141b" - integrity sha512-YpzpreB6kUunQBbrlArlsMpXYyndt9JATbt95tajx0t4MTJJcCJdd4hdNpHmOIDiUJrF/oX5wtVFrS3uofWfGw== +jss@10.9.2, jss@^10.5.1: + version "10.9.2" + resolved "https://registry.yarnpkg.com/jss/-/jss-10.9.2.tgz#9379be1f195ef98011dfd31f9448251bd61b95a9" + integrity sha512-b8G6rWpYLR4teTUbGd4I4EsnWjg7MN0Q5bSsjKhVkJVjhQDy2KzkbD2AW3TuT0RYZVmZZHKIrXDn6kjU14qkUg== dependencies: "@babel/runtime" "^7.3.1" csstype "^3.0.2" @@ -6611,7 +6611,7 @@ react-redux@^7.2.6: "react-structural-metadata-editor@https://github.com/avalonmediasystem/react-structural-metadata-editor": version "1.1.0" - resolved "https://github.com/avalonmediasystem/react-structural-metadata-editor#f44d879e5537117297dc670c6685fbffc407b482" + resolved "https://github.com/avalonmediasystem/react-structural-metadata-editor#649f6e415a414c04e231e9f400bf268915209956" dependencies: "@babel/runtime" "^7.4.4" "@fortawesome/fontawesome-svg-core" "^1.2.4" @@ -6645,7 +6645,17 @@ react-transition-group@2.5.3: prop-types "^15.6.2" react-lifecycles-compat "^3.0.4" -react-transition-group@^4.4.0, react-transition-group@^4.4.1: +react-transition-group@^4.4.0: + version "4.4.5" + resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.5.tgz#e53d4e3f3344da8521489fbef8f2581d42becdd1" + integrity sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g== + dependencies: + "@babel/runtime" "^7.5.5" + dom-helpers "^5.0.1" + loose-envify "^1.4.0" + prop-types "^15.6.2" + +react-transition-group@^4.4.1: version "4.4.2" resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.2.tgz#8b59a56f09ced7b55cbd53c36768b922890d5470" integrity sha512-/RNYfRAMlZwDSr6z4zNKV6xu53/e2BuaBbGhbyYIXTrmgu/bGHzmqOs7mJSJBHy9Ud+ApHx3QjrkKSp1pxvlFg== From fd69e41f6af2d6b3c8acef3b081cf8b5a1c54d8b Mon Sep 17 00:00:00 2001 From: Chris Colvard Date: Mon, 22 Aug 2022 13:08:45 -0400 Subject: [PATCH 100/230] Upgrade to ActiveEncode 1.x Increase the raw_object column since ActiveEncode captures more ffmpeg error output now Add ApplicationJob because ActiveEncode depends on it --- Gemfile | 2 +- Gemfile.lock | 16 ++++++++-------- app/jobs/application_job.rb | 2 ++ ...e_encode_records_raw_object_to_medium_text.rb | 5 +++++ db/schema.rb | 4 ++-- 5 files changed, 18 insertions(+), 11 deletions(-) create mode 100644 app/jobs/application_job.rb create mode 100644 db/migrate/20220822170237_change_active_encode_encode_records_raw_object_to_medium_text.rb diff --git a/Gemfile b/Gemfile index 8cbbe785ca..b6e44ad24f 100644 --- a/Gemfile +++ b/Gemfile @@ -70,7 +70,7 @@ gem 'omniauth-lti', git: "https://github.com/avalonmediasystem/omniauth-lti.git" gem "omniauth-saml", "~> 2.0" # Media Access & Transcoding -gem 'active_encode', '~> 0.8.2' +gem 'active_encode', '~> 1.0' gem 'audio_waveform-ruby', '~> 1.0.7', require: 'audio_waveform' gem 'browse-everything', git: "https://github.com/avalonmediasystem/browse-everything.git", branch: 'v1.2-avalon' gem 'fastimage' diff --git a/Gemfile.lock b/Gemfile.lock index 485c820eba..77d554bc32 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -132,9 +132,9 @@ GEM active_elastic_job (3.2.0) aws-sdk-sqs (~> 1) rails (>= 5.2.6, < 7.1) - active_encode (0.8.2) + active_encode (1.0.0) + addressable (~> 2.8) rails - sprockets (< 4) active_fedora-datastreams (0.4.0) active-fedora (>= 11.0.0.pre, < 14) activemodel (< 6.1) @@ -398,7 +398,7 @@ GEM mail (~> 2.7) equivalent-xml (0.6.0) nokogiri (>= 1.4.3) - erubi (1.10.0) + erubi (1.11.0) et-orbi (1.2.6) tzinfo ethon (0.15.0) @@ -483,7 +483,7 @@ GEM hydra-access-controls (= 12.0.1) hydra-core (= 12.0.1) rails (>= 5.2, < 7) - i18n (1.11.0) + i18n (1.12.0) concurrent-ruby (~> 1.0) iconv (1.0.8) iiif_manifest (0.6.0) @@ -566,7 +566,7 @@ GEM mime-types-data (3.2022.0105) mini_mime (1.1.2) mini_portile2 (2.8.0) - minitest (5.16.2) + minitest (5.16.3) msgpack (1.4.5) multi_json (1.15.0) multi_xml (0.6.0) @@ -582,7 +582,7 @@ GEM noid-rails (3.0.3) actionpack (>= 5.0.0, < 7) noid (~> 0.9) - nokogiri (1.13.7) + nokogiri (1.13.8) mini_portile2 (~> 2.8.0) racc (~> 1.4) nom-xml (1.2.0) @@ -898,7 +898,7 @@ GEM railties (>= 3.1) typhoeus (1.4.0) ethon (>= 0.9.0) - tzinfo (1.2.9) + tzinfo (1.2.10) thread_safe (~> 0.1) uber (0.0.15) uglifier (4.2.0) @@ -958,7 +958,7 @@ DEPENDENCIES active-fedora (~> 13.2, >= 13.2.5) active_annotations (~> 0.4) active_elastic_job - active_encode (~> 0.8.2) + active_encode (~> 1.0) active_fedora-datastreams (~> 0.4) activejob-traffic_control activejob-uniqueness diff --git a/app/jobs/application_job.rb b/app/jobs/application_job.rb new file mode 100644 index 0000000000..a009ace51c --- /dev/null +++ b/app/jobs/application_job.rb @@ -0,0 +1,2 @@ +class ApplicationJob < ActiveJob::Base +end diff --git a/db/migrate/20220822170237_change_active_encode_encode_records_raw_object_to_medium_text.rb b/db/migrate/20220822170237_change_active_encode_encode_records_raw_object_to_medium_text.rb new file mode 100644 index 0000000000..939a30b946 --- /dev/null +++ b/db/migrate/20220822170237_change_active_encode_encode_records_raw_object_to_medium_text.rb @@ -0,0 +1,5 @@ +class ChangeActiveEncodeEncodeRecordsRawObjectToMediumText < ActiveRecord::Migration[6.0] + def change + change_column :active_encode_encode_records, :raw_object, :text, limit: 16777215 + end +end diff --git a/db/schema.rb b/db/schema.rb index 3aec8f700c..cae4d0b097 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,14 +10,14 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2022_05_26_185425) do +ActiveRecord::Schema.define(version: 2022_08_22_170237) do create_table "active_encode_encode_records", force: :cascade do |t| t.string "global_id" t.string "state" t.string "adapter" t.string "title" - t.text "raw_object" + t.text "raw_object", limit: 16777215 t.datetime "created_at", null: false t.datetime "updated_at", null: false t.text "create_options" From 65c7ab21c303140cccc1e4b6acd6306ce7ab317e Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Wed, 17 Aug 2022 09:33:14 -0400 Subject: [PATCH 101/230] Checkout Stream token WIP --- app/services/security_service.rb | 63 +++++++++++++------ spec/services/security_service_spec.rb | 86 +++++++++++++++++++++----- 2 files changed, 113 insertions(+), 36 deletions(-) diff --git a/app/services/security_service.rb b/app/services/security_service.rb index 290bc74a79..02b9ac18e9 100644 --- a/app/services/security_service.rb +++ b/app/services/security_service.rb @@ -1,11 +1,11 @@ # Copyright 2011-2022, The Trustees of Indiana University and Northwestern # University. Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. -# +# # You may obtain a copy of the License at -# +# # http://www.apache.org/licenses/LICENSE-2.0 -# +# # Unless required by applicable law or agreed to in writing, software distributed # under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR # CONDITIONS OF ANY KIND, either express or implied. See the License for the @@ -27,9 +27,25 @@ def rewrite_url(url, context) url end else - session = context[:session] || { media_token: nil } - token = StreamToken.find_or_create_session_token(session, context[:target]) - "#{url}?token=#{token}" + begin + byebug + if Avalon::Configuration.controlled_digital_lending_enabled? && Checkout.where("media_object_id = ? AND user_id = ? AND return_time > ?", MasterFile.find(context[:target]).media_object_id, context[:session].fetch(:"warden.user.user.key")[0][0], DateTime.current).empty? + raise StreamToken::Unauthorized, "Unauthorized" + else + session = context[:session] || { media_token: nil } + token = StreamToken.find_or_create_session_token(session, context[:target]) + "#{url}?token=#{token}" + end + # rescue NoMethodError + # if context + # session = context[:session] || { media_token: nil } + # token = StreamToken.find_or_create_session_token(session, context[:target]) + # "#{url}?token=#{token}" + # else + # raise StreamToken::Unauthorized, "Unauthorized" + # end + rescue StreamToken::Unauthorized + end end end @@ -37,20 +53,27 @@ def create_cookies(context) result = {} case Settings.streaming.server.to_sym when :aws - configure_signer - domain = Addressable::URI.parse(Settings.streaming.http_base).host - cookie_domain = (context[:request_host].split(/\./) & domain.split(/\./)).join('.') - resource = "http*://#{domain}/#{context[:target]}/*" - Rails.logger.info "Creating signed policy for resource #{resource}" - expiration = Settings.streaming.stream_token_ttl.minutes.from_now - params = Aws::CF::Signer.signed_params(resource, expires: expiration, resource: resource) - params.each_pair do |param,value| - result["CloudFront-#{param}"] = { - value: value, - path: "/#{context[:target]}", - domain: cookie_domain, - expires: expiration - } + begin + if Avalon::Configuration.controlled_digital_lending_enabled? && Checkout.where("media_object_id = ? AND user_id = ? AND return_time > ?", MasterFile.find(context[:target]).media_object_id, context[:session].fetch(:"warden.user.user.key")[0][0], DateTime.current).empty? + raise StreamToken::Unauthorized, "Unauthorized" + else + configure_signer + domain = Addressable::URI.parse(Settings.streaming.http_base).host + cookie_domain = (context[:request_host].split(/\./) & domain.split(/\./)).join('.') + resource = "http*://#{domain}/#{context[:target]}/*" + Rails.logger.info "Creating signed policy for resource #{resource}" + expiration = Settings.streaming.stream_token_ttl.minutes.from_now + params = Aws::CF::Signer.signed_params(resource, expires: expiration, resource: resource) + params.each_pair do |param,value| + result["CloudFront-#{param}"] = { + value: value, + path: "/#{context[:target]}", + domain: cookie_domain, + expires: expiration + } + end + end + rescue StreamToken::Unauthorized end end result diff --git a/spec/services/security_service_spec.rb b/spec/services/security_service_spec.rb index 2a9544bbaa..c92d31f0d9 100644 --- a/spec/services/security_service_spec.rb +++ b/spec/services/security_service_spec.rb @@ -1,11 +1,11 @@ # Copyright 2011-2022, The Trustees of Indiana University and Northwestern # University. Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. -# +# # You may obtain a copy of the License at -# +# # http://www.apache.org/licenses/LICENSE-2.0 -# +# # Unless required by applicable law or agreed to in writing, software distributed # under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR # CONDITIONS OF ANY KIND, either express or implied. See the License for the @@ -40,6 +40,7 @@ describe '.rewrite_url' do let(:url) { "http://example.com/streaming/id" } let(:context) {{ session: {}, target: 'abcd1234', protocol: :stream_hls }} + let(:user) { FactoryBot.create(:user) } context 'when AWS streaming server' do before do @@ -64,19 +65,48 @@ end end - context 'when non-AMS streaming server' do + context 'when non-AWS streaming server' do + let(:context) {{ session: { "warden.user.user.key": [[user.id], "token"] }, target: 'abcd1234', protocol: :stream_hls }} before do allow(Settings.streaming).to receive(:server).and_return("wowza") + FactoryBot.create(:master_file, id: context[:target], media_object_id: context[:target]) end - - it 'adds a StreamToken param' do - expect(subject.rewrite_url(url, context)).to start_with "http://example.com/streaming/id?token=" + context "controlled digital lending is disabled" do + it 'adds a StreamToken param' do + allow(Settings.controlled_digital_lending).to receive(:enable).and_return(false) + expect(subject.rewrite_url(url, context)).to start_with "http://example.com/streaming/id?token=" + end + end + context "controlled digital lending is enabled" do + before { allow(Settings.controlled_digital_lending).to receive(:enable).and_return(true) } + context "the user has the item checked out" do + before { FactoryBot.create(:checkout, user_id: user.id, media_object_id: context[:target]) } + it 'adds a StreamToken param' do + expect(subject.rewrite_url(url, context)).to start_with "http://example.com/streaming/id?token=" + end + end + context "the user does not have the item checked out" do + it 'does not add a StreamToken param' do + expect(subject.rewrite_url(url, context)).not_to start_with "http://example.com/streaming/id?token=" + end + it 'properly rescues StreamToken::Unauthorized' do + expect { subject.rewrite_url(url, context) }.not_to raise_error + end + end + context 'no user, no waveform job' do + let(:context) {{ target: 'abcd1234', protocol: :stream_hls }} + it 'does not add a StreamToken param' do + expect(subject.rewrite_url(url, context)).not_to start_with "http://example.com/streaming/id?token=" + expect { subject.rewrite_url(url, context) }.to raise_error StreamToken::Unauthorized + end + end end end end describe '.create_cookies' do - let(:context) {{ target: 'abcd1234', request_host: 'localhost' }} + let(:context) {{ session: { "user_return_to": "id", "warden.user.user.key": [[user.id], "token"] }, target: 'abcd1234', request_host: 'localhost' }} + let(:user) { FactoryBot.create(:user)} context 'when AWS streaming server' do before do @@ -86,19 +116,43 @@ allow(Settings.streaming).to receive(:signing_key_id).and_return("signing_key_id") allow(Settings.streaming).to receive(:stream_token_ttl).and_return(20) allow(Settings.streaming).to receive(:http_base).and_return("http://localhost:3000/streams") + FactoryBot.create(:master_file, id: context[:target], media_object_id: context[:target]) end - it 'returns a hash of cookies' do - cookies = subject.create_cookies(context) - expect(cookies.first[0]).to eq "CloudFront-Policy" - expect(cookies.first[1][:path]).to eq "/#{context[:target]}" - expect(cookies.first[1][:value]).to be_present - expect(cookies.first[1][:domain]).to be_present - expect(cookies.first[1][:expires]).to be_present + context 'controlled digital lending is disabled' do + it 'returns a hash of cookies' do + allow(Settings.controlled_digital_lending).to receive(:enable).and_return(false) + cookies = subject.create_cookies(context) + expect(cookies.first[0]).to eq "CloudFront-Policy" + expect(cookies.first[1][:path]).to eq "/#{context[:target]}" + expect(cookies.first[1][:value]).to be_present + expect(cookies.first[1][:domain]).to be_present + expect(cookies.first[1][:expires]).to be_present + end + end + + context 'controlled digital lending is enabled' do + before { allow(Settings.controlled_digital_lending).to receive(:enable).and_return(true) } + context 'the active user has the item checked out' do + before { FactoryBot.create(:checkout, user_id: user.id, media_object_id: context[:target]) } + it 'returns a hash of cookies' do + cookies = subject.create_cookies(context) + expect(cookies.first[0]).to eq "CloudFront-Policy" + expect(cookies.first[1][:path]).to eq "/#{context[:target]}" + expect(cookies.first[1][:value]).to be_present + expect(cookies.first[1][:domain]).to be_present + expect(cookies.first[1][:expires]).to be_present + end + end + context 'the active user does not have the item checked out' do + it 'does nothing' do + expect(subject.create_cookies(context)).to eq({}) + end + end end end - context 'when non-AMS streaming server' do + context 'when non-AWS streaming server' do before do allow(Settings.streaming).to receive(:server).and_return("wowza") end From 27810e19b4f3415a99c0a2a20f8e7abb4471082c Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Thu, 18 Aug 2022 16:31:09 -0400 Subject: [PATCH 102/230] Add CDL check to Stream Token assignment --- app/controllers/master_files_controller.rb | 12 +- app/controllers/media_objects_controller.rb | 14 +- app/helpers/security_helper.rb | 27 +++- app/models/checkout.rb | 1 + app/services/security_service.rb | 59 +++------ app/views/playlists/_player.html.erb | 2 +- .../master_files_controller_spec.rb | 14 +- spec/helpers/security_helper_spec.rb | 121 ++++++++++++++---- spec/models/checkout_spec.rb | 24 ++++ spec/services/security_service_spec.rb | 78 ++--------- 10 files changed, 198 insertions(+), 154 deletions(-) diff --git a/app/controllers/master_files_controller.rb b/app/controllers/master_files_controller.rb index 8357828d32..98854b85c4 100644 --- a/app/controllers/master_files_controller.rb +++ b/app/controllers/master_files_controller.rb @@ -1,11 +1,11 @@ # Copyright 2011-2022, The Trustees of Indiana University and Northwestern # University. Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. -# +# # You may obtain a copy of the License at -# +# # http://www.apache.org/licenses/LICENSE-2.0 -# +# # Unless required by applicable law or agreed to in writing, software distributed # under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR # CONDITIONS OF ANY KIND, either express or implied. See the License for the @@ -70,7 +70,7 @@ def show def embed if can? :read, @master_file - @stream_info = secure_streams(@master_file.stream_details) + @stream_info = secure_streams(@master_file.stream_details, @master_file.media_object_id) @stream_info['t'] = view_context.parse_media_fragment(params[:t]) # add MediaFragment from params @stream_info['link_back_url'] = view_context.share_link_for(@master_file) end @@ -411,14 +411,14 @@ def ensure_readable_filedata end def gather_hls_streams(master_file) - stream_info = secure_streams(master_file.stream_details) + stream_info = secure_streams(master_file.stream_details, master_file.media_object_id) hls_streams = stream_info[:stream_hls].reject { |stream| stream[:quality] == 'auto' } hls_streams.each { |stream| unnest_wowza_stream(stream) } if Settings.streaming.server == "wowza" hls_streams end def hls_stream(master_file, quality) - stream_info = secure_streams(master_file.stream_details) + stream_info = secure_streams(master_file.stream_details, master_file.media_object_id) hls_stream = stream_info[:stream_hls].select { |stream| stream[:quality] == quality } unnest_wowza_stream(hls_stream&.first) if Settings.streaming.server == "wowza" hls_stream diff --git a/app/controllers/media_objects_controller.rb b/app/controllers/media_objects_controller.rb index 313f08853d..db70860984 100644 --- a/app/controllers/media_objects_controller.rb +++ b/app/controllers/media_objects_controller.rb @@ -1,11 +1,11 @@ # Copyright 2011-2022, The Trustees of Indiana University and Northwestern # University. Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. -# +# # You may obtain a copy of the License at -# +# # http://www.apache.org/licenses/LICENSE-2.0 -# +# # Unless required by applicable law or agreed to in writing, software distributed # under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR # CONDITIONS OF ANY KIND, either express or implied. See the License for the @@ -468,7 +468,7 @@ def manifest master_files = master_file_presenters canvas_presenters = master_files.collect do |mf| - stream_info = secure_streams(mf.stream_details) + stream_info = secure_streams(mf.stream_details, @media_object.id) IiifCanvasPresenter.new(master_file: mf, stream_info: stream_info) end presenter = IiifManifestPresenter.new(media_object: @media_object, master_files: canvas_presenters) @@ -546,7 +546,11 @@ def set_player_token def load_current_stream set_active_file set_player_token - @currentStreamInfo = @currentStream.nil? ? {} : secure_streams(@currentStream.stream_details) + if params[:id] + @currentStreamInfo = @currentStream.nil? ? {} : secure_streams(@currentStream.stream_details, params[:id]) + else + @currentStreamInfo = @currentStream.nil? ? {} : secure_streams(@currentStream.stream_details, @media_object.id) + end @currentStreamInfo['t'] = view_context.parse_media_fragment(params[:t]) # add MediaFragment from params @currentStreamInfo['lti_share_link'] = view_context.lti_share_url_for(@currentStream) @currentStreamInfo['link_back_url'] = view_context.share_link_for(@currentStream) diff --git a/app/helpers/security_helper.rb b/app/helpers/security_helper.rb index 50bbcd110c..7bcd7f667e 100644 --- a/app/helpers/security_helper.rb +++ b/app/helpers/security_helper.rb @@ -1,11 +1,11 @@ # Copyright 2011-2022, The Trustees of Indiana University and Northwestern # University. Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. -# +# # You may obtain a copy of the License at -# +# # http://www.apache.org/licenses/LICENSE-2.0 -# +# # Unless required by applicable law or agreed to in writing, software distributed # under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR # CONDITIONS OF ANY KIND, either express or implied. See the License for the @@ -19,13 +19,30 @@ def add_stream_cookies(stream_info) end end - def secure_streams(stream_info) + def secure_streams(stream_info, media_object_id) + begin + unless MediaObject.find(media_object_id).visibility == 'public' + if Avalon::Configuration.controlled_digital_lending_enabled? && Checkout.checked_out_to_user(media_object_id, current_user.id).empty? + raise StreamToken::Unauthorized + else + add_stream_url(stream_info) + end + else + add_stream_url(stream_info) + end + rescue Ldp::BadRequest + add_stream_url(stream_info) + rescue StreamToken::Unauthorized + end + stream_info + end + + def add_stream_url(stream_info) add_stream_cookies(id: stream_info[:id]) [:stream_hls].each do |protocol| stream_info[protocol].each do |quality| quality[:url] = SecurityHandler.secure_url(quality[:url], session: session, target: stream_info[:id], protocol: protocol) end end - stream_info end end diff --git a/app/models/checkout.rb b/app/models/checkout.rb index 234a41a0a4..790b8f7ee0 100644 --- a/app/models/checkout.rb +++ b/app/models/checkout.rb @@ -8,6 +8,7 @@ class Checkout < ApplicationRecord scope :active_for_media_object, ->(media_object_id) { where(media_object_id: media_object_id).where("return_time > now()") } scope :active_for_user, ->(user_id) { where(user_id: user_id).where("return_time > now()") } scope :returned_for_user, ->(user_id) { where(user_id: user_id).where("return_time < now()") } + scope :checked_out_to_user, ->(media_object_id, user_id) { where("media_object_id = ? AND user_id = ? AND return_time > now()", media_object_id, user_id)} def media_object MediaObject.find(media_object_id) diff --git a/app/services/security_service.rb b/app/services/security_service.rb index 02b9ac18e9..9a4339c2ed 100644 --- a/app/services/security_service.rb +++ b/app/services/security_service.rb @@ -27,25 +27,9 @@ def rewrite_url(url, context) url end else - begin - byebug - if Avalon::Configuration.controlled_digital_lending_enabled? && Checkout.where("media_object_id = ? AND user_id = ? AND return_time > ?", MasterFile.find(context[:target]).media_object_id, context[:session].fetch(:"warden.user.user.key")[0][0], DateTime.current).empty? - raise StreamToken::Unauthorized, "Unauthorized" - else - session = context[:session] || { media_token: nil } - token = StreamToken.find_or_create_session_token(session, context[:target]) - "#{url}?token=#{token}" - end - # rescue NoMethodError - # if context - # session = context[:session] || { media_token: nil } - # token = StreamToken.find_or_create_session_token(session, context[:target]) - # "#{url}?token=#{token}" - # else - # raise StreamToken::Unauthorized, "Unauthorized" - # end - rescue StreamToken::Unauthorized - end + session = context[:session] || { media_token: nil } + token = StreamToken.find_or_create_session_token(session, context[:target]) + "#{url}?token=#{token}" end end @@ -53,28 +37,21 @@ def create_cookies(context) result = {} case Settings.streaming.server.to_sym when :aws - begin - if Avalon::Configuration.controlled_digital_lending_enabled? && Checkout.where("media_object_id = ? AND user_id = ? AND return_time > ?", MasterFile.find(context[:target]).media_object_id, context[:session].fetch(:"warden.user.user.key")[0][0], DateTime.current).empty? - raise StreamToken::Unauthorized, "Unauthorized" - else - configure_signer - domain = Addressable::URI.parse(Settings.streaming.http_base).host - cookie_domain = (context[:request_host].split(/\./) & domain.split(/\./)).join('.') - resource = "http*://#{domain}/#{context[:target]}/*" - Rails.logger.info "Creating signed policy for resource #{resource}" - expiration = Settings.streaming.stream_token_ttl.minutes.from_now - params = Aws::CF::Signer.signed_params(resource, expires: expiration, resource: resource) - params.each_pair do |param,value| - result["CloudFront-#{param}"] = { - value: value, - path: "/#{context[:target]}", - domain: cookie_domain, - expires: expiration - } - end - end - rescue StreamToken::Unauthorized - end + configure_signer + domain = Addressable::URI.parse(Settings.streaming.http_base).host + cookie_domain = (context[:request_host].split(/\./) & domain.split(/\./)).join('.') + resource = "http*://#{domain}/#{context[:target]}/*" + Rails.logger.info "Creating signed policy for resource #{resource}" + expiration = Settings.streaming.stream_token_ttl.minutes.from_now + params = Aws::CF::Signer.signed_params(resource, expires: expiration, resource: resource) + params.each_pair do |param,value| + result["CloudFront-#{param}"] = { + value: value, + path: "/#{context[:target]}", + domain: cookie_domain, + expires: expiration + } + end end result end diff --git a/app/views/playlists/_player.html.erb b/app/views/playlists/_player.html.erb index c07c39da37..6dbba4ee63 100644 --- a/app/views/playlists/_player.html.erb +++ b/app/views/playlists/_player.html.erb @@ -17,7 +17,7 @@ Unless required by applicable law or agreed to in writing, software distributed <% f_start = @current_clip.start_time / 1000.0 %> <% f_end = @current_clip.end_time / 1000.0 %> <% @currentStream = @current_masterfile %> -<% @currentStreamInfo = secure_streams(@currentStream.stream_details) %> +<% @currentStreamInfo = secure_streams(@currentStream.stream_details, @current_masterfile.media_object_id) %> <% @currentStreamInfo['t'] = [f_start,f_end] %> <% if can? :read, @current_masterfile %> diff --git a/spec/controllers/master_files_controller_spec.rb b/spec/controllers/master_files_controller_spec.rb index 781034099f..b6d3146802 100644 --- a/spec/controllers/master_files_controller_spec.rb +++ b/spec/controllers/master_files_controller_spec.rb @@ -1,11 +1,11 @@ # Copyright 2011-2022, The Trustees of Indiana University and Northwestern # University. Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. -# +# # You may obtain a copy of the License at -# +# # http://www.apache.org/licenses/LICENSE-2.0 -# +# # Unless required by applicable law or agreed to in writing, software distributed # under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR # CONDITIONS OF ANY KIND, either express or implied. See the License for the @@ -664,10 +664,10 @@ class << file describe "#update" do let(:master_file) { FactoryBot.create(:master_file, :with_media_object) } - subject { put('update', params: { id: master_file.id, - master_file: { title: "New label", - poster_offset: "00:00:03", - date_digitized: "2020-08-27", + subject { put('update', params: { id: master_file.id, + master_file: { title: "New label", + poster_offset: "00:00:03", + date_digitized: "2020-08-27", permalink: "https://perma.link" }})} before do diff --git a/spec/helpers/security_helper_spec.rb b/spec/helpers/security_helper_spec.rb index 41e81461a0..c9f3192ce5 100644 --- a/spec/helpers/security_helper_spec.rb +++ b/spec/helpers/security_helper_spec.rb @@ -1,11 +1,11 @@ # Copyright 2011-2022, The Trustees of Indiana University and Northwestern # University. Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. -# +# # You may obtain a copy of the License at -# +# # http://www.apache.org/licenses/LICENSE-2.0 -# +# # Unless required by applicable law or agreed to in writing, software distributed # under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR # CONDITIONS OF ANY KIND, either express or implied. See the License for the @@ -19,9 +19,12 @@ {:id=>"bk1289888", :label=>nil, :is_video=>true, :poster_image=>nil, :embed_code=>"", :stream_hls=>[{:quality=>nil, :mimetype=>nil, :format=>"other", :url=>"http://localhost:3000/6f69c008-06a4-4bad-bb60-26297f0b4c06/35bddaa0-fbb4-404f-ab76-58f22921529c/warning.mp4.m3u8"}], :captions_path=>nil, :captions_format=>nil, :duration=>200.0, :embed_title=>"Fugit veniam numquam harum et adipisci est est. - video.mp4"} } let(:secure_url) { "http://example.com/secure/id" } + let(:media_object) { FactoryBot.create(:media_object) } + let(:user) { FactoryBot.create(:user) } before do allow(SecurityHandler).to receive(:secure_url).and_return(secure_url) + allow(helper).to receive(:current_user).and_return(user) end context 'when AWS streaming server' do @@ -38,25 +41,62 @@ describe '#add_stream_cookies' do it 'adds security tokens to cookies' do - expect { helper.add_stream_cookies(stream_info) }.to change { controller.cookies.sum {|k,v| 1} }.by(1) - expect(controller.cookies[secure_cookies.first[0]]).to eq secure_cookies.first[1][:value] + expect { helper.add_stream_cookies(stream_info) }.to change { controller.cookies.sum {|k,v| 1} }.by(1) + expect(controller.cookies[secure_cookies.first[0]]).to eq secure_cookies.first[1][:value] end end describe '#secure_streams' do - it 'sets secure cookies' do - expect { helper.secure_streams(stream_info) }.to change { controller.cookies.sum {|k,v| 1} }.by(1) - expect(controller.cookies[secure_cookies.first[0]]).to eq secure_cookies.first[1][:value] + context 'controlled digital lending is disabled' do + before { allow(Settings.controlled_digital_lending).to receive(:enable).and_return(false) } + it 'sets secure cookies' do + expect { helper.secure_streams(stream_info, media_object.id) }.to change { controller.cookies.sum {|k,v| 1} }.by(1) + expect(controller.cookies[secure_cookies.first[0]]).to eq secure_cookies.first[1][:value] + end + + it 'rewrites urls in the stream_info' do + expect { helper.secure_streams(stream_info, media_object.id) }.to change { stream_info.slice(:stream_flash, :stream_hls).values.flatten.collect {|v| v[:url]} } + [:stream_hls].each do |protocol| + stream_info[protocol].each do |quality| + expect(quality[:url]).to eq secure_url + end + end + end end + context 'controlled digital lending is enabled' do + before { allow(Settings.controlled_digital_lending).to receive(:enable).and_return(true) } + context 'the user has the item checked out' do + before { FactoryBot.create(:checkout, media_object_id: media_object.id, user_id: user.id)} + it 'sets secure cookies' do + expect { helper.secure_streams(stream_info, media_object.id) }.to change { controller.cookies.sum {|k,v| 1} }.by(1) + expect(controller.cookies[secure_cookies.first[0]]).to eq secure_cookies.first[1][:value] + end - it 'rewrites urls in the stream_info' do - expect { helper.secure_streams(stream_info) }.to change { stream_info.slice(:stream_flash, :stream_hls).values.flatten.collect {|v| v[:url]} } - [:stream_hls].each do |protocol| - stream_info[protocol].each do |quality| - expect(quality[:url]).to eq secure_url - end - end - end + it 'rewrites urls in the stream_info' do + expect { helper.secure_streams(stream_info, media_object.id) }.to change { stream_info.slice(:stream_flash, :stream_hls).values.flatten.collect {|v| v[:url]} } + [:stream_hls].each do |protocol| + stream_info[protocol].each do |quality| + expect(quality[:url]).to eq secure_url + end + end + end + end + context 'the user does not have the item checked out' do + it 'does not set secure cookies' do + expect { helper.secure_streams(stream_info, media_object.id) }.not_to change { controller.cookies.sum {|k,v| 1} } + expect(controller.cookies[secure_cookies.first[0]]).not_to eq secure_cookies.first[1][:value] + end + + it 'does not rewrite urls in the stream_info' do + expect { helper.secure_streams(stream_info, media_object.id) }.not_to change { stream_info.slice(:stream_flash, :stream_hls).values.flatten.collect {|v| v[:url]} } + [:stream_hls].each do |protocol| + stream_info[protocol].each do |quality| + expect(quality[:url]).not_to eq secure_url + end + end + end + end + end end end @@ -68,15 +108,50 @@ end describe '#secure_streams' do - it 'sets secure cookies' do - expect { helper.secure_streams(stream_info) }.not_to change { controller.cookies.sum {|k,v| 1} } + context 'controlled digital lending is disabled' do + before { allow(Settings.controlled_digital_lending).to receive(:enable).and_return(false) } + it 'sets secure cookies' do + expect { helper.secure_streams(stream_info, media_object.id) }.not_to change { controller.cookies.sum {|k,v| 1} } + end + + it 'rewrites urls in the stream_info' do + expect { helper.secure_streams(stream_info, media_object.id) }.to change { stream_info.slice(:stream_flash, :stream_hls).values.flatten.collect {|v| v[:url]} } + [:stream_hls].each do |protocol| + stream_info[protocol].each do |quality| + expect(quality[:url]).to eq secure_url + end + end + end end + context 'controlled digital lending is enabled' do + before { allow(Settings.controlled_digital_lending).to receive(:enable).and_return(true) } + context 'the user has the item checked out' do + before { FactoryBot.create(:checkout, media_object_id: media_object.id, user_id: user.id)} + it 'sets secure cookies' do + expect { helper.secure_streams(stream_info, media_object.id) }.not_to change { controller.cookies.sum {|k,v| 1} } + end + + it 'rewrites urls in the stream_info' do + expect { helper.secure_streams(stream_info, media_object.id) }.to change { stream_info.slice(:stream_flash, :stream_hls).values.flatten.collect {|v| v[:url]} } + [:stream_hls].each do |protocol| + stream_info[protocol].each do |quality| + expect(quality[:url]).to eq secure_url + end + end + end + end + context 'the user does not have the item checked out' do + it 'sets secure cookies' do + expect { helper.secure_streams(stream_info, media_object.id) }.not_to change { controller.cookies.sum {|k,v| 1} } + end - it 'rewrites urls in the stream_info' do - expect { helper.secure_streams(stream_info) }.to change { stream_info.slice(:stream_flash, :stream_hls).values.flatten.collect {|v| v[:url]} } - [:stream_hls].each do |protocol| - stream_info[protocol].each do |quality| - expect(quality[:url]).to eq secure_url + it 'does not rewrite urls in the stream_info' do + expect { helper.secure_streams(stream_info, media_object.id) }.not_to change { stream_info.slice(:stream_flash, :stream_hls).values.flatten.collect {|v| v[:url]} } + [:stream_hls].each do |protocol| + stream_info[protocol].each do |quality| + expect(quality[:url]).not_to eq secure_url + end + end end end end diff --git a/spec/models/checkout_spec.rb b/spec/models/checkout_spec.rb index 28c04c500a..01e0a7b07b 100644 --- a/spec/models/checkout_spec.rb +++ b/spec/models/checkout_spec.rb @@ -43,6 +43,8 @@ let(:user) { FactoryBot.create(:user) } let(:media_object) { FactoryBot.create(:media_object) } let!(:checkout) { FactoryBot.create(:checkout, user: user, media_object_id: media_object.id) } + let!(:user_checkout) { FactoryBot.create(:checkout, user: user) } + let!(:other_checkout) { FactoryBot.create(:checkout) } let!(:expired_checkout) { FactoryBot.create(:checkout, user: user, media_object_id: media_object.id) } before do @@ -57,6 +59,10 @@ it 'does not return inactive checkouts' do expect(Checkout.active_for_media_object(media_object.id)).not_to include(expired_checkout) end + + it 'does not return other checkouts' do + expect(Checkout.active_for_media_object(media_object.id)).not_to include(other_checkout) + end end describe 'active_for_user' do @@ -67,6 +73,10 @@ it 'does not return inactive checkouts' do expect(Checkout.active_for_user(user.id)).not_to include(expired_checkout) end + + it 'does not return other checkouts' do + expect(Checkout.active_for_media_object(media_object.id)).not_to include(other_checkout) + end end describe 'returned_for_user' do @@ -77,6 +87,20 @@ it 'does return inactive checkouts' do expect(Checkout.returned_for_user(user.id)).to include(expired_checkout) end + + it 'does not return other checkouts' do + expect(Checkout.active_for_media_object(media_object.id)).not_to include(other_checkout) + end + end + + describe 'checked_out_to_user' do + it 'returns the specified checkout' do + expect(Checkout.checked_out_to_user(media_object.id, user.id)).to include(checkout) + end + + it "does not return the user's other checkouts" do + expect(Checkout.checked_out_to_user(media_object.id, user.id)).not_to include(user_checkout) + end end end diff --git a/spec/services/security_service_spec.rb b/spec/services/security_service_spec.rb index c92d31f0d9..0f73682951 100644 --- a/spec/services/security_service_spec.rb +++ b/spec/services/security_service_spec.rb @@ -40,7 +40,6 @@ describe '.rewrite_url' do let(:url) { "http://example.com/streaming/id" } let(:context) {{ session: {}, target: 'abcd1234', protocol: :stream_hls }} - let(:user) { FactoryBot.create(:user) } context 'when AWS streaming server' do before do @@ -66,47 +65,18 @@ end context 'when non-AWS streaming server' do - let(:context) {{ session: { "warden.user.user.key": [[user.id], "token"] }, target: 'abcd1234', protocol: :stream_hls }} + let(:context) {{ target: 'abcd1234', protocol: :stream_hls }} before do allow(Settings.streaming).to receive(:server).and_return("wowza") - FactoryBot.create(:master_file, id: context[:target], media_object_id: context[:target]) end - context "controlled digital lending is disabled" do - it 'adds a StreamToken param' do - allow(Settings.controlled_digital_lending).to receive(:enable).and_return(false) - expect(subject.rewrite_url(url, context)).to start_with "http://example.com/streaming/id?token=" - end - end - context "controlled digital lending is enabled" do - before { allow(Settings.controlled_digital_lending).to receive(:enable).and_return(true) } - context "the user has the item checked out" do - before { FactoryBot.create(:checkout, user_id: user.id, media_object_id: context[:target]) } - it 'adds a StreamToken param' do - expect(subject.rewrite_url(url, context)).to start_with "http://example.com/streaming/id?token=" - end - end - context "the user does not have the item checked out" do - it 'does not add a StreamToken param' do - expect(subject.rewrite_url(url, context)).not_to start_with "http://example.com/streaming/id?token=" - end - it 'properly rescues StreamToken::Unauthorized' do - expect { subject.rewrite_url(url, context) }.not_to raise_error - end - end - context 'no user, no waveform job' do - let(:context) {{ target: 'abcd1234', protocol: :stream_hls }} - it 'does not add a StreamToken param' do - expect(subject.rewrite_url(url, context)).not_to start_with "http://example.com/streaming/id?token=" - expect { subject.rewrite_url(url, context) }.to raise_error StreamToken::Unauthorized - end - end + it 'adds a StreamToken param' do + expect(subject.rewrite_url(url, context)).to start_with "http://example.com/streaming/id?token=" end end end describe '.create_cookies' do - let(:context) {{ session: { "user_return_to": "id", "warden.user.user.key": [[user.id], "token"] }, target: 'abcd1234', request_host: 'localhost' }} - let(:user) { FactoryBot.create(:user)} + let(:context) {{ target: 'abcd1234', request_host: 'localhost' }} context 'when AWS streaming server' do before do @@ -116,43 +86,19 @@ allow(Settings.streaming).to receive(:signing_key_id).and_return("signing_key_id") allow(Settings.streaming).to receive(:stream_token_ttl).and_return(20) allow(Settings.streaming).to receive(:http_base).and_return("http://localhost:3000/streams") - FactoryBot.create(:master_file, id: context[:target], media_object_id: context[:target]) end - context 'controlled digital lending is disabled' do - it 'returns a hash of cookies' do - allow(Settings.controlled_digital_lending).to receive(:enable).and_return(false) - cookies = subject.create_cookies(context) - expect(cookies.first[0]).to eq "CloudFront-Policy" - expect(cookies.first[1][:path]).to eq "/#{context[:target]}" - expect(cookies.first[1][:value]).to be_present - expect(cookies.first[1][:domain]).to be_present - expect(cookies.first[1][:expires]).to be_present - end - end - - context 'controlled digital lending is enabled' do - before { allow(Settings.controlled_digital_lending).to receive(:enable).and_return(true) } - context 'the active user has the item checked out' do - before { FactoryBot.create(:checkout, user_id: user.id, media_object_id: context[:target]) } - it 'returns a hash of cookies' do - cookies = subject.create_cookies(context) - expect(cookies.first[0]).to eq "CloudFront-Policy" - expect(cookies.first[1][:path]).to eq "/#{context[:target]}" - expect(cookies.first[1][:value]).to be_present - expect(cookies.first[1][:domain]).to be_present - expect(cookies.first[1][:expires]).to be_present - end - end - context 'the active user does not have the item checked out' do - it 'does nothing' do - expect(subject.create_cookies(context)).to eq({}) - end - end + it 'returns a hash of cookies' do + cookies = subject.create_cookies(context) + expect(cookies.first[0]).to eq "CloudFront-Policy" + expect(cookies.first[1][:path]).to eq "/#{context[:target]}" + expect(cookies.first[1][:value]).to be_present + expect(cookies.first[1][:domain]).to be_present + expect(cookies.first[1][:expires]).to be_present end end - context 'when non-AWS streaming server' do + context 'when non-AMS streaming server' do before do allow(Settings.streaming).to receive(:server).and_return("wowza") end From 1db3214e4794c0c6cfe4774c90919be66223d75d Mon Sep 17 00:00:00 2001 From: Mason Ballengee <68433277+masaball@users.noreply.github.com> Date: Thu, 18 Aug 2022 16:40:27 -0400 Subject: [PATCH 103/230] Update security_service_spec.rb Missed a change that I had made when originally implementing the new StreamToken logic in the service level rather than helper level. Reverting to the original spec. --- spec/services/security_service_spec.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/services/security_service_spec.rb b/spec/services/security_service_spec.rb index 0f73682951..e73a83729c 100644 --- a/spec/services/security_service_spec.rb +++ b/spec/services/security_service_spec.rb @@ -65,7 +65,6 @@ end context 'when non-AWS streaming server' do - let(:context) {{ target: 'abcd1234', protocol: :stream_hls }} before do allow(Settings.streaming).to receive(:server).and_return("wowza") end From e0ff077bcc1bf3eda81651a33f16b1e57fe7f6e2 Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Thu, 18 Aug 2022 17:25:20 -0400 Subject: [PATCH 104/230] Fixes for codeclimate --- app/controllers/media_objects_controller.rb | 10 +++++----- app/helpers/security_helper.rb | 8 +++----- app/models/checkout.rb | 2 +- app/services/security_service.rb | 2 +- 4 files changed, 10 insertions(+), 12 deletions(-) diff --git a/app/controllers/media_objects_controller.rb b/app/controllers/media_objects_controller.rb index db70860984..917ccd58de 100644 --- a/app/controllers/media_objects_controller.rb +++ b/app/controllers/media_objects_controller.rb @@ -546,11 +546,11 @@ def set_player_token def load_current_stream set_active_file set_player_token - if params[:id] - @currentStreamInfo = @currentStream.nil? ? {} : secure_streams(@currentStream.stream_details, params[:id]) - else - @currentStreamInfo = @currentStream.nil? ? {} : secure_streams(@currentStream.stream_details, @media_object.id) - end + @currentStreamInfo = if params[:id] + @currentStream.nil? ? {} : secure_streams(@currentStream.stream_details, params[:id]) + else + @currentStream.nil? ? {} : secure_streams(@currentStream.stream_details, @media_object.id) + end @currentStreamInfo['t'] = view_context.parse_media_fragment(params[:t]) # add MediaFragment from params @currentStreamInfo['lti_share_link'] = view_context.lti_share_url_for(@currentStream) @currentStreamInfo['link_back_url'] = view_context.share_link_for(@currentStream) diff --git a/app/helpers/security_helper.rb b/app/helpers/security_helper.rb index 7bcd7f667e..5db5bfd781 100644 --- a/app/helpers/security_helper.rb +++ b/app/helpers/security_helper.rb @@ -21,17 +21,15 @@ def add_stream_cookies(stream_info) def secure_streams(stream_info, media_object_id) begin - unless MediaObject.find(media_object_id).visibility == 'public' + if MediaObject.find(media_object_id).visibility == 'public' + add_stream_url(stream_info) + else if Avalon::Configuration.controlled_digital_lending_enabled? && Checkout.checked_out_to_user(media_object_id, current_user.id).empty? raise StreamToken::Unauthorized else add_stream_url(stream_info) end - else - add_stream_url(stream_info) end - rescue Ldp::BadRequest - add_stream_url(stream_info) rescue StreamToken::Unauthorized end stream_info diff --git a/app/models/checkout.rb b/app/models/checkout.rb index 790b8f7ee0..34bbf08fd0 100644 --- a/app/models/checkout.rb +++ b/app/models/checkout.rb @@ -8,7 +8,7 @@ class Checkout < ApplicationRecord scope :active_for_media_object, ->(media_object_id) { where(media_object_id: media_object_id).where("return_time > now()") } scope :active_for_user, ->(user_id) { where(user_id: user_id).where("return_time > now()") } scope :returned_for_user, ->(user_id) { where(user_id: user_id).where("return_time < now()") } - scope :checked_out_to_user, ->(media_object_id, user_id) { where("media_object_id = ? AND user_id = ? AND return_time > now()", media_object_id, user_id)} + scope :checked_out_to_user, ->(media_object_id, user_id) { where("media_object_id = ? AND user_id = ? AND return_time > now()", media_object_id, user_id) } def media_object MediaObject.find(media_object_id) diff --git a/app/services/security_service.rb b/app/services/security_service.rb index 9a4339c2ed..c512a45118 100644 --- a/app/services/security_service.rb +++ b/app/services/security_service.rb @@ -51,7 +51,7 @@ def create_cookies(context) domain: cookie_domain, expires: expiration } - end + end end result end From 3d37da7eb604cfcd4265982396b9687794bec955 Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Fri, 19 Aug 2022 09:13:59 -0400 Subject: [PATCH 105/230] Re-add error catch for master files I had removed an error catch for some testing and accidentally left it out of the prior commit --- app/helpers/security_helper.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/helpers/security_helper.rb b/app/helpers/security_helper.rb index 5db5bfd781..0edb7e8c07 100644 --- a/app/helpers/security_helper.rb +++ b/app/helpers/security_helper.rb @@ -30,6 +30,8 @@ def secure_streams(stream_info, media_object_id) add_stream_url(stream_info) end end + rescue Ldp::BadRequest + add_stream_url(stream_info) rescue StreamToken::Unauthorized end stream_info From 1f7e43d243f3348d9110f29dec477b2dfb386776 Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Mon, 22 Aug 2022 14:56:12 -0400 Subject: [PATCH 106/230] Refactor errors in security helper --- app/helpers/security_helper.rb | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/app/helpers/security_helper.rb b/app/helpers/security_helper.rb index 0edb7e8c07..0926bc2670 100644 --- a/app/helpers/security_helper.rb +++ b/app/helpers/security_helper.rb @@ -21,17 +21,12 @@ def add_stream_cookies(stream_info) def secure_streams(stream_info, media_object_id) begin - if MediaObject.find(media_object_id).visibility == 'public' - add_stream_url(stream_info) + if Avalon::Configuration.controlled_digital_lending_enabled? && Checkout.checked_out_to_user(media_object_id, current_user.id).empty? + raise StreamToken::Unauthorized else - if Avalon::Configuration.controlled_digital_lending_enabled? && Checkout.checked_out_to_user(media_object_id, current_user.id).empty? - raise StreamToken::Unauthorized - else - add_stream_url(stream_info) - end + add_stream_url(stream_info) end - rescue Ldp::BadRequest - add_stream_url(stream_info) + rescue NoMethodError rescue StreamToken::Unauthorized end stream_info From 4336268ba99b555803316d157408100adb86ea43 Mon Sep 17 00:00:00 2001 From: Chris Colvard Date: Mon, 22 Aug 2022 15:30:35 -0400 Subject: [PATCH 107/230] Remove vestiges of old multipart dropdown handling: hidden field and unused JS --- .../dropdown_text_fields.js.coffee | 23 ------------------- .../_multipart_dropdown_field.html.erb | 1 - 2 files changed, 24 deletions(-) delete mode 100644 app/assets/javascripts/dropdown_text_fields.js.coffee diff --git a/app/assets/javascripts/dropdown_text_fields.js.coffee b/app/assets/javascripts/dropdown_text_fields.js.coffee deleted file mode 100644 index 1f4c254579..0000000000 --- a/app/assets/javascripts/dropdown_text_fields.js.coffee +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright 2011-2022, The Trustees of Indiana University and Northwestern -# University. Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software distributed -# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR -# CONDITIONS OF ANY KIND, either express or implied. See the License for the -# specific language governing permissions and limitations under the License. -# --- END LICENSE_HEADER BLOCK --- - - -$ -> - $(document).on 'click', '.dropdown-menu li a.dropdown-field', (event) -> - event.preventDefault() - d = $(this).parent() - group = d.closest('.input-group-btn') - group.find('.dropdown-toggle span').first().text(d.find('a').text()) - group.find('input[type="hidden"]').val(d.find('span.hidden').text()) - diff --git a/app/views/media_objects/_multipart_dropdown_field.html.erb b/app/views/media_objects/_multipart_dropdown_field.html.erb index 1e37b2dba9..c14af9df58 100644 --- a/app/views/media_objects/_multipart_dropdown_field.html.erb +++ b/app/views/media_objects/_multipart_dropdown_field.html.erb @@ -18,7 +18,6 @@ Unless required by applicable law or agreed to in writing, software distributed
      <% fieldname = "media_object[#{options[:dropdown_field]}]#{fieldarray}" %> - <%= hidden_field_tag fieldname, selected_value %> <% option_hash = (options[:dropdown_options].values).zip(options[:dropdown_options].keys) %> <%= select_tag fieldname.to_s, options_for_select(option_hash, selected_value) %>
      From 7282775718ab154edeeb800c252aa6cb396d65f4 Mon Sep 17 00:00:00 2001 From: dananji Date: Mon, 22 Aug 2022 08:54:49 -0700 Subject: [PATCH 108/230] Change CDL controls location in item page --- app/assets/stylesheets/avalon.scss | 32 ++++++--- .../_administrative_links.html.erb | 68 +++++++++---------- .../media_objects/_destroy_checkout.html.erb | 46 ++++++------- .../media_objects/_embed_checkout.html.erb | 2 +- app/views/media_objects/_item_view.html.erb | 3 +- app/views/media_objects/show.html.erb | 5 +- config/settings.yml | 2 +- 7 files changed, 84 insertions(+), 74 deletions(-) diff --git a/app/assets/stylesheets/avalon.scss b/app/assets/stylesheets/avalon.scss index 98084607cb..9af271e80c 100644 --- a/app/assets/stylesheets/avalon.scss +++ b/app/assets/stylesheets/avalon.scss @@ -1170,28 +1170,28 @@ td { /* CDL controls on view page styles */ .cdl-controls { - display: inline-flex; - margin-bottom: 1rem; - height: 2.5rem; - float: right; - @include media-breakpoint-down(sm) { - float: left; - margin-top: 0.5rem; + margin-top: 1rem; } - .remaining-time { display: flex; text-align: center; + + @include media-breakpoint-down(sm) { + margin: 0; + } } .remaining-time p { line-height: 1rem; margin: 0.25rem; text-align: left; + @include media-breakpoint-between(sm, md) { + padding: 0.75rem 0; + } } - .remaining-time span{ + .remaining-time span { color: #fff; margin-left: 0.25rem; padding: 0.15rem 0.25rem; @@ -1199,6 +1199,16 @@ td { background: $primary; font-size: small; line-height: initial; + + @include media-breakpoint-between(sm, md) { + padding: 0.75rem 0.25rem; + } + } + + #return-btn { + @include media-breakpoint-down(sm) { + float: right; + } } } @@ -1229,6 +1239,10 @@ td { right: 0; bottom: 0; height: 4rem; + + @include media-breakpoint-down(sm) { + top: -25%; + } } } diff --git a/app/views/media_objects/_administrative_links.html.erb b/app/views/media_objects/_administrative_links.html.erb index 221e59ee43..5865582148 100644 --- a/app/views/media_objects/_administrative_links.html.erb +++ b/app/views/media_objects/_administrative_links.html.erb @@ -14,46 +14,44 @@ Unless required by applicable law or agreed to in writing, software distributed --- END LICENSE_HEADER BLOCK --- %> <% if can? :update, @media_object %> -
      -

      -

      -

      -
      - <%= link_to 'Edit', edit_media_object_path(@media_object), class: 'btn btn-primary' %> - - <% if @media_object.published? %> - <% if can?(:unpublish, @media_object) %> - <%= link_to 'Unpublish', update_status_media_object_path(@media_object, status:'unpublish'), method: :put, class: 'btn btn-outline' %> +

      +

      +

      +
      + <%= link_to 'Edit', edit_media_object_path(@media_object), class: 'btn btn-primary' %> - <%# This might not be the best approach because it makes accidental - deletion possible just by following a link. Need to revisit when - extra cycles are available %> + <% if @media_object.published? %> + <% if can?(:unpublish, @media_object) %> + <%= link_to 'Unpublish', update_status_media_object_path(@media_object, status:'unpublish'), method: :put, class: 'btn btn-outline' %> + <% end %> + <% else %> + <%= link_to 'Publish', update_status_media_object_path(@media_object, status:'publish'), method: :put, class: 'btn btn-outline' %> + <% end %> - <% if can? :destroy, @media_object %> - <%= link_to 'Delete', confirm_remove_media_object_path(@media_object), class: 'btn btn-link' %> - <% end %> + <%# This might not be the best approach because it makes accidental + deletion possible just by following a link. Need to revisit when + extra cycles are available %> - <% if Settings.intercom.present? and can? :intercom_push, @media_object %> - <%= button_tag(Settings.intercom['default']['push_label'], class: 'btn btn-outline', data: {toggle:"modal", target:"#intercom_push"}) %> - <%= render "intercom_push_modal" %> - <% end %> -
      + <% if can? :destroy, @media_object %> + <%= link_to 'Delete', confirm_remove_media_object_path(@media_object), class: 'btn btn-link' %> + <% end %> + + <% if Settings.intercom.present? and can? :intercom_push, @media_object %> + <%= button_tag(Settings.intercom['default']['push_label'], class: 'btn btn-outline', data: {toggle:"modal", target:"#intercom_push"}) %> + <%= render "intercom_push_modal" %> + <% end %>
      <% end %> diff --git a/app/views/media_objects/_destroy_checkout.html.erb b/app/views/media_objects/_destroy_checkout.html.erb index 927961e2ad..d0e914c494 100644 --- a/app/views/media_objects/_destroy_checkout.html.erb +++ b/app/views/media_objects/_destroy_checkout.html.erb @@ -15,30 +15,30 @@ Unless required by applicable law or agreed to in writing, software distributed %> <% current_checkout=@media_object.current_checkout(current_user.id) if current_user %> <% if (can? :read, @media_object) && !current_checkout.nil? %> -
      > -
      +
      +
      + <%= t('media_object.cdl.time_remaining').html_safe %> + 0
      days
      + 00:00
      hh:mm
      +
      +
      <%= link_to 'Return now', return_checkout_url(current_checkout), class: 'btn btn-danger', method: :patch, - id: "return-btn", data: { checkout_returntime: current_checkout.return_time } %> -
      - <%= t('media_object.cdl.time_remaining').html_safe %> - 0
      days
      - 00:00
      hh:mm
      -
      -
      <%= t('.id_label') %> <%= t('.email_label') %><%= t('.role_label') %><%= t('.role_label') %> <%= t('.access_label') %> <%= t('.status_label') %>