From 3dec662ca52e31e246f79c43d9bce44813deac8e Mon Sep 17 00:00:00 2001 From: cjcolvar Date: Wed, 27 Mar 2024 15:04:43 -0400 Subject: [PATCH 001/152] Get latest Cypress version setup and running tests in docker and outside of docker --- .gitignore | 3 + docker-compose.yml | 4 +- package.json | 5 +- spec/cypress/cypress.config.js | 16 + spec/cypress/fixtures/example.json | 16 - spec/cypress/integration/homepage_spec.js | 2 +- spec/cypress/{cypress.json => support/e2e.js} | 34 +- yarn.lock | 856 +++++++++++++++++- 8 files changed, 886 insertions(+), 50 deletions(-) create mode 100644 spec/cypress/cypress.config.js rename spec/cypress/{cypress.json => support/e2e.js} (51%) diff --git a/.gitignore b/.gitignore index 1270104b50..96e25700a6 100644 --- a/.gitignore +++ b/.gitignore @@ -77,6 +77,9 @@ yarn-debug.log* # Cypress test output /cypress +/spec/cypress/downloads +/spec/cypress/screenshots +/spec/cypress/videos # ActiveStorage /storage diff --git a/docker-compose.yml b/docker-compose.yml index a3a3a8b389..6ad80861c3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -203,10 +203,10 @@ services: cypress: network_mode: host - image: cypress/included:3.8.2 + image: cypress/included:latest depends_on: - avalon - entrypoint: cypress run -C spec/cypress/cypress.json + entrypoint: cypress run -C spec/cypress/cypress.config.js working_dir: /e2e volumes: - ./:/e2e diff --git a/package.json b/package.json index 78d09be615..e86a704a19 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "@babel/plugin-transform-runtime": "7", "@babel/preset-env": "7", "babel-loader": "8", + "cypress": "^13.7.1", "html-webpack-plugin": "^5.3.2", "prop-types": "^15.7.2", "webpack-cli": "4", @@ -55,6 +56,8 @@ }, "scripts": { "start-collection-index": "webpack-dev-server --mode development --config config/webpack/collection_index.js --host 0.0.0.0", - "start-collection-view": "webpack-dev-server --mode development --config config/webpack/collection_view.js --host 0.0.0.0" + "start-collection-view": "webpack-dev-server --mode development --config config/webpack/collection_view.js --host 0.0.0.0", + "cypress:open": "cypress open -C spec/cypress/cypress.config.js", + "cypress:run": "cypress run -C spec/cypress/cypress.config.js" } } diff --git a/spec/cypress/cypress.config.js b/spec/cypress/cypress.config.js new file mode 100644 index 0000000000..b5d4cacd51 --- /dev/null +++ b/spec/cypress/cypress.config.js @@ -0,0 +1,16 @@ +const { defineConfig } = require("cypress"); + +module.exports = defineConfig({ + downloadsFolder: "spec/cypress/downloads", + fixturesFolder: "spec/cypress/fixtures", + screenshotsFolder: "spec/cypress/screenshots", + videosFolder: "spec/cypress/videos", + e2e: { + setupNodeEvents(on, config) { + // implement node event listeners here + }, + baseUrl: "http://localhost:3000", + supportFile: "spec/cypress/support/e2e.js", + specPattern: "spec/cypress/integration/**/*.js" + }, +}); diff --git a/spec/cypress/fixtures/example.json b/spec/cypress/fixtures/example.json index debf55d2e6..02e4254378 100644 --- a/spec/cypress/fixtures/example.json +++ b/spec/cypress/fixtures/example.json @@ -1,19 +1,3 @@ -/* - * Copyright 2011-2024, 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 --- -*/ - { "name": "Using fixtures to represent data", "email": "hello@cypress.io", diff --git a/spec/cypress/integration/homepage_spec.js b/spec/cypress/integration/homepage_spec.js index 2dd4d8acd4..e6932aa708 100644 --- a/spec/cypress/integration/homepage_spec.js +++ b/spec/cypress/integration/homepage_spec.js @@ -47,7 +47,7 @@ context('Homepage', () => { cy.contains('Email address') cy.contains('Confirm email address') cy.contains('Subject') - cy.contains('Comments') + cy.contains('Comment') cy.contains('Submit comments') }) diff --git a/spec/cypress/cypress.json b/spec/cypress/support/e2e.js similarity index 51% rename from spec/cypress/cypress.json rename to spec/cypress/support/e2e.js index 9b643838da..22b9ec0ca2 100644 --- a/spec/cypress/cypress.json +++ b/spec/cypress/support/e2e.js @@ -14,17 +14,23 @@ * --- END LICENSE_HEADER BLOCK --- */ -{ - "baseUrl": "http://localhost:3000", - "fixtureFolder": "spec/cypress/fixtures", - "integrationFolder": "spec/cypress/integration", - "pluginsFile": "spec/cypress/plugins/index.js", - "supportFile": "spec/cypress/support/index.js", - "env": { - "USERS_ADMINISTRATOR_EMAIL": "administrator@example.com", - "USERS_ADMINISTRATOR_PASSWORD": "password", - "USERS_USER_EMAIL": "user@example.com", - "USERS_USER_PASSWORD": "password", - "MEDIA_OBJECT_ID": "123456789" - } -} +// *********************************************************** +// This example support/index.js is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** + +// Import commands.js using ES2015 syntax: +import './commands' + +// Alternatively you can use CommonJS syntax: +// require('./commands') diff --git a/yarn.lock b/yarn.lock index dbe7b0c6ed..22a4340027 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1165,6 +1165,43 @@ "@babel/helper-validator-identifier" "^7.22.20" to-fast-properties "^2.0.0" +"@colors/colors@1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" + integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== + +"@cypress/request@^3.0.0": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@cypress/request/-/request-3.0.1.tgz#72d7d5425236a2413bd3d8bb66d02d9dc3168960" + integrity sha512-TWivJlJi8ZDx2wGOw1dbLuHJKUYX7bWySw377nlnGOW3hP9/MUKIsEdXT/YngWxVdgNCHRBmFlBipE+5/2ZZlQ== + dependencies: + aws-sign2 "~0.7.0" + aws4 "^1.8.0" + caseless "~0.12.0" + combined-stream "~1.0.6" + extend "~3.0.2" + forever-agent "~0.6.1" + form-data "~2.3.2" + http-signature "~1.3.6" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.19" + performance-now "^2.1.0" + qs "6.10.4" + safe-buffer "^5.1.2" + tough-cookie "^4.1.3" + tunnel-agent "^0.6.0" + uuid "^8.3.2" + +"@cypress/xvfb@^1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@cypress/xvfb/-/xvfb-1.2.4.tgz#2daf42e8275b39f4aa53c14214e557bd14e7748a" + integrity sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q== + dependencies: + debug "^3.1.0" + lodash.once "^4.1.1" + "@discoveryjs/json-ext@^0.5.0": version "0.5.7" resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" @@ -1704,6 +1741,16 @@ "@types/mime" "*" "@types/node" "*" +"@types/sinonjs__fake-timers@8.1.1": + version "8.1.1" + resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.1.tgz#b49c2c70150141a15e0fa7e79cf1f92a72934ce3" + integrity sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g== + +"@types/sizzle@^2.3.2": + version "2.3.8" + resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.8.tgz#518609aefb797da19bf222feb199e8f653ff7627" + integrity sha512-0vWLNK2D5MT9dg0iOo8GlKguPAU02QjmZitPEsXRuJXU/OGIOt9vT9Fc26wtYuavLxtO45v9PGleoL9Z0k1LHg== + "@types/sockjs@^0.3.33": version "0.3.33" resolved "https://registry.yarnpkg.com/@types/sockjs/-/sockjs-0.3.33.tgz#570d3a0b99ac995360e3136fd6045113b1bd236f" @@ -1735,6 +1782,13 @@ dependencies: "@types/yargs-parser" "*" +"@types/yauzl@^2.9.1": + version "2.10.3" + resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.10.3.tgz#e9b2808b4f109504a03cda958259876f61017999" + integrity sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q== + dependencies: + "@types/node" "*" + "@videojs/http-streaming@2.16.2": version "2.16.2" resolved "https://registry.yarnpkg.com/@videojs/http-streaming/-/http-streaming-2.16.2.tgz#a9be925b4e368a41dbd67d49c4f566715169b84b" @@ -1961,6 +2015,14 @@ aes-decrypter@3.1.3: global "^4.4.0" pkcs7 "^1.0.4" +aggregate-error@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" + integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA== + dependencies: + clean-stack "^2.0.0" + indent-string "^4.0.0" + ajv-formats@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520" @@ -2010,6 +2072,18 @@ ajv@^8.9.0: require-from-string "^2.0.2" uri-js "^4.2.2" +ansi-colors@^4.1.1: + version "4.1.3" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.3.tgz#37611340eb2243e70cc604cad35d63270d48781b" + integrity sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw== + +ansi-escapes@^4.3.0: + version "4.3.2" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" + integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== + dependencies: + type-fest "^0.21.3" + ansi-html-community@^0.0.8: version "0.0.8" resolved "https://registry.yarnpkg.com/ansi-html-community/-/ansi-html-community-0.0.8.tgz#69fbc4d6ccbe383f9736934ae34c3f8290f1bf41" @@ -2020,6 +2094,11 @@ ansi-regex@^2.0.0: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" integrity sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA== +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + ansi-styles@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" @@ -2027,7 +2106,7 @@ ansi-styles@^3.2.1: dependencies: color-convert "^1.9.0" -ansi-styles@^4.1.0: +ansi-styles@^4.0.0, ansi-styles@^4.1.0: version "4.3.0" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== @@ -2042,6 +2121,11 @@ anymatch@~3.1.2: normalize-path "^3.0.0" picomatch "^2.0.4" +arch@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/arch/-/arch-2.2.0.tgz#1bc47818f305764f23ab3306b0bfc086c5a29d11" + integrity sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ== + argparse@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" @@ -2069,16 +2153,53 @@ asap@^2.0.6: resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" integrity sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA== +asn1@~0.2.3: + version "0.2.6" + resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.6.tgz#0d3a7bb6e64e02a90c0303b31f292868ea09a08d" + integrity sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ== + dependencies: + safer-buffer "~2.1.0" + +assert-plus@1.0.0, assert-plus@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" + integrity sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw== + +astral-regex@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" + integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== + +async@^3.2.0: + version "3.2.5" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.5.tgz#ebd52a8fdaf7a2289a24df399f8d8485c8a46b66" + integrity sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg== + asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== +at-least-node@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" + integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== + available-typed-arrays@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7" integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw== +aws-sign2@~0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" + integrity sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA== + +aws4@^1.8.0: + version "1.12.0" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.12.0.tgz#ce1c9d143389679e253b314241ea9aa5cec980d3" + integrity sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg== + axios@^1.6.0: version "1.6.7" resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.7.tgz#7b48c2e27c96f9c68a2f8f31e2ab19f59b06b0a7" @@ -2151,6 +2272,13 @@ batch@0.6.1: resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16" integrity sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY= +bcrypt-pbkdf@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" + integrity sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w== + dependencies: + tweetnacl "^0.14.3" + big.js@^5.2.2: version "5.2.2" resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" @@ -2161,6 +2289,16 @@ binary-extensions@^2.0.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== +blob-util@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/blob-util/-/blob-util-2.0.2.tgz#3b4e3c281111bb7f11128518006cdc60b403a1eb" + integrity sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ== + +bluebird@^3.7.2: + version "3.7.2" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" + integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== + bluebird@~3.4.0: version "3.4.7" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.4.7.tgz#f72d760be09b7f76d08ed8fae98b289a8d05fab3" @@ -2235,11 +2373,24 @@ browserslist@^4.14.5, browserslist@^4.21.4, browserslist@^4.21.9: node-releases "^2.0.13" update-browserslist-db "^1.0.11" +buffer-crc32@~0.2.3: + version "0.2.13" + resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" + integrity sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ== + buffer-from@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== +buffer@^5.7.1: + version "5.7.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" + integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.1.13" + buffer@^6.0.3: version "6.0.3" resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" @@ -2258,6 +2409,11 @@ bytes@3.1.2: resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== +cachedir@^2.3.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/cachedir/-/cachedir-2.4.0.tgz#7fef9cf7367233d7c88068fe6e34ed0d355a610d" + integrity sha512-9EtFOZR8g22CL7BWjJ9BUx1+A/djkofnyW3aOXZORNW2kxoUpx2h+uN2cOqwPmFhnpVmxg+KW2OjOSgChTEvsQ== + call-bind@^1.0.0, call-bind@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" @@ -2299,6 +2455,11 @@ caniuse-lite@^1.0.30001517: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001520.tgz#62e2b7a1c7b35269594cf296a80bdf8cb9565006" integrity sha512-tahF5O9EiiTzwTUqAeFjIZbn4Dnqxzz7ktrgGlMYNLH43Ul26IgTMH/zvL3DG0lZxBYnlT04axvInszUsZULdA== +caseless@~0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" + integrity sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw== + chalk@^2.0.0, chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" @@ -2308,7 +2469,7 @@ chalk@^2.0.0, chalk@^2.4.2: escape-string-regexp "^1.0.5" supports-color "^5.3.0" -chalk@^4.0, chalk@^4.0.0: +chalk@^4.0, chalk@^4.0.0, chalk@^4.1.0: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== @@ -2316,6 +2477,11 @@ chalk@^4.0, chalk@^4.0.0: ansi-styles "^4.1.0" supports-color "^7.1.0" +check-more-types@^2.24.0: + version "2.24.0" + resolved "https://registry.yarnpkg.com/check-more-types/-/check-more-types-2.24.0.tgz#1420ffb10fd444dcfc79b43891bbfffd32a84600" + integrity sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA== + "chokidar@>=3.0.0 <4.0.0": version "3.5.2" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.2.tgz#dba3976fcadb016f66fd365021d91600d01c1e75" @@ -2368,6 +2534,35 @@ clean-css@^4.2.3: dependencies: source-map "~0.6.0" +clean-stack@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" + integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== + +cli-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" + integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw== + dependencies: + restore-cursor "^3.1.0" + +cli-table3@~0.6.1: + version "0.6.4" + resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.4.tgz#d1c536b8a3f2e7bec58f67ac9e5769b1b30088b0" + integrity sha512-Lm3L0p+/npIQWNIiyF/nAn7T5dnOwR3xNTHXYEBFBFVPXzCVNZ5lqEC/1eo/EVfpDsQ1I+TX4ORPQgp+UI0CRw== + dependencies: + string-width "^4.2.0" + optionalDependencies: + "@colors/colors" "1.5.0" + +cli-truncate@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-2.1.0.tgz#c39e28bf05edcde5be3b98992a22deed5a2b93c7" + integrity sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg== + dependencies: + slice-ansi "^3.0.0" + string-width "^4.2.0" + clone-deep@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387" @@ -2416,12 +2611,12 @@ colorette@^2.0.10: resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.16.tgz#713b9af84fdb000139f04546bd4a93f62a5085da" integrity sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g== -colorette@^2.0.14: +colorette@^2.0.14, colorette@^2.0.16: version "2.0.20" resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a" integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== -combined-stream@^1.0.8: +combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6: version "1.0.8" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== @@ -2438,11 +2633,21 @@ commander@^4.1.1: resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== +commander@^6.2.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c" + integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA== + commander@^7.0.0, commander@^7.2.0: version "7.2.0" resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== +common-tags@^1.8.0: + version "1.8.2" + resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.8.2.tgz#94ebb3c076d26032745fd54face7f688ef5ac9c6" + integrity sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA== + commondir@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" @@ -2522,6 +2727,11 @@ core-js-compat@^3.31.0: dependencies: browserslist "^4.21.9" +core-util-is@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + integrity sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ== + core-util-is@~1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" @@ -2543,7 +2753,7 @@ cropperjs@^1.5.5: resolved "https://registry.yarnpkg.com/cropperjs/-/cropperjs-1.5.12.tgz#d9c0db2bfb8c0d769d51739e8f916bbc44e10f50" integrity sha512-re7UdjE5UnwdrovyhNzZ6gathI4Rs3KGCBSc8HCIjUo5hO42CtzyblmWLj6QWVw7huHyDMfpKxhiO2II77nhDw== -cross-spawn@^7.0.3: +cross-spawn@^7.0.0, cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== @@ -2709,6 +2919,66 @@ csstype@^3.0.2: resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.9.tgz#6410af31b26bd0520933d02cbc64fce9ce3fbf0b" integrity sha512-rpw6JPxK6Rfg1zLOYCSwle2GFOOsnjmDYDaBwEcwoOg4qlsIVCN789VkBZDJAGi4T07gI4YSutR43t9Zz4Lzuw== +cypress@^13.7.1: + version "13.7.1" + resolved "https://registry.yarnpkg.com/cypress/-/cypress-13.7.1.tgz#d1208eb04efd46ef52a30480a5da71a03373261a" + integrity sha512-4u/rpFNxOFCoFX/Z5h+uwlkBO4mWzAjveURi3vqdSu56HPvVdyGTxGw4XKGWt399Y1JwIn9E1L9uMXQpc0o55w== + dependencies: + "@cypress/request" "^3.0.0" + "@cypress/xvfb" "^1.2.4" + "@types/sinonjs__fake-timers" "8.1.1" + "@types/sizzle" "^2.3.2" + arch "^2.2.0" + blob-util "^2.0.2" + bluebird "^3.7.2" + buffer "^5.7.1" + cachedir "^2.3.0" + chalk "^4.1.0" + check-more-types "^2.24.0" + cli-cursor "^3.1.0" + cli-table3 "~0.6.1" + commander "^6.2.1" + common-tags "^1.8.0" + dayjs "^1.10.4" + debug "^4.3.4" + enquirer "^2.3.6" + eventemitter2 "6.4.7" + execa "4.1.0" + executable "^4.1.1" + extract-zip "2.0.1" + figures "^3.2.0" + fs-extra "^9.1.0" + getos "^3.2.1" + is-ci "^3.0.1" + is-installed-globally "~0.4.0" + lazy-ass "^1.6.0" + listr2 "^3.8.3" + lodash "^4.17.21" + log-symbols "^4.0.0" + minimist "^1.2.8" + ospath "^1.2.2" + pretty-bytes "^5.6.0" + process "^0.11.10" + proxy-from-env "1.0.0" + request-progress "^3.0.0" + semver "^7.5.3" + supports-color "^8.1.1" + tmp "~0.2.1" + untildify "^4.0.0" + yauzl "^2.10.0" + +dashdash@^1.12.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" + integrity sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g== + dependencies: + assert-plus "^1.0.0" + +dayjs@^1.10.4: + version "1.11.10" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.10.tgz#68acea85317a6e164457d6d6947564029a6a16a0" + integrity sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ== + debug@2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" @@ -2716,6 +2986,13 @@ debug@2.6.9: dependencies: ms "2.0.0" +debug@^3.1.0: + version "3.2.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" + integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== + dependencies: + ms "^2.1.1" + debug@^4.1.0, debug@^4.1.1: version "4.3.2" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b" @@ -2723,7 +3000,7 @@ debug@^4.1.0, debug@^4.1.1: dependencies: ms "2.1.2" -debug@^4.3.1: +debug@^4.3.1, debug@^4.3.4: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== @@ -2905,6 +3182,14 @@ duck@^0.1.12: dependencies: underscore "^1.13.1" +ecc-jsbn@~0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" + integrity sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw== + dependencies: + jsbn "~0.1.0" + safer-buffer "^2.1.0" + ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" @@ -2920,6 +3205,11 @@ electron-to-chromium@^1.4.477: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.492.tgz#83fed8beb64ec60578069e15dddd17b13a77ca56" integrity sha512-36K9b/6skMVwAIEsC7GiQ8I8N3soCALVSHqWHzNDtGemAcI9Xu8hP02cywWM0A794rTHm0b0zHPeLJHtgFVamQ== +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + emojis-list@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78" @@ -2930,6 +3220,13 @@ encodeurl@~1.0.2: resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= +end-of-stream@^1.1.0: + version "1.4.4" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" + integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== + dependencies: + once "^1.4.0" + enhanced-resolve@^5.15.0: version "5.15.0" resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz#1af946c7d93603eb88e9896cee4904dc012e9c35" @@ -2938,6 +3235,14 @@ enhanced-resolve@^5.15.0: graceful-fs "^4.2.4" tapable "^2.2.0" +enquirer@^2.3.6: + version "2.4.1" + resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.4.1.tgz#93334b3fbd74fc7097b224ab4a8fb7e40bf4ae56" + integrity sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ== + dependencies: + ansi-colors "^4.1.1" + strip-ansi "^6.0.1" + entities@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" @@ -3025,6 +3330,11 @@ etag@~1.8.1: resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= +eventemitter2@6.4.7: + version "6.4.7" + resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-6.4.7.tgz#a7f6c4d7abf28a14c1ef3442f21cb306a054271d" + integrity sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg== + eventemitter3@3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.0.tgz#090b4d6cdbd645ed10bf750d4b5407942d7ba163" @@ -3040,6 +3350,21 @@ events@^3.2.0: resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== +execa@4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-4.1.0.tgz#4e5491ad1572f2f17a77d388c6c857135b22847a" + integrity sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA== + dependencies: + cross-spawn "^7.0.0" + get-stream "^5.0.0" + human-signals "^1.1.1" + is-stream "^2.0.0" + merge-stream "^2.0.0" + npm-run-path "^4.0.0" + onetime "^5.1.0" + signal-exit "^3.0.2" + strip-final-newline "^2.0.0" + execa@^5.0.0: version "5.1.1" resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" @@ -3055,6 +3380,13 @@ execa@^5.0.0: signal-exit "^3.0.3" strip-final-newline "^2.0.0" +executable@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/executable/-/executable-4.1.1.tgz#41532bff361d3e57af4d763b70582db18f5d133c" + integrity sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg== + dependencies: + pify "^2.2.0" + express@^4.17.3: version "4.18.1" resolved "https://registry.yarnpkg.com/express/-/express-4.18.1.tgz#7797de8b9c72c857b9cd0e14a5eea80666267caf" @@ -3092,6 +3424,32 @@ express@^4.17.3: utils-merge "1.0.1" vary "~1.1.2" +extend@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + +extract-zip@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-2.0.1.tgz#663dca56fe46df890d5f131ef4a06d22bb8ba13a" + integrity sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg== + dependencies: + debug "^4.1.1" + get-stream "^5.1.0" + yauzl "^2.10.0" + optionalDependencies: + "@types/yauzl" "^2.9.1" + +extsprintf@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" + integrity sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g== + +extsprintf@^1.2.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.1.tgz#8d172c064867f235c0c84a596806d279bf4bcc07" + integrity sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA== + fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" @@ -3114,6 +3472,20 @@ faye-websocket@^0.11.3: dependencies: websocket-driver ">=0.5.1" +fd-slicer@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e" + integrity sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g== + dependencies: + pend "~1.2.0" + +figures@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" + integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg== + dependencies: + escape-string-regexp "^1.0.5" + fill-range@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" @@ -3163,6 +3535,11 @@ for-each@^0.3.3: dependencies: is-callable "^1.1.3" +forever-agent@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" + integrity sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw== + form-data@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" @@ -3172,6 +3549,15 @@ form-data@^4.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" +form-data@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" + integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.6" + mime-types "^2.1.12" + forwarded@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" @@ -3182,6 +3568,16 @@ fresh@0.5.2: resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= +fs-extra@^9.1.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" + integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== + dependencies: + at-least-node "^1.0.0" + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + fs-monkey@1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/fs-monkey/-/fs-monkey-1.0.3.tgz#ae3ac92d53bb328efe0e9a1d9541f6ad8d48e2d3" @@ -3226,6 +3622,13 @@ get-intrinsic@^1.1.3: has-proto "^1.0.1" has-symbols "^1.0.3" +get-stream@^5.0.0, get-stream@^5.1.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" + integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA== + dependencies: + pump "^3.0.0" + get-stream@^6.0.0: version "6.0.1" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" @@ -3238,6 +3641,20 @@ get-value@^3.0.0: dependencies: isobject "^3.0.1" +getos@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/getos/-/getos-3.2.1.tgz#0134d1f4e00eb46144c5a9c0ac4dc087cbb27dc5" + integrity sha512-U56CfOK17OKgTVqozZjUKNdkfEv6jk5WISBJ8SHoagjE6L69zOwl3Z+O8myjY9MEW3i2HPWQBt/LTbCgcC973Q== + dependencies: + async "^3.2.0" + +getpass@^0.1.1: + version "0.1.7" + resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" + integrity sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng== + dependencies: + assert-plus "^1.0.0" + glob-parent@^6.0.2, glob-parent@~5.1.2: version "6.0.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" @@ -3274,6 +3691,13 @@ glob@^7.2.0: once "^1.3.0" path-is-absolute "^1.0.0" +global-dirs@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-3.0.1.tgz#0c488971f066baceda21447aecb1a8b911d22485" + integrity sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA== + dependencies: + ini "2.0.0" + global@^4.3.1, global@^4.4.0, global@~4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/global/-/global-4.4.0.tgz#3e7b105179006a323ed71aafca3e9c57a5cc6406" @@ -3299,7 +3723,7 @@ graceful-fs@^4.1.2: resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.8.tgz#e412b8d33f5e006593cbd3cee6df9f2cebbe802a" integrity sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg== -graceful-fs@^4.2.4, graceful-fs@^4.2.9: +graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4, graceful-fs@^4.2.9: version "4.2.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== @@ -3502,6 +3926,20 @@ http-proxy@^1.18.1: follow-redirects "^1.0.0" requires-port "^1.0.0" +http-signature@~1.3.6: + version "1.3.6" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.3.6.tgz#cb6fbfdf86d1c974f343be94e87f7fc128662cf9" + integrity sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw== + dependencies: + assert-plus "^1.0.0" + jsprim "^2.0.2" + sshpk "^1.14.1" + +human-signals@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" + integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw== + human-signals@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" @@ -3524,7 +3962,7 @@ icss-utils@^5.0.0, icss-utils@^5.1.0: resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae" integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA== -ieee754@^1.2.1: +ieee754@^1.1.13, ieee754@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== @@ -3555,6 +3993,11 @@ import-local@^3.0.2: pkg-dir "^4.2.0" resolve-cwd "^3.0.0" +indent-string@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" + integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== + individual@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/individual/-/individual-2.0.0.tgz#833b097dad23294e76117a98fb38e0d9ad61bb97" @@ -3578,6 +4021,11 @@ inherits@2.0.3: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= +ini@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ini/-/ini-2.0.0.tgz#e5fd556ecdd5726be978fa1001862eacb0a94bc5" + integrity sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA== + interpret@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/interpret/-/interpret-2.2.0.tgz#1a78a0b5965c40a5416d007ad6f50ad27c417df9" @@ -3625,6 +4073,13 @@ is-callable@^1.1.3: resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== +is-ci@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-3.0.1.tgz#db6ecbed1bd659c43dac0f45661e7674103d1867" + integrity sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ== + dependencies: + ci-info "^3.2.0" + is-core-module@^2.13.0: version "2.13.0" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.0.tgz#bb52aa6e2cbd49a30c2ba68c42bf3435ba6072db" @@ -3649,6 +4104,11 @@ is-extglob@^2.1.1: resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + is-function@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/is-function/-/is-function-1.0.2.tgz#4f097f30abf6efadac9833b17ca5dc03f8144e08" @@ -3673,11 +4133,24 @@ is-in-browser@^1.0.2, is-in-browser@^1.1.3: resolved "https://registry.yarnpkg.com/is-in-browser/-/is-in-browser-1.1.3.tgz#56ff4db683a078c6082eb95dad7dc62e1d04f835" integrity sha512-FeXIBgG/CPGd/WUxuEyvgGTEfwiG9Z4EKGxjNMRqviiIIfsmgrpnHLffEDdwUHqNva1VEW91o3xBT/m8Elgl9g== +is-installed-globally@~0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.4.0.tgz#9a0fd407949c30f86eb6959ef1b7994ed0b7b520" + integrity sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ== + dependencies: + global-dirs "^3.0.0" + is-path-inside "^3.0.2" + is-number@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== +is-path-inside@^3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" + integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== + is-plain-obj@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-3.0.0.tgz#af6f2ea14ac5a646183a5bbdb5baabbc156ad9d7" @@ -3712,6 +4185,16 @@ is-typed-array@^1.1.3: dependencies: which-typed-array "^1.1.11" +is-typedarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA== + +is-unicode-supported@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" + integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== + is-wsl@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" @@ -3747,6 +4230,11 @@ isomorphic-unfetch@^3.0.0: node-fetch "^2.6.1" unfetch "^4.2.0" +isstream@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" + integrity sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g== + jest-util@^29.6.2: version "29.6.2" resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.6.2.tgz#8a052df8fff2eebe446769fd88814521a517664d" @@ -3790,6 +4278,11 @@ js-yaml@^4.1.0: dependencies: argparse "^2.0.1" +jsbn@~0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" + integrity sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg== + jsesc@^2.5.1: version "2.5.2" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" @@ -3815,11 +4308,40 @@ json-schema-traverse@^1.0.0: resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== +json-schema@0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.4.0.tgz#f7de4cf6efab838ebaeb3236474cbba5a1930ab5" + integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA== + +json-stringify-safe@~5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA== + json5@^2.1.2, json5@^2.2.2: version "2.2.3" resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== +jsonfile@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" + integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== + dependencies: + universalify "^2.0.0" + optionalDependencies: + graceful-fs "^4.1.6" + +jsprim@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-2.0.2.tgz#77ca23dbcd4135cd364800d22ff82c2185803d4d" + integrity sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ== + dependencies: + assert-plus "1.0.0" + extsprintf "1.3.0" + json-schema "0.4.0" + verror "1.10.0" + jss-plugin-camel-case@^10.5.1: version "10.10.0" resolved "https://registry.yarnpkg.com/jss-plugin-camel-case/-/jss-plugin-camel-case-10.10.0.tgz#27ea159bab67eb4837fa0260204eb7925d4daa1c" @@ -3923,6 +4445,11 @@ launch-editor@^2.6.0: picocolors "^1.0.0" shell-quote "^1.7.3" +lazy-ass@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/lazy-ass/-/lazy-ass-1.6.0.tgz#7999655e8646c17f089fdd187d150d3324d54513" + integrity sha512-cc8oEVoctTvsFZ/Oje/kGnHbpWHYBe8IAJe4C0QNc3t8uM/0Y8+erSz/7Y1ALuXTEZTMvxXwO6YbX1ey3ujiZw== + lie@~3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/lie/-/lie-3.3.0.tgz#dcf82dee545f46074daf200c7c1c5a08e0f40f6a" @@ -3940,6 +4467,20 @@ lines-and-columns@^1.1.6: resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA= +listr2@^3.8.3: + version "3.14.0" + resolved "https://registry.yarnpkg.com/listr2/-/listr2-3.14.0.tgz#23101cc62e1375fd5836b248276d1d2b51fdbe9e" + integrity sha512-TyWI8G99GX9GjE54cJ+RrNMcIFBfwMPxc3XTFiAYGN4s10hWROGtOg7+O6u6LE3mNkyld7RSLE6nrKBvTfcs3g== + dependencies: + cli-truncate "^2.1.0" + colorette "^2.0.16" + log-update "^4.0.0" + p-map "^4.0.0" + rfdc "^1.3.0" + rxjs "^7.5.1" + through "^2.3.8" + wrap-ansi "^7.0.0" + loader-runner@^4.2.0: version "4.3.0" resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.3.0.tgz#c1b4a163b99f614830353b16755e7149ac2314e1" @@ -3988,6 +4529,11 @@ lodash.memoize@^4.1.2: resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4= +lodash.once@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" + integrity sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg== + lodash.uniq@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" @@ -3998,6 +4544,24 @@ lodash@4.17.21, lodash@^4.17.20, lodash@^4.17.21: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== +log-symbols@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" + integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== + dependencies: + chalk "^4.1.0" + is-unicode-supported "^0.1.0" + +log-update@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/log-update/-/log-update-4.0.0.tgz#589ecd352471f2a1c0c570287543a64dfd20e0a1" + integrity sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg== + dependencies: + ansi-escapes "^4.3.0" + cli-cursor "^3.1.0" + slice-ansi "^4.0.0" + wrap-ansi "^6.2.0" + loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" @@ -4132,7 +4696,7 @@ mime-db@1.52.0, mime-db@^1.52.0: resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== -mime-types@^2.1.12, mime-types@^2.1.27, mime-types@^2.1.31, mime-types@~2.1.34: +mime-types@^2.1.12, mime-types@^2.1.27, mime-types@^2.1.31, mime-types@~2.1.19, mime-types@~2.1.34: version "2.1.35" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== @@ -4182,6 +4746,11 @@ minimatch@^3.0.4, minimatch@^3.1.1: dependencies: brace-expansion "^1.1.7" +minimist@^1.2.8: + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + moment@^2.29.4: version "2.29.4" resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108" @@ -4207,7 +4776,7 @@ ms@2.1.2: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -ms@2.1.3: +ms@2.1.3, ms@^2.1.1: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== @@ -4288,7 +4857,7 @@ normalize-path@^3.0.0, normalize-path@~3.0.0: resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== -npm-run-path@^4.0.1: +npm-run-path@^4.0.0, npm-run-path@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== @@ -4329,14 +4898,14 @@ on-headers@~1.0.2: resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== -once@^1.3.0: +once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= dependencies: wrappy "1" -onetime@^5.1.2: +onetime@^5.1.0, onetime@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== @@ -4357,6 +4926,11 @@ option@~0.2.1: resolved "https://registry.yarnpkg.com/option/-/option-0.2.4.tgz#fd475cdf98dcabb3cb397a3ba5284feb45edbfe4" integrity sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A== +ospath@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/ospath/-/ospath-1.2.2.tgz#1276639774a3f8ef2572f7fe4280e0ea4550c07b" + integrity sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA== + p-limit@^2.2.0: version "2.3.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" @@ -4371,6 +4945,13 @@ p-locate@^4.1.0: dependencies: p-limit "^2.2.0" +p-map@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b" + integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ== + dependencies: + aggregate-error "^3.0.0" + p-retry@^4.5.0: version "4.6.2" resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-4.6.2.tgz#9baae7184057edd4e17231cee04264106e092a16" @@ -4479,6 +5060,16 @@ peaks.js@^2.1.0: dependencies: eventemitter3 "~4.0.7" +pend@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" + integrity sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg== + +performance-now@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" + integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow== + picocolors@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" @@ -4494,6 +5085,11 @@ picomatch@^2.2.3, picomatch@^2.3.1: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== +pify@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" + integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog== + pkcs7@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/pkcs7/-/pkcs7-1.0.4.tgz#6090b9e71160dabf69209d719cbafa538b00a1cb" @@ -4772,6 +5368,11 @@ postcss@^8.3.11, postcss@^8.4.21, postcss@^8.4.24: picocolors "^1.0.0" source-map-js "^1.2.0" +pretty-bytes@^5.6.0: + version "5.6.0" + resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb" + integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg== + pretty-error@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/pretty-error/-/pretty-error-3.0.4.tgz#94b1d54f76c1ed95b9c604b9de2194838e5b574e" @@ -4824,16 +5425,39 @@ proxy-addr@~2.0.7: forwarded "0.2.0" ipaddr.js "1.9.1" +proxy-from-env@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.0.0.tgz#33c50398f70ea7eb96d21f7b817630a55791c7ee" + integrity sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A== + proxy-from-env@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== +psl@^1.1.33: + version "1.9.0" + resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7" + integrity sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag== + +pump@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" + integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + punycode@^2.1.0: version "2.1.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== +punycode@^2.1.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== + qs@6.10.3: version "6.10.3" resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.3.tgz#d6cde1b2ffca87b5aa57889816c5f81535e22e8e" @@ -4841,6 +5465,18 @@ qs@6.10.3: dependencies: side-channel "^1.0.4" +qs@6.10.4: + version "6.10.4" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.4.tgz#6a3003755add91c0ec9eacdc5f878b034e73f9e7" + integrity sha512-OQiU+C+Ds5qiH91qh/mg0w+8nwQuLjM4F4M/PbmhDOoYehPh+Fb0bDjtR1sOvy7YKxvj28Y/M0PhP5uVX0kB+g== + dependencies: + side-channel "^1.0.4" + +querystringify@^2.1.1: + version "2.2.0" + resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6" + integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ== + randombytes@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" @@ -5173,6 +5809,13 @@ renderkid@^2.0.6: lodash "^4.17.21" strip-ansi "^3.0.1" +request-progress@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/request-progress/-/request-progress-3.0.0.tgz#4ca754081c7fec63f505e4faa825aa06cd669dbe" + integrity sha512-MnWzEHHaxHO2iWiQuHrUPBi/1WeBf5PkxQqNyNvLl9VAYSdXkP8tQ3pBSeCPD+yw0v0Aq1zosWLz0BdeXpWwZg== + dependencies: + throttleit "^1.0.0" + require-from-string@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" @@ -5217,11 +5860,24 @@ resolve@^1.19.0, resolve@^1.9.0: path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" +restore-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e" + integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA== + dependencies: + onetime "^5.1.0" + signal-exit "^3.0.2" + retry@^0.13.1: version "0.13.1" resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658" integrity sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg== +rfdc@^1.3.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.1.tgz#2b6d4df52dffe8bb346992a10ea9451f24373a8f" + integrity sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg== + rimraf@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" @@ -5243,12 +5899,19 @@ rxjs@^6.4.0: dependencies: tslib "^1.9.0" +rxjs@^7.5.1: + version "7.8.1" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.1.tgz#6f6f3d99ea8044291efd92e7c7fcf562c4057543" + integrity sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg== + dependencies: + tslib "^2.1.0" + safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.2" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.1.0, safe-buffer@~5.2.0: +safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.2, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== @@ -5260,7 +5923,7 @@ safe-json-parse@4.0.0: dependencies: rust-result "^1.0.0" -"safer-buffer@>= 2.1.2 < 3": +"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== @@ -5363,6 +6026,13 @@ semver@^7.3.8: dependencies: lru-cache "^6.0.0" +semver@^7.5.3: + version "7.6.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.0.tgz#1a46a4db4bffcccd97b743b5005c8325f23d4e2d" + integrity sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg== + dependencies: + lru-cache "^6.0.0" + send@0.18.0: version "0.18.0" resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" @@ -5487,6 +6157,24 @@ signal-exit@^3.0.2, signal-exit@^3.0.3: resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== +slice-ansi@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-3.0.0.tgz#31ddc10930a1b7e0b67b08c96c2f49b77a789787" + integrity sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ== + dependencies: + ansi-styles "^4.0.0" + astral-regex "^2.0.0" + is-fullwidth-code-point "^3.0.0" + +slice-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b" + integrity sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ== + dependencies: + ansi-styles "^4.0.0" + astral-regex "^2.0.0" + is-fullwidth-code-point "^3.0.0" + sockjs@^0.3.24: version "0.3.24" resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.24.tgz#c9bc8995f33a111bea0395ec30aa3206bdb5ccce" @@ -5555,6 +6243,21 @@ sprintf-js@~1.0.2: resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== +sshpk@^1.14.1: + version "1.18.0" + resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.18.0.tgz#1663e55cddf4d688b86a46b77f0d5fe363aba028" + integrity sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ== + dependencies: + asn1 "~0.2.3" + assert-plus "^1.0.0" + bcrypt-pbkdf "^1.0.0" + dashdash "^1.12.0" + ecc-jsbn "~0.1.1" + getpass "^0.1.1" + jsbn "~0.1.0" + safer-buffer "^2.0.2" + tweetnacl "~0.14.0" + statuses@2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" @@ -5573,6 +6276,15 @@ stream-browserify@^3.0.0: inherits "~2.0.4" readable-stream "^3.5.0" +string-width@^4.1.0, string-width@^4.2.0: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + string_decoder@^1.1.1: version "1.3.0" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" @@ -5594,6 +6306,13 @@ strip-ansi@^3.0.1: dependencies: ansi-regex "^2.0.0" +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + strip-final-newline@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" @@ -5626,7 +6345,7 @@ supports-color@^7.1.0: dependencies: has-flag "^4.0.0" -supports-color@^8.0.0: +supports-color@^8.0.0, supports-color@^8.1.1: version "8.1.1" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== @@ -5685,6 +6404,16 @@ terser@^5.16.8: commander "^2.20.0" source-map-support "~0.5.20" +throttleit@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-1.0.1.tgz#304ec51631c3b770c65c6c6f76938b384000f4d5" + integrity sha512-vDZpf9Chs9mAdfY046mcPt8fg5QSZr37hEH4TXYBnDF+izxgrbRGUAAaBvIk/fJm9aOFCGFd1EsNg5AZCbnQCQ== + +through@^2.3.8: + version "2.3.8" + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== + thunky@^1.0.2: version "1.1.0" resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d" @@ -5695,6 +6424,11 @@ tiny-warning@^1.0.2: resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== +tmp@~0.2.1: + version "0.2.3" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.3.tgz#eb783cc22bc1e8bebd0671476d46ea4eb32a79ae" + integrity sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w== + to-fast-properties@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" @@ -5712,6 +6446,16 @@ toidentifier@1.0.1: resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== +tough-cookie@^4.1.3: + version "4.1.3" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.3.tgz#97b9adb0728b42280aa3d814b6b999b2ff0318bf" + integrity sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw== + dependencies: + psl "^1.1.33" + punycode "^2.1.1" + universalify "^0.2.0" + url-parse "^1.5.3" + tr46@~0.0.3: version "0.0.3" resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" @@ -5727,6 +6471,28 @@ tslib@^2.0.3: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01" integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== +tslib@^2.1.0: + version "2.6.2" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" + integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== + +tunnel-agent@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" + integrity sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w== + dependencies: + safe-buffer "^5.0.1" + +tweetnacl@^0.14.3, tweetnacl@~0.14.0: + version "0.14.5" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" + integrity sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA== + +type-fest@^0.21.3: + version "0.21.3" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" + integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== + type-is@~1.6.18: version "1.6.18" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" @@ -5783,6 +6549,16 @@ unicode-property-aliases-ecmascript@^2.0.0: resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.0.0.tgz#0a36cb9a585c4f6abd51ad1deddb285c165297c8" integrity sha512-5Zfuy9q/DFr4tfO7ZPeVXb1aPoeQSdeFMLpYuFebehDAhbuevLs5yxSZmIFN1tP5F9Wl4IpJrYojg85/zgyZHQ== +universalify@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0" + integrity sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg== + +universalify@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d" + integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== + unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" @@ -5796,6 +6572,11 @@ unset-value@^2.0.1: has-value "^2.0.2" isobject "^4.0.0" +untildify@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/untildify/-/untildify-4.0.0.tgz#2bc947b953652487e4600949fb091e3ae8cd919b" + integrity sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw== + update-browserslist-db@^1.0.11: version "1.0.11" resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz#9a2a641ad2907ae7b3616506f4b977851db5b940" @@ -5811,6 +6592,14 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" +url-parse@^1.5.3: + version "1.5.10" + resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.10.tgz#9d3c2f736c1d75dd3bd2be507dcc111f1e2ea9c1" + integrity sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ== + dependencies: + querystringify "^2.1.1" + requires-port "^1.0.0" + url-search-params-polyfill@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/url-search-params-polyfill/-/url-search-params-polyfill-7.0.1.tgz#b900cd9a0d9d2ff757d500135256f2344879cbff" @@ -5867,6 +6656,15 @@ vary@~1.1.2: resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= +verror@1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" + integrity sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw== + dependencies: + assert-plus "^1.0.0" + core-util-is "1.0.2" + extsprintf "^1.2.0" + video.js@7.21.4, "video.js@^6 || ^7": version "7.21.4" resolved "https://registry.yarnpkg.com/video.js/-/video.js-7.21.4.tgz#362a2549467434b27507e0420b30eb4758feb128" @@ -6101,6 +6899,24 @@ wildcard@^2.0.0: resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.1.tgz#5ab10d02487198954836b6349f74fff961e10f67" integrity sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ== +wrap-ansi@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" + integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" @@ -6130,3 +6946,11 @@ yaml@^1.10.0: version "1.10.2" resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== + +yauzl@^2.10.0: + version "2.10.0" + resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" + integrity sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g== + dependencies: + buffer-crc32 "~0.2.3" + fd-slicer "~1.1.0" From 45733b1126242aef15898100ba15a374dc81d190 Mon Sep 17 00:00:00 2001 From: cjcolvar Date: Wed, 27 Mar 2024 16:14:12 -0400 Subject: [PATCH 002/152] Restore login credentials --- spec/cypress/cypress.config.js | 7 +++++++ spec/cypress/support/index.js | 36 ---------------------------------- 2 files changed, 7 insertions(+), 36 deletions(-) delete mode 100644 spec/cypress/support/index.js diff --git a/spec/cypress/cypress.config.js b/spec/cypress/cypress.config.js index b5d4cacd51..479d710455 100644 --- a/spec/cypress/cypress.config.js +++ b/spec/cypress/cypress.config.js @@ -1,6 +1,13 @@ const { defineConfig } = require("cypress"); module.exports = defineConfig({ + env: { + "USERS_ADMINISTRATOR_EMAIL": "administrator@example.com", + "USERS_ADMINISTRATOR_PASSWORD": "password", + "USERS_USER_EMAIL": "user@example.com", + "USERS_USER_PASSWORD": "password", + "MEDIA_OBJECT_ID": "123456789" + }, downloadsFolder: "spec/cypress/downloads", fixturesFolder: "spec/cypress/fixtures", screenshotsFolder: "spec/cypress/screenshots", diff --git a/spec/cypress/support/index.js b/spec/cypress/support/index.js deleted file mode 100644 index 22b9ec0ca2..0000000000 --- a/spec/cypress/support/index.js +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2011-2024, 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 --- -*/ - -// *********************************************************** -// This example support/index.js is processed and -// loaded automatically before your test files. -// -// This is a great place to put global configuration and -// behavior that modifies Cypress. -// -// You can change the location of this file or turn off -// automatically serving support files with the -// 'supportFile' configuration option. -// -// You can read more here: -// https://on.cypress.io/configuration -// *********************************************************** - -// Import commands.js using ES2015 syntax: -import './commands' - -// Alternatively you can use CommonJS syntax: -// require('./commands') From 0896ee53de7c2679ce14e1a5e3a7fddc3baab5f4 Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Tue, 19 Mar 2024 14:28:04 -0400 Subject: [PATCH 003/152] Set timespan off full item duration for top level sections --- app/assets/javascripts/ramp_utils.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/ramp_utils.js b/app/assets/javascripts/ramp_utils.js index 438053825b..5009a84f3f 100644 --- a/app/assets/javascripts/ramp_utils.js +++ b/app/assets/javascripts/ramp_utils.js @@ -110,10 +110,15 @@ function getTimelineScopes() { let parent = currentStructureItem.closest('ul').closest('li'); while (parent.length > 0) { let next = parent.closest('ul').closest('li'); + let begin = 0; + let end = ''; let tracks = parent.find('li a'); trackCount = tracks.length; - let begin = parseFloat(tracks[0].hash.split('#t=').reverse()[0].split(',')[0]) || 0; - let end = parseFloat(tracks[trackCount - 1].hash.split('#t=').reverse()[0].split(',')[1]) || ''; + // Only assign begin/end when structure item is a subsection, not a top level section + if (next.length > 0) { + begin = parseFloat(tracks[0].hash.split('#t=').reverse()[0].split(',')[0]) || 0; + end = parseFloat(tracks[trackCount - 1].hash.split('#t=').reverse()[0].split(',')[1]) || ''; + } streamId = tracks[0].pathname.split('/').reverse()[0]; let label = parent[0].dataset.label; scopes.push({ From 6c36c8f73d9aad4d78f6955498db332b9637e425 Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Thu, 4 Apr 2024 16:29:33 -0400 Subject: [PATCH 004/152] Serialize captions as both supplementing annotation and rendering --- .../iiif_supplemental_file_behavior.rb | 20 ++++++++++++------- spec/models/iiif_canvas_presenter_spec.rb | 6 +++--- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/app/models/concerns/iiif_supplemental_file_behavior.rb b/app/models/concerns/iiif_supplemental_file_behavior.rb index 7e43a9b61d..c9a6e4c433 100644 --- a/app/models/concerns/iiif_supplemental_file_behavior.rb +++ b/app/models/concerns/iiif_supplemental_file_behavior.rb @@ -16,14 +16,20 @@ module IiifSupplementalFileBehavior private def supplemental_files_rendering(object) - object.supplemental_files(tag: nil).collect do |sf| - { - "@id" => object_supplemental_file_url(object, sf), - "type" => determine_rendering_type(sf.file.content_type), - "label" => { "en" => [sf.label] }, - "format" => sf.file.content_type - } + tags = ['caption', nil] + supplemental_files = [] + tags.each do |tag| + supplemental_files += object.supplemental_files(tag: tag).collect do |sf| + { + "@id" => object_supplemental_file_url(object, sf), + "type" => determine_rendering_type(sf.file.content_type), + "label" => { "en" => [sf.label] }, + "format" => sf.file.content_type + } + end end + + supplemental_files end def object_supplemental_file_url(object, supplemental_file) diff --git a/spec/models/iiif_canvas_presenter_spec.rb b/spec/models/iiif_canvas_presenter_spec.rb index 051d320fa0..b8523bb2be 100644 --- a/spec/models/iiif_canvas_presenter_spec.rb +++ b/spec/models/iiif_canvas_presenter_spec.rb @@ -215,14 +215,14 @@ describe '#sequence_rendering' do subject { presenter.sequence_rendering } - it 'includes supplemental files' do + it 'includes supplemental files and captions' do expect(subject.any? { |rendering| rendering["@id"] =~ /supplemental_files\/#{supplemental_file.id}/ }).to eq true + expect(subject.any? { |rendering| rendering["@id"] =~ /supplemental_files\/#{caption_file.id}/ }).to eq true end - it 'does not include waveform, transcripts, or captions' do + it 'does not include waveform or transcripts' do expect(subject.any? { |rendering| rendering["label"]["en"] == ["waveform.json"] }).to eq false expect(subject.any? { |rendering| rendering["@id"] =~ /supplemental_files\/#{transcript_file.id}/ }).to eq false - expect(subject.any? { |rendering| rendering["@id"] =~ /supplemental_files\/#{caption_file.id}/ }).to eq false end end From 8278cb547ed31387cb6c1fc37585f379c577b930 Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Fri, 29 Mar 2024 17:15:05 -0400 Subject: [PATCH 005/152] Add 'Has Captions' and 'Has Transcripts' facets --- app/controllers/catalog_controller.rb | 2 ++ app/helpers/application_helper.rb | 4 +++ app/models/concerns/master_file_behavior.rb | 4 +++ app/models/concerns/media_object_behavior.rb | 8 ++++++ app/models/master_file.rb | 5 ++++ app/models/media_object.rb | 2 ++ spec/controllers/catalog_controller_spec.rb | 22 +++++++++++++++ spec/helpers/application_helper_spec.rb | 10 +++++++ spec/models/master_file_spec.rb | 14 +++++++++- spec/models/media_object_spec.rb | 29 ++++++++++++++++++++ 10 files changed, 99 insertions(+), 1 deletion(-) diff --git a/app/controllers/catalog_controller.rb b/app/controllers/catalog_controller.rb index dd9b3adb7f..be83708bda 100644 --- a/app/controllers/catalog_controller.rb +++ b/app/controllers/catalog_controller.rb @@ -88,6 +88,8 @@ class CatalogController < ApplicationController config.add_facet_field 'collection_ssim', label: 'Collection', limit: 5 config.add_facet_field 'unit_ssim', label: 'Unit', limit: 5 config.add_facet_field 'language_ssim', label: 'Language', limit: 5 + config.add_facet_field 'has_captions_bsi', label: 'Has Captions', helper_method: :display_has_caption_or_transcript + config.add_facet_field 'has_transcripts_bsi', label: 'Has Transcripts', helper_method: :display_has_caption_or_transcript # Hide these facets if not a Collection Manager config.add_facet_field 'workflow_published_sim', label: 'Published', limit: 5, if: Proc.new {|context, config, opts| Ability.new(context.current_user, context.user_session).can? :create, MediaObject}, group: "workflow" config.add_facet_field 'avalon_uploader_ssi', label: 'Created by', limit: 5, if: Proc.new {|context, config, opts| Ability.new(context.current_user, context.user_session).can? :create, MediaObject}, group: "workflow" diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 53d2e26dbc..a5f1419f36 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -104,6 +104,10 @@ def display_metadata(label, value, default=nil) content_tag(:dt, label) + contents end + def display_has_caption_or_transcript value + value = value == "true" ? 'Yes' : 'No' + end + def search_result_label item if item['title_tesi'].present? label = truncate(item['title_tesi'], length: 100) diff --git a/app/models/concerns/master_file_behavior.rb b/app/models/concerns/master_file_behavior.rb index 9423a1bac5..e4420c7b09 100644 --- a/app/models/concerns/master_file_behavior.rb +++ b/app/models/concerns/master_file_behavior.rb @@ -171,6 +171,10 @@ def supplemental_file_captions supplemental_files(tag: 'caption') end + def supplemental_file_transcripts + supplemental_files(tag: 'transcript') + end + # Supplies the route to the master_file as an rdf formatted URI # @return [String] the route as a uri # @example uri for a mf on avalon.iu.edu with a id of: avalon:1820 diff --git a/app/models/concerns/media_object_behavior.rb b/app/models/concerns/media_object_behavior.rb index 498522fbae..bc7084d779 100644 --- a/app/models/concerns/media_object_behavior.rb +++ b/app/models/concerns/media_object_behavior.rb @@ -57,6 +57,14 @@ def access_text "This item is accessible by: #{actors.join(', ')}." end + def has_captions + master_files.any? { |mf| mf.has_captions? } + end + + def has_transcripts + master_files.any? { |mf| mf.has_transcripts? } + end + # CDL methods def lending_status Checkout.active_for_media_object(id).any? ? "checked_out" : "available" diff --git a/app/models/master_file.rb b/app/models/master_file.rb index 6ae184a631..1a6d4e1240 100644 --- a/app/models/master_file.rb +++ b/app/models/master_file.rb @@ -492,6 +492,10 @@ def has_captions? !captions.empty? || !supplemental_file_captions.empty? end + def has_transcripts? + supplemental_file_transcripts.present? + end + def has_waveform? !waveform.empty? end @@ -508,6 +512,7 @@ def to_solr *args super.tap do |solr_doc| solr_doc['file_size_ltsi'] = file_size if file_size.present? solr_doc['has_captions?_bs'] = has_captions? + solr_doc['has_transcripts?_bs'] = has_transcripts? solr_doc['has_waveform?_bs'] = has_waveform? solr_doc['has_poster?_bs'] = has_poster? solr_doc['has_thumbnail?_bs'] = has_thumbnail? diff --git a/app/models/media_object.rb b/app/models/media_object.rb index 09bc1c4f6f..368bb39d1e 100644 --- a/app/models/media_object.rb +++ b/app/models/media_object.rb @@ -236,6 +236,8 @@ def fill_in_solr_fields_that_need_master_files(solr_doc) solr_doc['section_id_ssim'] = ordered_master_file_ids solr_doc["other_identifier_sim"] += master_files.collect {|mf| mf.identifier.to_a }.flatten solr_doc["date_digitized_ssim"] = master_files.collect {|mf| mf.date_digitized }.compact.map {|t| Time.parse(t).strftime "%F" } + solr_doc["has_captions_bsi"] = has_captions + solr_doc["has_transcripts_bsi"] = has_transcripts solr_doc["section_label_tesim"] = section_labels solr_doc['section_physical_description_ssim'] = section_physical_descriptions solr_doc['all_comments_ssim'] = all_comments diff --git a/spec/controllers/catalog_controller_spec.rb b/spec/controllers/catalog_controller_spec.rb index 8ab264d255..d61d0664f9 100644 --- a/spec/controllers/catalog_controller_spec.rb +++ b/spec/controllers/catalog_controller_spec.rb @@ -232,6 +232,28 @@ end end + describe "facet fields" do + let(:media_object) { FactoryBot.create(:fully_searchable_media_object, :with_master_file, :with_completed_workflow, avalon_uploader: 'archivist1', governing_policies: [lease]) } + let(:lease) { FactoryBot.create(:lease, inherited_read_groups: ['ExternalGroup']) } + before(:each) do + MediaObjectIndexingJob.perform_now(media_object.id) + end + ["avalon_resource_type_ssim", "creator_ssim", "date_sim", "genre_ssim", "series_ssim", "collection_ssim", "unit_ssim", "language_ssim", "has_captions_bsi", "has_transcripts_bsi", + "workflow_published_sim", "avalon_uploader_ssi", "read_access_group_ssim", "read_access_virtual_group_ssim", "date_digitized_ssim", "date_ingested_ssim"].each do |field| + it "should facet results on #{field}" do + query = Array(media_object.to_solr(include_child_fields:true)[field]).first + # The following line is to check that the test is using a valid solr field name + # since an incorrect one will lead to an empty query resulting in a false positive below + expect(query.to_s).not_to be_empty + get :index, params: { 'f' => { field => [query] } } + expect(response).to be_successful + expect(response).to render_template('catalog/index') + expect(assigns(:response).documents.count).to eq 1 + expect(assigns(:response).documents.map(&:id)).to contain_exactly(media_object.id) + end + end + end + describe "gated discovery" do context "with bad ldap groups" do let(:ldap_groups) { ['good-group', 'bad group'] } diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index c37e0fcf3b..8f517d29e9 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -163,6 +163,16 @@ end end + describe "#display_has_caption_or_transcript" do + it "should return 'Yes' for 'true'" do + expect(helper.display_has_caption_or_transcript("true")).to eq("Yes") + end + + it "should return 'No' for 'false'" do + expect(helper.display_has_caption_or_transcript("false")).to eq("No") + end + end + describe "#pretty_time" do it 'returns a formatted time' do expect(helper.pretty_time(0)).to eq '00:00:00.000' diff --git a/spec/models/master_file_spec.rb b/spec/models/master_file_spec.rb index 70ffa27e91..388f77cfb9 100644 --- a/spec/models/master_file_spec.rb +++ b/spec/models/master_file_spec.rb @@ -721,7 +721,7 @@ let(:caption_file) { FactoryBot.create(:supplemental_file, :with_caption_file, :with_caption_tag) } let(:transcript_file) { FactoryBot.create(:supplemental_file, :with_transcript_tag) } let(:master_file) { FactoryBot.create(:master_file, supplemental_files: [caption_file, transcript_file]) } - it 'has a caption' do + it 'returns only caption files' do expect(master_file.supplemental_file_captions).to_not be_empty expect(master_file.supplemental_file_captions).to all(be_kind_of(SupplementalFile)) expect(master_file.supplemental_file_captions).to_not include(transcript_file) @@ -729,6 +729,18 @@ end end + describe 'supplemental_file_transcripts' do + let(:caption_file) { FactoryBot.create(:supplemental_file, :with_caption_file, :with_caption_tag) } + let(:transcript_file) { FactoryBot.create(:supplemental_file, :with_transcript_tag) } + let(:master_file) { FactoryBot.create(:master_file, supplemental_files: [caption_file, transcript_file]) } + it 'returns only transcript files' do + expect(master_file.supplemental_file_transcripts).to_not be_empty + expect(master_file.supplemental_file_transcripts).to all(be_kind_of(SupplementalFile)) + expect(master_file.supplemental_file_transcripts).to include(transcript_file) + expect(master_file.supplemental_file_transcripts).to_not include(caption_file) + end + end + describe 'waveforms' do let(:master_file) { FactoryBot.create(:master_file) } it 'sets original_name to default value' do diff --git a/spec/models/media_object_spec.rb b/spec/models/media_object_spec.rb index ff67c818ed..dcffe4fd0c 100644 --- a/spec/models/media_object_spec.rb +++ b/spec/models/media_object_spec.rb @@ -1162,4 +1162,33 @@ expect(MediaObject.autocomplete('te', mo1.id)).to include({ id: 'Test 2', display: 'Test 2' }) end end + + describe "#has_captions" do + let(:captionless_media_object) { FactoryBot.create(:media_object, :with_master_file) } + let(:captioned_media_object) { FactoryBot.create(:media_object, master_files: [master_file1, master_file2]) } + let(:master_file1) { FactoryBot.create(:master_file) } + let(:master_file2) { FactoryBot.create(:master_file, :with_captions) } + it "returns false when child master files contain no captions" do + expect(captionless_media_object.has_captions).to be false + end + + it "returns true when any child master file contains a caption" do + expect(captioned_media_object.has_captions).to be true + end + end + + describe "#has_transcripts" do + let(:transcriptless_media_object) { FactoryBot.create(:media_object, :with_master_file) } + let(:transcript_media_object) { FactoryBot.create(:media_object, master_files: [master_file1, master_file2]) } + let(:master_file1) { FactoryBot.create(:master_file) } + let(:master_file2) { FactoryBot.create(:master_file, supplemental_files: [transcript]) } + let(:transcript) { FactoryBot.create(:supplemental_file, :with_transcript_tag) } + it "returns false when child master files contain no transcript" do + expect(transcriptless_media_object.has_transcripts).to be false + end + + it "returns true when any child master file contains a transcript" do + expect(transcript_media_object.has_transcripts).to be true + end + end end From 254bb48192b8637dc036f7ebdd479e445e975d28 Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Fri, 5 Apr 2024 13:05:09 -0400 Subject: [PATCH 006/152] Disable captions upload for audio items --- app/views/media_objects/_file_upload.html.erb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/views/media_objects/_file_upload.html.erb b/app/views/media_objects/_file_upload.html.erb index cc24a9183a..1a0348623a 100644 --- a/app/views/media_objects/_file_upload.html.erb +++ b/app/views/media_objects/_file_upload.html.erb @@ -155,7 +155,9 @@ Unless required by applicable law or agreed to in writing, software distributed <% end %>
- <%= render partial: "supplemental_files_upload", locals: { section: section, label: 'Captions', tag: 'caption' } %> + <% if section.is_video? %> + <%= render partial: "supplemental_files_upload", locals: { section: section, label: 'Captions', tag: 'caption' } %> + <% end %> <%= render partial: "supplemental_files_upload", locals: { section: section, label: 'Transcripts', tag: 'transcript' } %> From 07a4844e9ffc8d748f65d8ebbd5a04c5de44c1be Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Mon, 4 Mar 2024 12:57:37 -0500 Subject: [PATCH 007/152] Support creation of multiple captions in batch ingest --- app/jobs/ingest_batch_entry_job.rb | 1 + lib/avalon/batch/entry.rb | 42 ++++++++++++++---- lib/avalon/batch/manifest.rb | 13 +++++- .../example_batch_ingest/batch_manifest.xlsx | Bin 6099 -> 6662 bytes spec/lib/avalon/batch/entry_spec.rb | 32 +++++++++++-- 5 files changed, 75 insertions(+), 13 deletions(-) diff --git a/app/jobs/ingest_batch_entry_job.rb b/app/jobs/ingest_batch_entry_job.rb index 8c5e58cbfa..4ec4e94c40 100644 --- a/app/jobs/ingest_batch_entry_job.rb +++ b/app/jobs/ingest_batch_entry_job.rb @@ -69,6 +69,7 @@ def process_success(batch_entry, entry) old_media_object_id = batch_entry.media_object_pid batch_entry.media_object_pid = entry.media_object.id batch_entry.complete = true + batch_entry.current_status = 'Completed' batch_entry.save! # Delete pre-existing media object MediaObject.find(old_media_object_id).destroy if old_media_object_id.present? && MediaObject.exists?(old_media_object_id) diff --git a/lib/avalon/batch/entry.rb b/lib/avalon/batch/entry.rb index 57a983fb03..33b33a8b32 100644 --- a/lib/avalon/batch/entry.rb +++ b/lib/avalon/batch/entry.rb @@ -71,7 +71,7 @@ def self.from_json(json) json_hash = JSON.parse(json) opts = json_hash.except("fields", "files", "position") opts[:collection] = Admin::Collection.find(json_hash["collection"]) - self.new(json_hash["fields"].symbolize_keys, json_hash["files"].map(&:symbolize_keys!), opts.symbolize_keys, json_hash["position"], nil) + self.new(json_hash["fields"].deep_symbolize_keys, json_hash["files"].map(&:symbolize_keys!), opts.symbolize_keys, json_hash["position"], nil) end def user_key @@ -102,7 +102,10 @@ def media_object_fields note_type = mo_parameters.delete(:note_type) mo_parameters[:note] = note.zip(note_type).map{|a|{note: a[0],type: a[1]}} if note.present? - mo_parameters + # These fields are validated against the media object model. + # Because captions are stored on the master file, we need to + # remove them here to prevent an invalid manifest error. + mo_parameters.except(*gather_captions.keys) end def valid? @@ -176,17 +179,23 @@ def self.offset_valid?( offset ) true end - def self.attach_datastreams_to_master_file( master_file, filename ) + def self.attach_datastreams_to_master_file( master_file, filename, captions ) structural_file = "#{filename}.structure.xml" if FileLocator.new(structural_file).exist? master_file.structuralMetadata.content=FileLocator.new(structural_file).reader master_file.structuralMetadata.original_name = structural_file end - captions_file = "#{filename}.vtt" - if FileLocator.new(captions_file).exist? - master_file.captions.content=FileLocator.new(captions_file).reader - master_file.captions.mime_type='text/vtt' - master_file.captions.original_name = captions_file + captions.each do |c| + return unless c.present? + if c[:caption_file].present? && FileLocator.new(c[:caption_file]).exist? + filename = c[:caption_file].split('/').last + label = c[:caption_label].presence || filename + language = c[:caption_language].present? ? caption_language(c[:caption_language]) : Settings.caption_default.language + supplemental_file = SupplementalFile.new(label: label, tags: ['caption'], language: language) + supplemental_file.file.attach(io: FileLocator.new(c[:caption_file]).reader, filename: filename, content_type: 'text/vtt', identify: false) + supplemental_file.save + master_file.supplemental_files += [supplemental_file] + end end end @@ -198,7 +207,8 @@ def process! # master_file.save(validate: false) #required: need id before setting media_object # master_file.media_object = media_object files = self.class.gatherFiles(file_spec[:file]) - self.class.attach_datastreams_to_master_file(master_file, file_spec[:file]) + captions = gather_captions.values + self.class.attach_datastreams_to_master_file(master_file, file_spec[:file], captions) master_file.setContent(files, dropbox_dir: media_object.collection.dropbox_absolute_path) # Overwrite files hash with working file paths to pass to matterhorn @@ -263,11 +273,25 @@ def self.derivativePath(filename, quality) filename.dup.insert(filename.rindex('.'), ".#{quality}") end + def self.caption_language(language) + begin + LanguageTerm.find(language.capitalize).code + rescue LanguageTerm::LookupError + Settings.caption_default.language + end + end + private_class_method :caption_language + private def hidden !!opts[:hidden] end + + def gather_captions + [] unless @fields.keys.any? { |k| k.to_s.include?('caption') } + @fields.select { |f| f.to_s.include?('caption') } + end end end end diff --git a/lib/avalon/batch/manifest.rb b/lib/avalon/batch/manifest.rb index 786e47cb83..9b913c6bc8 100644 --- a/lib/avalon/batch/manifest.rb +++ b/lib/avalon/batch/manifest.rb @@ -22,6 +22,7 @@ class Manifest EXTENSIONS = ['csv','xls','xlsx','ods'] FILE_FIELDS = [:file,:label,:offset,:skip_transcoding,:absolute_location,:date_digitized] + CAPTION_FIELDS = [:caption_file, :caption_label, :caption_language] SKIP_FIELDS = [:collection] def_delegators :@entries, :each @@ -117,11 +118,22 @@ def create_entries! content=[] fields = Hash.new { |h,k| h[k] = [] } + caption_count = 0 @field_names.each_with_index do |f,i| unless f.blank? || SKIP_FIELDS.include?(f) || values[i].blank? if FILE_FIELDS.include?(f) content << {} if f == :file content.last[f] = f == :skip_transcoding ? true?(values[i]) : values[i] + elsif CAPTION_FIELDS.include?(f) + if f.to_s.include?('file') + caption_count += 1 + @caption_key = "caption_#{caption_count}".to_sym + fields[@caption_key] = {} + # Set file path to caption file + fields[@caption_key][f] = path_to(values[i]) + end + # Set caption metadata fields + fields[@caption_key][f] ||= values[i] else fields[f] << values[i] end @@ -140,7 +152,6 @@ def create_entries! entries << Entry.new(fields.select { |f| !FILE_FIELDS.include?(f) }, files, opts, index, self) end end - end end end diff --git a/spec/fixtures/dropbox/example_batch_ingest/batch_manifest.xlsx b/spec/fixtures/dropbox/example_batch_ingest/batch_manifest.xlsx index 4127ed5cc0b48419b8e1f043874cd3335372c03e..8b44e1003bb1fefa379ee47d622bced3c6c04ea2 100644 GIT binary patch literal 6662 zcmaJ_1yq#((q`!nX+&a40clV=1(sYEq(K^yltn@W>F(|hK`EuXR63PfS&+^J0ZD;x z`Fqcm>;0~?XWz5$IXm;d6TfF>9)Z&GYZmmFgxC^4J+sg*gujdy@en3t~>gf&#O!GBDks} z7o3+>#U$*Jei&*itA%$-&%os~Paff+e4mdb4-_FzavUx(8p?-*-(>m=yf&sE zh&j}9E0EX5)k}@ZTS*m>Lw-l#v2uh#_RoP;Qm_gZc2K%tIszGlVN?_p@c#@G5%M1% z_B`%RE_UWlPIlZL4)#%6Fqc_gV((pDk3+&X=exYA_(C4>X>iuUzW4T()!|&8g)Gvd z7snM=nZ*;5hk(M?_3fFtrIn$^WViqoXHB^N1N>Ahj;SE$#)MHfQ7vSEk~iT=rajK==%Eu1ygyg)vD;S80Mx?u+t=1gB1-E@E368jXHUDP}53 z!_EF2Vp$A9*+JxK%pa{8*-a7Qy^Ke^pB>c?D6xb0Nxik@8SO==*`+r8872eWqT0FF zo;pbb+2Xqyu)8QIhfl`x#VR53>4;Z-pzuLyHiiUHVpk$tbd1fZa}q5Z9N2!53g)Ya483VK~QJ9@@W8N6iP^OaRU;j)nUI#TfS zEdCv** zSRNi=H7*oK1Ilt9A%iSkWIlPiTl6(}u1>3Y(`x5>voV^>L zJOud3XdV=3skD)&ngvXu`Va)*+l(}J-{h5lfbZRADNu|{9LAyxpsi;#%^0bk6#l_b zFE`rJOa1m~O8!L=$wCss#H0Pn!18XQkIEi53VohXjQ;;&NqC z)3{L|WHf$x^yMKwtOifMPxcn6Dlt<{jTzo>ELV+Ya%GvLoxS#Ku6Rn1q;bmVoNmLh zp|FFMH@vWn??9e?rR_G72Siv z##WeAH2(5wsD*Gw4?XcK|6CtO(Z!{Swpuq3tbwvLLh+_mvw z`VPSjVtVAwS?0W6z)E5%8;_;tz{*=bReV~*ek(23+XS1;q_k3&aUfr4P9<^Vd6iJy zUIWM5U1a(GbmO)zY%O=_H>CCrU#IvK)f?GEGu!8?(-*9Tm@OHrko+QgKWa0$SzV~I zbBjcPu1B2FdaYI{l)4As9Fs2~)wst)@%*slP{FI{BaBmM$r}Pa%lYtNZl`%}(EaFp zf^q%#H_O%g71>gWm$OHQ60nw(nWJOYpO2c4x>9!TH49T4otIR!mJ?-YT``HWN$p&j z(0pEiq2`slp)(3m9xQnG zf%cde4ox*FPoP71+(z6QP#TfJ6WNj3PJ_>?O6bv>gh&CdPX&g{178>tOaYXCI~7d7 z0Tueh%E}eW^Q#N}bHjIiO#bOdQL!2-Xne#Uc74Km-31JZfaCT&kJdXQK0|T(wpm>T za6nC;opz1T@o>A0R_Avm;BM|}5eB7osWTl4M7g2HM{zc2^q;2A*`}l1VeSU@DG3zj zw#q&Y)Yz>lHa)ATEHGE@o_A?3YIQ1xHe8cgJE|wV_%V8~_)S~P$^!0#iG0vDPxRNN zw#C)P#w43nfQWV_Q(}lQFbCu5mg=1EpIe>lR5F&Pk*?x+_sq9RI_$0@nooTZSgwVR<5hN zI?}ymc$>rJo{a z_?w38rgZ4O?~uK&w20r`+2}m>4-!a*P$iig6Z(8JWbnYDks15KM#mo7NKb4}4){QA z+~3a}SeMI3z)Y|;GBm}H$rfR}-@DW=t(0PUN9Cc+RyF-R%939eN09|@pTLT_>XL`4 zhByqD>GmP&iysNU@gBo(cglr8XX$?l$t$eYPNrZtVHN;A(NUAmGf z&qux%di1X99%{jDgQ~RJt>X^`I+t2D=&NOcce{UVc|R>~N@bWTx|Q1{foCOHf>=aRihj@bpr`nZasg{lX zCboilYw>I>#^=L2b9U=y zOJ2c?<~hC3B(!3E-fV5qurJ?p;{x_o@tjQhKRrIGh)~sryM}ArX2#u?T*DZ_tX6O( zuGtqtwP|$28Cm6RqHglGDVR24? zB-yd(X$V`VWnxv6Kc;&2?f$Z;h=OWH=!8YjIcvUqWP?3$IGRLPEii=tyc0#H^x}yx zgT4(W_V|xk6qzm`e(BhEP&9OqmxV8bgVb#XrLR1g*bsNK*TRK}a)AHmRsreQrpE-t zg@}s*qqf$;mGc23I(*{9k8!rt`Qp*|cqE?$Y3@UP18Arzn2N=lJTPp!{SP(GqVQG5 zrL-rB?5!F|o^5pWTU1HeY%!ITMh1!TrTiXIFoyd1w+{kx-8 zhp8AP<{&k^JUx{gPEDZlek;xW&G6Msxz926PuNb`#BxykxabfM*UvPadWeJW>u6R? z-II)Nl+Jltq#tn)6L>2&LW+|Vgp!{vDR=L_FZGMH8@4}ovOda(sopjKOduk70v=8% zV@;K7iSa&cj>Kg+Nt~(}7!EOjFJ=NXpOJUruvmIhB;47v=96%zRb)-8OL~|XD0Qn% zoyBY;CU7}ET&L^)nYfmF98Pk-XYp;F&!40%#9@@1?2u4yrv<$6P}OqQ z7ISiZ9n1AZJ&_>S4yc1q`>CmVu}+&vnrw!{k$L10gosn=Eo_T)tyBOsWltT5PdVkB zw88wemgAwe*`k7(lW9pLp|3i#OY2gm3^R~Pbc&9QOz$-SOuB@oQg;Q?vVLU%MbzIQ{^_ zvWR@p8YxqGaRT_Lxj5P*96wom<->j35Z7vZG+}h=piSw=reM@&XpbB|W?4RZQ0E6X z>{h=E#LhQrouPdA!V{L5D*9V*T1cmTDM362?++g6bDMX?iWwL7DXW)G^)&_TXz~o~ zgC;ToL*!cJhG;d8oZx0?*Hx9p6anGm0+MSB9F)2GpPTjMh)HpI${+xqs~BcJL$B<9qFhk3tSXR-?^P;7 zW3fayz!cm78H}U+sy$m4M^5zeai!)8B!a}0E-~hF1lMADL<^~~blW?*Mgo~g^7;jL z^tRmHNC{W-Pf*=t>^M)&ud(%6qL8HU&8V5TF(dsE)Te}X(fvT%?^OKs z_J0SwI!btznkfn_ibhs9K28?NnFpZWWq7kjTqy@D+2-H zJ3GOjpVlgWdqh)dX7N)$a4PjI-_m=jn5Ay@nH0^g!=3Y_^QiKguJoXtY#O=yeYNim z@^Tf!u?0VIRuT95a80SZp;PDpYk}4o-*)Y0QL_iFdnOJwq6~`%df!eteA3+*z#!g;YUdekI1a?rE zu`ThO9=WYE+UttV7D1Lg9bSAP3ug;m>9o#xmvpWNPyUYZLbSh(3C4gOw~19@En4Fk z=$q(XSi*Ng5J{ZWxXbsQB5Y(vhSg)5Url){*j+8$zlMU?rbYEiWU&k4{|yRAkOgn) zWC3<@a)$C)IJsC|kICK;H4P*Jv>tGleQmBWP6OAtEy{(WCB#ypKEn&Uj|!q;p@UsY z*w!akhGpy=?{C}Kh{9;HUiuQ1Rg?EAQiD3e6*!v9db<}zC73|cjx1AALE+mBvHITp zn_ATG2wA%BdBi!3i4J>B@X{5PD zTxM~L9l^|4pu3o$`lBY&Sbu*^GzzpKMUjg|pa*HR>wmx3-`f5oIx-7n*C%PF_Jr=O}!%*$#D-`KIrRYZ3K`8Yl)!&N`g+G)u zTe-^_zIONGM;rOqPf?X4Ecg2erf7%_S5a!1ab!GJc;W969rQt|c8$Ti{)mc zX(xG{7@T))l*}V>g|-L0Pu46ZbFLHCQ9bDDf*jzN|B6t=Hxp*&?0lWCtXMUtu4{(v zocvKwE$D6}FcH%s-A#6u@Xl;`|1C;>A$nzq_T>fYlMd<}`}RqT^P(-y;~Bt9@Z{$w z;dqIDBsv`L-;C)VWb8V;YPXc?c+>t>ZfJq9{C&NRvKxdL!Mc~R)E^ZRj2Pt!^9|t)=UBf;-L7V*{ zj#{vsEG)d=rb(lxKa}8nZzBTQdP|&DV_M^Xv( zjj+eWaf9J~Te&aNQWC#*t1s_-uow3J_%QsS<5f(2M&&lqb>JADyuEvl3`^!;w;$4* zfwOoHb#-$1Cw9ZtxLv$tLEq*|uxDV}SaIVu5f8+)k7MC=0H{;1T7+q=;B>Gd*;IWIuxlMh|zTZ7%{6M76(Y&IFO30$+-pe%}XGa&&#H?dfa_$9J6RhV9>;P+wu7e&y9B*20DGXZ%GZJ#;}Pr zQNwMywfPiy4nhuT)Yt;-+xQmZN8fyL+^|H9btD@fhioa_@6dFC9X>4O%m1kDuSws}tOMd4mR%9`<^1rf zL8X4!S$a~Ug6yO5&vH}k?{d<;sa)`SCOAQj+@@@Yc+fM$PR2CNE&5V?f@L9A#)dU< z2j#8smrt5KCSLe8e}d;19~kj2q~EjhHMQXaBa~5iIE`La7s}olb@$Qo+cIory-?hz zS*RneBr15a68U~!^kfq83}5ULO)~xDJj1yd&=7C1Fai&zEIz(7y@uE)*%NpZh3AwI zw50ZOC3^icIQ{;7TwwXQ+^w-_Uj z^uN{r1@V7s-^7M%1o$ngYa(DQD00pdqfy_G!3Lf$^hHO#v KNG=Wq<^KSSr$H$I delta 5345 zcmZu#c{o(<`^U%@#xj;6j3tBY*+SU|BgPVuT^dW4tPwgiPEZIf&eaV(BB(m>Y zA~Zt8pdVe=`@X&3_xGGX&VA1FIoEZb&wW4l{e12aiBBTbx>_J&2mu)x89`v~7i26o z8!(6jiP2zi0bwzI4{I1yZ7Z8YKfcfic#_>+3sLBBG9&{jxKt4>nkv@82Ha{$PaU0pET9dgPtc9xTp; zI^aWyTy3I74XPR#X0qrsI*B|y^wS~Z8#66m2z5)VyHy#H+~Or)k8w{^p#So$+K%lEdN``(NRJ%YxKp;m%aQ6Hs0R#kCXL=YQhUyTd34Zuk z&9+=h=j8%V0+xbY%rJ-(R?|~1@-n50nB?TuOLGHkC5dh-R7g_5c=V zwzS^5U<}_Z;R^?SqVxzrMko`+Yl09fCafZ^F9#I&v}V#ffLFtzIRDe;mWiGXV^^XK zziP!KF@J{5p(zt`cAG<&QUs}pf`nNfPozi7?V~Dc4oNoqlo>%vu`PJ$6Ngv@TSFb< z1T_myeR7z4KYxTaf6uKR#~kgeqm=XMHb0Nve!F@Ra-I_YFFDu-S}q{Y z64e2Nbk3hZsBl76c=}IOf;yGSYjpH>5^rhiHbNCytv2?za=nTSnC9zV&R?GaK`NV# zhNz3N)%<#?G?2i>>XNT4|aE|9s7p0%pxc}DAw(o zE3)eQ+g>qme^6>Y4-IeV#+2i#3gB>MM)b5zpcTq#P~3L4O}-<|dWJG;&7S|K)uhv# zuT;d{nTk)pQ|D`jwU~g78pElY-4TxKU@9|J)RH1D?NV!vGuC?~+xisJBE*q_FDRFo zHFQ&~lA(_CxY++RG)U>xYev(TZFcSrV+-YPr#0;D}X7xpzrAB74{Hw>}^55Nfs|B03g`11 z-%dF0(Ye2~ol)!6NRO-JgFk&tcTsNgT9L*dS1NM|-6U2m(50gw*W=DrOX>ua$TxN--RA+h0OImhbnzrIjR<#wc{s5eQ=w zABd#TN@~|4Ya{31?F!zG_p|bNM-mou#2-fF8EO$$4asw#8RN3sUB4}Fx-HpL_O)w# zVdCY7gDeOQ&`aT4Gc#3?D;JaV*2)7M_U0R7#!i}e;sQk6pn?IBFH%>+VxjiaxmhjN zqc`tKJ^dK=ReS3F{NXw5}U@g0%jEh1r-BlKd<>i>5BECIFHs)GIgf==zGt zn7yqET=3?H#yhWwa6aq~^#^$ z%mW}3LY3WNU#*A$+p+WM3VdAXQEupfworh^X1qG9U=UI$)k^Co9gpf!A&5mb<@SJ9A&nVM#)c& z@S%+Sj^)ZZ6u1$5Gx2jS#<-D=$}1b>5kR;mY*iD$$Dk?f;V@Yd;T-5V+bK*o?dBh} z|Ke4?T%PQs?CBxEwMsU?@EF|K@8S9L&Wt4YcMe9HXCwMK;fl_tF_GzamD`k^-Kry6 z>y^Qwl9@lgn3cSf8a5GxPRH~eaR-Q4=r_;WYD+o9=q!}Pud|-7I7>{3aquiPYW*iQ zp1u5gFrQgIP9oTPQZ67ovTfz=6Y9X>Ds3Vm%}@Ke+-0q)6N#mQnd_x3R}Qh&t9fbD z{2ih-o>hMPo5%?{{~y<*ia`v}<=&oe-e%KTy&(w#h+> zO3TP!EbR3@@0F3c7f9lqY%%(INl{p5Wfp94he2vz%?5#TS&Ga4$Xh~;f==PBYk{jyCHMine__H&w!sAtL*9hNRj-uF`VuL= zAZ7B8C#eF4fyu5(SCAt|X z6hpZ2O&-A9!@;kmHu&AdllBVNvY9O}VPW}}W9r8PJDFwjqQ?6(I=KEfZ(DjMuY8ul zZ@K&AvZw3OlrA??Ew!KWxMj_NG;n ztxQz#?#}k^Hv6l9C5GkiSSxzIPns%9@g~1NYV8X!H>!NeoL~qoy0j=oGi&e;b7^rs z6BZ&sgKHuB4tT&Ul(O>HUnx$7&hgt!A@)$Tl~U~rYEAoh+^KYEccqp#A>Z>NWfauR zYH6ABLu;K?neOeU5eDhw=LnQ4=9pm`et%(qjq5 zmcmq z-!jCe5E>+n_G*scv2eXoHgPc< z?RA+PRAw>RA^~^P7P(Sc1P@o=9RcFyr6>El+)FnOEafwjoUdFrtI=6*=JMAmBnU#8 zEX%qY6}%A=hHgKr_40`V8z&-Gp!Sua7P3*?%eP)2RggihV)O90vK7;t;+kskRnn7@ z`Mns5#2&TRa-xZMI^)b|2Z4#vW=i>rtkDeTo(uT4HbaSAav8^_$7}^LPM^~G?nu?D zZZgY6d3JYfy*yN-&uNO4AH~Vt7v_vT5+BiqT-a`$^%yU`QB(cdEH(ctJw)5L_{c8h>l)9k z7ccKFeSTY@e^m>599anKdpW|*nlAp@Qcx-1Y`#8`PQa-#xaqRKT*0-3j&PVyK!Nm( zPtQ{r&Rr=@vSd-bHW|!+dtBS@f$5LS-M@&=6Syo8D;{})fM6Q(pNjPW!~vM1rbU?p z=1a?tE#eYuTH}e`HY6Ni#A;I#`wP4}Hj5LnTgrW<8EkL|eXH!*Du$t^i5rLhZ?Vf- zf_|9Fh#)^fp2dF5EE^M1zcM>*;?Hfg{MStt7=sOM)=O)S59QA=ZKSBz*E+}fyD02Q zcmt9J5&+Lj&^#Fxcg&aMbwFB-398?~Pp7_0l&z7f1NC}TBIhZt%1kLQHGoKogRL0k z`A-yaT7?hBj!A8*Sgq^$09ucM4Jtf^*a6PKudYLirt%K4A=ih|-h}nhr zgX{xavC#|cdIKX@7di9I2x4#g%dpgU8my>6Ypmg+anE5S5qgLt8r3B7S?#SkAVGVu;&dt}OLSS4&v}3bL~Ici-yRg{35x&mU4ESV!=8V& zgxZX27JcCA>|Ey1D?ZOTa;8`d4#QR9Vi*l={DY7+?Qb2-de5z?(soFV0t40EiY3Zn znvTd$kM0=0&a38yz)|u@VtvWl9%tU(lweKi_oWSUZQ1Y)VQM3ZJr=2{zSs8zSosFm z{oheNr!z1k>Q>XZD4WG&4YiJ>1 z_EF}5pC#)HgHvu6X@wuZY{*#oL~9*yG>Im?tXiD0MHX-sI|UmQDy&S{M4I(Uz~%dN zJBQh-?7eU25YEqXNRF1NNty*Z3J{|go6uAD#eUqjb6lTtE#GQR-=YiGjrP&@{K&=9 zbhAq4;#!**Bhy+N>U>M|Hz_P`ogLzWnFzrDBZje(gfKu~+jUx$vKe=qtU;va6F5gH zKFeOO3KL+pHFZ^JsGP+_oQK!tU_vo5XCpV1<>}xBj$>MR{h7)9FIXfo5-FfhD8bu#k5Q`jz^i2E7gG zJL?%pVKhL-TOB%5Z)1`8MU8Z<57Z%k3RbsgDf-U4W{ydx=AmVyDMG#3!kU$`Y;3Pv zX(-ME!BO`tpMI(^Zs|K^SEG8=xZzrVrk zDXSKIz-gKB=2aq-&*KW2H1~)0(n9?81;VTPG$RUKO8mY?JC$8ss`Z*^LhlOoZ_)^- zf}aa~I49ZQLC}Cmk)A-tL5SQJK$&vOxuW`Rr<(&b^Z=a>g|pNY`JN_sw+sht2)YY=pD054$rJ&e!({3CBVdI z^VSNt@IBo}xrs25z8$0^j;k)Y8(87w9R)3aWa&XuEH{76WZZ$ix@?k@-{Yb{{M_Us zgYy0Au$c@qx!v$Ya8AzV{etp9;X1{h7R{~==#gSvWcaOzkPjk^Mj*c?Z$W;m4kU;d z>_pBibg65MebMKq%UI5hm^ufH{k(jMMU&;_uh(qQqLLJC@`x4lEI?hG5Kw|T5^$lm zJN!@_4to~rDvT%A{#`t8y|z1VShHlD;$3)LOl1xY_wq=2%1m3ZC4?>EIwsQF>Is{G zA@<=p!IP=D?dxX*PZFOO4MHLa!9NYNKRZbm|5gBh8J6z%u)0hT*tuLk@2mXPR-yR| z@cRxW#F|0be}n#1{$aq)b`<{;j_uhXAWoi7ibfNZKk?}EoG+s4LlQQ zf9tceg7?QDxqzK!=K2l#Q`2z&0U;o0BE=pviTwusSvSw+%|By?46Dt|`y2E}CLy5u z2XSVVQe)egSk2FA_%m9;{{oGlE%;0}J#=-yZ{zBEU)aal>3`Y&(-ZXn`nZ%HI}K%{ RrXZjoh&iK0h~a$p{{U5*b#edz diff --git a/spec/lib/avalon/batch/entry_spec.rb b/spec/lib/avalon/batch/entry_spec.rb index 3ca61a1c18..66453fdd22 100644 --- a/spec/lib/avalon/batch/entry_spec.rb +++ b/spec/lib/avalon/batch/entry_spec.rb @@ -176,20 +176,46 @@ expect(master_file.absolute_location).to eq(Avalon::FileResolver.new.path_to(master_file.file_location)) expect(master_file.date_digitized).to eq('2015-10-30T00:00:00Z') end + + context 'with caption files' do + let(:caption_file) { File.join(Rails.root, 'spec/fixtures/dropbox/example_batch_ingest/assets/sheephead_mountain.mov.vtt')} + let(:caption) {{ :caption_file => caption_file, :caption_label => 'Sheephead Captions', :caption_language => 'English' }} + let(:entry_fields) {{ title: Faker::Lorem.sentence, date_issued: "#{DateTime.now.strftime('%F')}", caption_1: caption }} + + it 'adds captions to masterfile' do + expect(master_file.supplemental_file_captions).to be_present + end + end end describe '#attach_datastreams_to_master_file' do - let(:master_file) { FactoryBot.build(:master_file) } + let(:master_file) { FactoryBot.create(:master_file) } let(:filename) { File.join(Rails.root, 'spec/fixtures/dropbox/example_batch_ingest/assets/sheephead_mountain.mov') } + let(:caption_file) { File.join(Rails.root, 'spec/fixtures/dropbox/example_batch_ingest/assets/sheephead_mountain.mov.vtt')} + let(:caption) { [{ :caption_file => caption_file, :caption_label => 'Sheephead Captions', :caption_language => 'English' }] } + before do - Avalon::Batch::Entry.attach_datastreams_to_master_file(master_file, filename) + Avalon::Batch::Entry.attach_datastreams_to_master_file(master_file, filename, caption) end it 'should attach structural metadata' do expect(master_file.structuralMetadata.has_content?).to be_truthy end it 'should attach captions' do - expect(master_file.captions.has_content?).to be_truthy + expect(master_file.supplemental_file_captions).to be_present + end + + context 'with multiple captions' do + let(:caption) { [{ :caption_file => caption_file, :caption_label => 'Sheephead Captions', :caption_language => 'english' }, + { :caption_file => caption_file, :caption_label => 'Second Caption', :caption_language => 'fre' }] } + it 'should attach all captions to master file' do + expect(master_file.supplemental_file_captions).to be_present + expect(master_file.supplemental_file_captions.count).to eq 2 + expect(master_file.supplemental_file_captions[0].label).to eq 'Sheephead Captions' + expect(master_file.supplemental_file_captions[1].label).to eq 'Second Caption' + expect(master_file.supplemental_file_captions[0].language).to eq 'eng' + expect(master_file.supplemental_file_captions[1].language).to eq 'fre' + end end end From 469e0171a12d346cafe8d5697b263eab74f5106a Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Tue, 5 Mar 2024 09:16:19 -0500 Subject: [PATCH 008/152] Restrict caption files to individual masterfiles --- lib/avalon/batch/entry.rb | 17 ++++------ lib/avalon/batch/manifest.rb | 31 ++++++++++-------- .../example_batch_ingest/batch_manifest.xlsx | Bin 6662 -> 6661 bytes spec/lib/avalon/batch/entry_spec.rb | 2 +- 4 files changed, 26 insertions(+), 24 deletions(-) diff --git a/lib/avalon/batch/entry.rb b/lib/avalon/batch/entry.rb index 33b33a8b32..b91f54ade5 100644 --- a/lib/avalon/batch/entry.rb +++ b/lib/avalon/batch/entry.rb @@ -71,7 +71,7 @@ def self.from_json(json) json_hash = JSON.parse(json) opts = json_hash.except("fields", "files", "position") opts[:collection] = Admin::Collection.find(json_hash["collection"]) - self.new(json_hash["fields"].deep_symbolize_keys, json_hash["files"].map(&:symbolize_keys!), opts.symbolize_keys, json_hash["position"], nil) + self.new(json_hash["fields"].symbolize_keys, json_hash["files"].map(&:deep_symbolize_keys!), opts.symbolize_keys, json_hash["position"], nil) end def user_key @@ -102,10 +102,7 @@ def media_object_fields note_type = mo_parameters.delete(:note_type) mo_parameters[:note] = note.zip(note_type).map{|a|{note: a[0],type: a[1]}} if note.present? - # These fields are validated against the media object model. - # Because captions are stored on the master file, we need to - # remove them here to prevent an invalid manifest error. - mo_parameters.except(*gather_captions.keys) + mo_parameters end def valid? @@ -192,7 +189,7 @@ def self.attach_datastreams_to_master_file( master_file, filename, captions ) label = c[:caption_label].presence || filename language = c[:caption_language].present? ? caption_language(c[:caption_language]) : Settings.caption_default.language supplemental_file = SupplementalFile.new(label: label, tags: ['caption'], language: language) - supplemental_file.file.attach(io: FileLocator.new(c[:caption_file]).reader, filename: filename, content_type: 'text/vtt', identify: false) + supplemental_file.file.attach(io: FileLocator.new(c[:caption_file]).reader, filename: filename) supplemental_file.save master_file.supplemental_files += [supplemental_file] end @@ -207,7 +204,7 @@ def process! # master_file.save(validate: false) #required: need id before setting media_object # master_file.media_object = media_object files = self.class.gatherFiles(file_spec[:file]) - captions = gather_captions.values + captions = gather_captions(file_spec).values self.class.attach_datastreams_to_master_file(master_file, file_spec[:file], captions) master_file.setContent(files, dropbox_dir: media_object.collection.dropbox_absolute_path) @@ -288,9 +285,9 @@ def hidden !!opts[:hidden] end - def gather_captions - [] unless @fields.keys.any? { |k| k.to_s.include?('caption') } - @fields.select { |f| f.to_s.include?('caption') } + def gather_captions(file) + [] unless file.keys.any? { |k| k.to_s.include?('caption') } + file.select { |f| f.to_s.include?('caption') } end end end diff --git a/lib/avalon/batch/manifest.rb b/lib/avalon/batch/manifest.rb index 9b913c6bc8..a904622df8 100644 --- a/lib/avalon/batch/manifest.rb +++ b/lib/avalon/batch/manifest.rb @@ -21,8 +21,7 @@ class Manifest extend Forwardable EXTENSIONS = ['csv','xls','xlsx','ods'] - FILE_FIELDS = [:file,:label,:offset,:skip_transcoding,:absolute_location,:date_digitized] - CAPTION_FIELDS = [:caption_file, :caption_label, :caption_language] + FILE_FIELDS = [:file,:label,:offset,:skip_transcoding,:absolute_location,:date_digitized, :caption_file, :caption_label, :caption_language] SKIP_FIELDS = [:collection] def_delegators :@entries, :each @@ -103,6 +102,18 @@ def true?(value) not (value.to_s =~ /^(y(es)?|t(rue)?)$/i).nil? end + def process_captions(field, content, values, i) + if field.to_s.include?('file') + @caption_count += 1 + @caption_key = "caption_#{@caption_count}".to_sym + content.last[@caption_key] = {} + # Set file path to caption file + content.last[@caption_key][field] = path_to(values[i]) + end + # Set caption metadata fields + content.last[@caption_key][field] ||= values[i] + end + def create_entries! first = @spreadsheet.first_row + 2 last = @spreadsheet.last_row @@ -118,22 +129,16 @@ def create_entries! content=[] fields = Hash.new { |h,k| h[k] = [] } - caption_count = 0 + @caption_count = 0 @field_names.each_with_index do |f,i| unless f.blank? || SKIP_FIELDS.include?(f) || values[i].blank? if FILE_FIELDS.include?(f) content << {} if f == :file - content.last[f] = f == :skip_transcoding ? true?(values[i]) : values[i] - elsif CAPTION_FIELDS.include?(f) - if f.to_s.include?('file') - caption_count += 1 - @caption_key = "caption_#{caption_count}".to_sym - fields[@caption_key] = {} - # Set file path to caption file - fields[@caption_key][f] = path_to(values[i]) + if f.to_s.include?('caption') + process_captions(f, content, values, i) + next end - # Set caption metadata fields - fields[@caption_key][f] ||= values[i] + content.last[f] = f == :skip_transcoding ? true?(values[i]) : values[i] else fields[f] << values[i] end diff --git a/spec/fixtures/dropbox/example_batch_ingest/batch_manifest.xlsx b/spec/fixtures/dropbox/example_batch_ingest/batch_manifest.xlsx index 8b44e1003bb1fefa379ee47d622bced3c6c04ea2..77063dee718f6293340a527ab95ffe8ee4a03771 100644 GIT binary patch delta 3022 zcmZ8jcT^Kd6AzHkLJ43Jic~?WAc_$XG@zkL?_EVYCNYM|uckk}L{b$Pl_V;FX=gm&ELX-ll;Y~W=DF7oQBS5^sv6@wshVEF_ zD$pp>o+w^)Ty!T&6g~gR7O{{)8Z+oKp$rKj-TJZ~B#*!iungpiin`_xrwPk)h}z#r&o=ceQci+4WkSri=~S zbj##&X4~p1f+L5vzedbj3T&-q@LB{b+yJFpiWWkevrJp|9(HZUpD&V>#8|5o^SqOJ zp%oWtQ@s^$Ysk4AGt>p1pq`NvlX=j`C_Z~he$SypJ+Swcna)KkdJY9)&aZI!z686v zTZ84ZsBq7m5B%BmuZMk#9xwS}{UyO5xjpp;^(^xb@1#BjdackaTEEr`Dke9PPhOG))ALvgSVv3w^(vi>AzJPuoGRg$q;hYupwOdNxL<J0>xFvi!V?%Vt*E;rm?yg@CEk6MLr+YJ-?v7cVb z3HruoPjIA_vzmluj$*EO6a~tMjV5#EbgCves-O=lLw|i8`zE;Sp5C4WE)!+=cz&P7 zoU$gp9#FagwHpQwWavF$=}dhcz%Z^!XIM9YNp4{30Wa7?6>On|Vrb=1b{dMKzrxLz zoOHB?v&{J=BQS_hM#JWqp@#Jk_%a_QXk4UwMBsi(WooS;BgO_j-+baij~F+b(f}&Z~0F zEOu8cZyLE56Z>9FWB{ec>MPuVfeszKiULC8RQmx?2NC6%*toc_0wVf)H@-oL6+?Dj!rRuZiIF82J@-C(?g=Ht0O}~}=No8BvIiP!N7shDp zHWcZYEbO11y@s4wOIqP3pmA6@jB$c46~@?m*g>ln^f@0C?%7s@9O!-7fE-ZYa}3Mz zRjNhe#4i_#^UwFVa1EhWLaUDh+T_sFGyFO7B2endyz8qP3UnPaXzG`T5tNWK&H*^dnjBt1xrT+awSpU+)h zUB*{c2WasU{?Kq`@*tC(wXHijz?Qayu|Jf5A)l{MCsV%3E2z7UD5hn+3dOFLZRR#K zD&9ew+G>1{;;a@i=!W^%MrQeEi4WVp?Y*m$Cn$N#iSV4Fsl%*|l3wsQQXSUCgHw8} zp7EJK4vcr|3z>9m@=E`VSE|HJ+?5@>7-QNK>5MB`Z*3}4uNin!!iyZeA;Tkz(MR7c zoHkYk7&V*CXl1R+HEGIrXov8u+GA0-DIbPYm*o8krrgiBXeK&o#dfOaSV@Sh){_8BYuufE^SCWa|ApVXg3LPEY-R2;HAq@B%^V2X+?!5in`RJNGz zQL0;=yeJi1ouhs}YNjP+XH7Q}s1F`41v)pwRiub*TjBZJnnma$wEgv$8Y@>XV6~a5GX%Q$d9NBKq>va0m>yrKvV?+K|QU zClJ^i%MOPoH+exg`$(3bv}?7yUZBtRyVzG_G+F1bZlPCRW~o4g~(C2L)Ne zwsiXIeavue=1tt}*HD}oE+vNkyqPz=;oQUkqhx0PpBT}&<=)M_VD=0Li&$^q8RuQ% zk#_KC5rl==BA#dCQKpv7rJC8WZ!6F;TX%WjyNC^ZARhJ78}Ijuw>y!dB@aGokl_%y zC^Tj1rR-251s##Dm8AFKy4Zm@#I`rCJgwGr5X?$~n+)+!Nl9T6&`O%ceDNJ!3j{mn zu6VV@myRko;;VC0xEc#zFl>?YGNdbK<5NY7?Ogq9lyJg|mF!(uFkz{8Bqs#@F5I1& zLk`m-TyHCD;*WphVF|{-zK9jINm#kqdd1nSzuI*Jq)^ zT;eNUgtmX3Sq1|n2=*lt`NN`;wK&7Z1NROx8vfL*6h_vBmjzP2az*w2uW>X_< zby_U?=4TJCiuR;v(6)M80%)K{;&CYkbV?^Dr}Fk11FD8->Vw-VzpYFoCdvK$^Lb;0 z!&YVj#8YK2=lV{X`P`dVYVPc8l=meJvw^JwmF15!7G1WMcNW`fGdxD0>Cl!Ix2U~f z5rUYe0QVZ~5>?FMshiTqsi5+W@CB5T%iCa;FHS#H3Q04K;gc_zw$nKgEwDEjfJ5Ku zKT`^XdUI5ZfP$LOgSW{+=^KJF6`yyexqv=ePqG95=;cLeb^tQJlm;i^mzjB zneiu|eOacMNeNg5HIr$sq7Rr>wMWrfllZxn1y<4fM=aX$M)_g)n%I8@zuO1@eu%|O zZYDl70Kok5WBlD^KqrGI(Va`4CW82cs}nV=)WYXF%taCsNrw_g)wVClNjAki?Vd~+ z_f&q1ir;LKDD1xpn^{6_4gdhalVj4q3IF2DKQ(ePwgy3;|JA-^;fUW3q6=L4 zL>9mmPvj@KKGU(^@}vV25!|Xmzb%9BKLUr2iN%g1(oD}W9=nzg8R$~=HF>^{0CD`Jk29?g86qQkpEz8i8effC!vP$@X;g_(|JTrthQt8 P9&v)_EWH5y_ksTb8sT5k delta 3023 zcmZ8jc{~)_7axYPgvdHW43RzC;9+F!#yXa4k?hG(s+1HS5!r01MLY7b_ zlzmAIUe=QQ`pi3VLfJHnOHh@!uBR~h$cYLHDL3PVCK=NCA4JTD4Sb` zouF2CwY5_4Wf8hPP2|R5O~`^^RB3?Yyxoglki!)lgfQG%7>18#QFj&y591|B%w9iY zZy&fJ(#N6kd8fiOhfNNzT035sS%=upk#ZHELn5oe%UON_Z0;9c#8_**8dnYh#>mlb zVqY6>!gNDEl6rlk8eIq_=!GY-m`tH~KYG73cuMeKw2);gFvRxPGwhnocEP4MVvz6C6r)`nfx(*_u z)jKhP=-OyE+yTW*iaOV)S*B9*rjFQh6l`gHBDHkT0o8EoaPhA=iB*o z-{tGVox07A)iPD^e5L-5z3U?0zp6TLW3%yH`CG)Pr|6kd^sIR34^?wa*t7LDh{%#; zfP_GQoz&q(#O;IqnnXj38UF+mZbAButER&Z;md+|qn zTp)FKHUA6eSKi^<_fgE6>~SX!;r=Igpkc@b#JFxajD_s{syY4(&!x*oS_fQr=M|Tf zm2?cVL~9o4SgugASxph~{-qoO)9S#;T(Wum)~@~K;PTO1J6;Cn^j@q7 zxCoxYaE|2-45H}wC=vqZxKsvj4Wf4MiP$xDOlB}ZkEvNqjMk8TWw+gE1M^m{1cGQQ zSMFX+%Wzt}e|0i4=lIG8AH6Xh(N-7&_1PW_?c1RNQhj)<3#2Lyc6(GX@InEB;(sWM zc*i8Ch(H7aDoQGGu2NXUzq_j8q}|6 z1wI=-nXC>w0RNmOSB@`5cZ?|yNlE<7vTQQ*wkl7#46-`R?g4bo@|*^O)y7HJti_h$ zb>uVdF6#Ge)mMaLJcqpweBJhorVY7mDbVAaiBgf; zB`gpv&Lh|+#e4)uc9EM{gT9jXB>!3+iq<^R4xHqhf7m`9ZoZV}=aHuj%bCc+v&5mh z@+yd#BwMjomTP$_FTWALYJJR*mEoKU>1$f^)*4sRqLmloqAH&b1F9@)(02>gr69Lq z`aMT+{gq9w(Gl;b^eB=SWlStiJfy913wHJ1A$#W-OUlW3coG%d*@DlXzf}WY!2$T{ z)I!y0uH-79)?690>KH^&s=_(u$2yKuMt&`~?{Re`aMjeT56=;Aw=e?ElzT1L*l+I~Ni5IWb=$YdJ9{PMu6>LEyl;!pncC=HGGdZH*jCe^}Z zRbtZyr^ExoIK2k=XaNL)ERp=`6+Zwukbz68r`Ci245G#sZxyH7Md&;ho9D zZ8!^8sbon2$B%x9bPLbJHGUm+i^SuSlu-v4@QOTFe8tYqHKVl%EH*4g46VtB;IIa2 z71tLVufpWaCbgDo2!#Bci^4^OuchIRM{oLrWxo_cc8Rfe1(`~@Mh@z<26~`Hs5g^{ zju;x0_)>!D!6fK}?x~lO$LPIw5Q>dp?B{O%Gb;gN-N*0BZGY2IS2p4?Q~_1DJ!@h5 z$W^`egNC>j^_nL>vHwd4TF*(0v%uzko3$o0E4@^u8HLnIs-VpEki3EO1;{oHLjf)s z^y5vFg{KOUH&M-IgILZ_7E_g2_VW)NYE75T5?OBYrl-D7lvt=ve8H-KXn(HR!l<6a z-ZU3T)n2fbr0VbV#?LB)78`8(3DcCTY=%$xnmk#RIx>(5{?0^9K@SMnOTJr+8Gh1g z_uL@dTBa{2bJw}Vj-fH9S^PDyAdusOh6g(dE}9ciI6t|9ikWM;%G`8C?}iwuXLD5M zTqROqhD?rmf4fnC{u-p#(fOxM6zTDgBA3v`G9lv1T{Xy-_r)XpQF3)d_mgOH9z;{I zzp>u7us}C~7WPKG4$a&bCP6X|pyIF;XcTbEp5OAEqRbm~do{jml6<}FZC}+#HGhI1 z)6C$>?wtuv<=jLvF3(?CqB}JvD%6AL9K4B|w!&k_6s}vim)Nv{xn|blK6;3DInfHS zjq2oZ6ke42z8~;~-^)s6s?=;T*!#f;wgln0LbI(scmRdsE+5!oZ|2D#G-cGrXOj=UZMeHDPeXDaK004X zVDHxjBRdmx#M&zRdKQ#bFCh^=LZsyAgbgUhCRCO^CBC;O*Ow>Af;mmZ1;|<>P#c$G z3V9{Y#jAsLywvRVVxG&qqbMaYu$|J@WikqaMqE+4H$`9OgA$}cih__$`>m{)hzJmn z1309sbi5!C#F}#T3&z*5_|EE_NFxo+zCw9;^Q4zf_FdiNoz-2Bu#j0BBfPhj(4+U` zJy64-Ia=9xO0U@(UEVjDR*9ZwoHkdD>)}HQ+E*QyMa2_VMUVH|*+0EWt{o9l>}MpQ zAOsrWGxeLDe9@0ReXQ1-e?q)s&j*K?4*`c4_cH01USFZ-sn{#d(j+_h zYJ-zYUr!3~)!*xW{w=%dM#1{Vyk=v5-Ica7&8ZZg2`OLboh^4grzCp$jklqPtIiYo zzZ20(81%nK0RSwWCgbluLwz=R;yhSoD3Wm5tlip9d7l{@*5VzQv4aWy>je+8GSffx z7%!1udnttWYA5V;qEpkdYd6mSCWPLq5&OgrN45u;Osonal2jb+pRQZ1IOt!y27&|2vI`tHCD*su9I4qaw8Rp+* z#r6;2RBOXca$P#Z{5xWn|6r caption_file, :caption_label => 'Sheephead Captions', :caption_language => 'English' }} - let(:entry_fields) {{ title: Faker::Lorem.sentence, date_issued: "#{DateTime.now.strftime('%F')}", caption_1: caption }} + let(:entry_files) { [{ file: File.join(testdir, filename), offset: '00:00:00.500', label: 'Quis quo', date_digitized: '2015-10-30', skip_transcoding: false, caption_1: caption }] } it 'adds captions to masterfile' do expect(master_file.supplemental_file_captions).to be_present From c52588102844a3c2a1eafa999ab843dbe629c467 Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Fri, 5 Apr 2024 13:18:28 -0400 Subject: [PATCH 009/152] Improve guard clause in attach_datastreams method --- lib/avalon/batch/entry.rb | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/lib/avalon/batch/entry.rb b/lib/avalon/batch/entry.rb index b91f54ade5..53b023c808 100644 --- a/lib/avalon/batch/entry.rb +++ b/lib/avalon/batch/entry.rb @@ -183,16 +183,14 @@ def self.attach_datastreams_to_master_file( master_file, filename, captions ) master_file.structuralMetadata.original_name = structural_file end captions.each do |c| - return unless c.present? - if c[:caption_file].present? && FileLocator.new(c[:caption_file]).exist? - filename = c[:caption_file].split('/').last - label = c[:caption_label].presence || filename - language = c[:caption_language].present? ? caption_language(c[:caption_language]) : Settings.caption_default.language - supplemental_file = SupplementalFile.new(label: label, tags: ['caption'], language: language) - supplemental_file.file.attach(io: FileLocator.new(c[:caption_file]).reader, filename: filename) - supplemental_file.save - master_file.supplemental_files += [supplemental_file] - end + next unless c.present? && c[:caption_file].present? && FileLocator.new(c[:caption_file]).exist? + filename = c[:caption_file].split('/').last + label = c[:caption_label].presence || filename + language = c[:caption_language].present? ? caption_language(c[:caption_language]) : Settings.caption_default.language + supplemental_file = SupplementalFile.new(label: label, tags: ['caption'], language: language) + supplemental_file.file.attach(io: FileLocator.new(c[:caption_file]).reader, filename: filename) + supplemental_file.save + master_file.supplemental_files += [supplemental_file] end end From 83935ff64bf0dd253372388516d9a938cb252991 Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Tue, 2 Apr 2024 16:40:15 -0400 Subject: [PATCH 010/152] Add option to treat a caption file as transcript --- .../supplemental_files_controller.rb | 15 ++++---- app/models/iiif_canvas_presenter.rb | 34 +++++++++++++------ app/models/supplemental_file.rb | 4 +++ .../_supplemental_files_list.html.erb | 24 +++++++++---- spec/models/iiif_canvas_presenter_spec.rb | 9 +++++ .../supplemental_files_controller_examples.rb | 28 +++++++++++++++ 6 files changed, 90 insertions(+), 24 deletions(-) diff --git a/app/controllers/supplemental_files_controller.rb b/app/controllers/supplemental_files_controller.rb index 298289a855..676c7879c9 100644 --- a/app/controllers/supplemental_files_controller.rb +++ b/app/controllers/supplemental_files_controller.rb @@ -151,12 +151,15 @@ def edit_structure_path end def edit_file_information - ident = "machine_generated_#{params[:id]}".to_sym - - if params[ident] && !@supplemental_file.machine_generated? - @supplemental_file.tags += ['machine_generated'] - elsif !params[ident] && @supplemental_file.machine_generated? - @supplemental_file.tags -= ['machine_generated'] + file_params = { machine_generated?: "machine_generated_#{params[:id]}".to_sym, caption_transcript?: "treat_as_transcript_#{params[:id]}".to_sym } + + file_params.each do |k,v| + tag = k.to_s.gsub(/(caption_)?(.*)(\?)/, '\2') + if params[v] && !@supplemental_file.send(k) + @supplemental_file.tags += [tag] + elsif !params[v] && @supplemental_file.send(k) + @supplemental_file.tags -= [tag] + end end @supplemental_file.label = supplemental_file_params[:label] return unless supplemental_file_params[:language].present? diff --git a/app/models/iiif_canvas_presenter.rb b/app/models/iiif_canvas_presenter.rb index 7b650248a5..31d40420ca 100644 --- a/app/models/iiif_canvas_presenter.rb +++ b/app/models/iiif_canvas_presenter.rb @@ -41,7 +41,7 @@ def display_content end def annotation_content - supplemental_captions_transcripts.collect { |file| supplementing_content_data(file) } + supplemental_captions_transcripts.uniq.collect { |file| supplementing_content_data(file) }.flatten end def sequence_rendering @@ -103,16 +103,28 @@ def audio_display_content(quality) end def supplementing_content_data(file) - url = if !file.is_a?(SupplementalFile) - Rails.application.routes.url_helpers.captions_master_file_url(master_file.id) - elsif file.tags.include?('caption') - Rails.application.routes.url_helpers.captions_master_file_supplemental_file_url(master_file.id, file.id) - elsif file.tags.include?('transcript') - Rails.application.routes.url_helpers.transcripts_master_file_supplemental_file_url(master_file.id, file.id) - else - Rails.application.routes.url_helpers.master_file_supplemental_file_url(master_file.id, file.id) - end - IIIFManifest::V3::AnnotationContent.new(body_id: url, **supplemental_attributes(file)) + unless file.is_a?(SupplementalFile) + url = Rails.application.routes.url_helpers.captions_master_file_url(master_file.id) + return IIIFManifest::V3::AnnotationContent.new(body_id: url, **supplemental_attributes(file)) + end + + tags = file.tags.reject { |t| t == 'machine_generated' }.compact + case tags + when ['caption'] + url = Rails.application.routes.url_helpers.captions_master_file_supplemental_file_url(master_file.id, file.id) + IIIFManifest::V3::AnnotationContent.new(body_id: url, **supplemental_attributes(file)) + when ['transcript'] + url = Rails.application.routes.url_helpers.transcripts_master_file_supplemental_file_url(master_file.id, file.id) + IIIFManifest::V3::AnnotationContent.new(body_id: url, **supplemental_attributes(file)) + when ['caption', 'transcript'] + caption_url = Rails.application.routes.url_helpers.captions_master_file_supplemental_file_url(master_file.id, file.id) + transcript_url = Rails.application.routes.url_helpers.transcripts_master_file_supplemental_file_url(master_file.id, file.id) + [IIIFManifest::V3::AnnotationContent.new(body_id: caption_url, **supplemental_attributes(file)), + IIIFManifest::V3::AnnotationContent.new(body_id: transcript_url, **supplemental_attributes(file))] + else + url = Rails.application.routes.url_helpers.master_file_supplemental_file_url(master_file.id, file.id) + IIIFManifest::V3::AnnotationContent.new(body_id: url, **supplemental_attributes(file)) + end end def stream_urls diff --git a/app/models/supplemental_file.rb b/app/models/supplemental_file.rb index 84722d1fa5..34ae5df198 100644 --- a/app/models/supplemental_file.rb +++ b/app/models/supplemental_file.rb @@ -46,6 +46,10 @@ def machine_generated? tags.include?('machine_generated') end + def caption_transcript? + tags.include?('caption') && tags.include?('transcript') + end + # Adapted from https://github.com/opencoconut/webvtt-ruby/blob/e07d59220260fce33ba5a0c3b355e3ae88b99457/lib/webvtt/parser.rb#L11-L30 def self.convert_from_srt(srt) # normalize timestamps in srt diff --git a/app/views/media_objects/_supplemental_files_list.html.erb b/app/views/media_objects/_supplemental_files_list.html.erb index a3847bbde0..76f74af561 100644 --- a/app/views/media_objects/_supplemental_files_list.html.erb +++ b/app/views/media_objects/_supplemental_files_list.html.erb @@ -18,22 +18,26 @@ Unless required by applicable law or agreed to in writing, software distributed
captions<% end %>"> <% if tag == "caption" %>
-
- -
-
- +
+
+ +
+
+ +
<% end %> <% files.each do |file| %> + <% next if tag == 'transcript' and file.tags.include?('caption') %>
" class="display-item"><%= file.label %> <%= form_for :supplemental_file, url: object_supplemental_file_path(section, file), remote: true, html: { method: "put", class: "supplemental-file-form edit-item", id: "form-#{file.id}" }, data: { file_id: file.id, masterfile_id: section.id } do |form| %>
-
+ <% col = tag == 'caption' ? 4 : 6 %> +
<%= form.text_field :label, id: "supplemental_file_input_#{section.id}_#{file.id}", value: file.label %>
<% if tag == 'transcript' %> @@ -45,11 +49,17 @@ Unless required by applicable law or agreed to in writing, software distributed <% end %> <% if tag == 'caption' %> -
+
<%= form.text_field :language, id: "supplemental_file_language_#{section.id}_#{file.id}", value: LanguageTerm.find(file.language).text, class: "typeahead from-model form-control", data: { model: 'languageTerm', validate: false } %>
+ + <%= label_tag "treat_as_transcript_#{file.id}", class: "ml-3" do %> + <%= check_box_tag "treat_as_transcript_#{file.id}", '1', file.caption_transcript? %> + Treat as transcript + <% end %> + <% end %>
diff --git a/spec/models/iiif_canvas_presenter_spec.rb b/spec/models/iiif_canvas_presenter_spec.rb index b8523bb2be..c94be13778 100644 --- a/spec/models/iiif_canvas_presenter_spec.rb +++ b/spec/models/iiif_canvas_presenter_spec.rb @@ -298,6 +298,15 @@ expect(subject.any? { |content| content.label['eng'][0] =~ /#{transcript_file.label} \(machine generated\)/ }).to eq true end end + + context 'caption being treated as a transcript' do + let(:caption_file) { FactoryBot.create(:supplemental_file, :with_caption_file, tags: ['caption', 'transcript']) } + + it 'returns a caption entry and a transcript entry' do + expect(subject.any? { |content| content.body_id =~ /supplemental_files\/#{caption_file.id}\/transcripts/ }).to eq true + expect(subject.any? { |content| content.body_id =~ /supplemental_files\/#{caption_file.id}\/captions/ }).to eq true + end + end end end diff --git a/spec/support/supplemental_files_controller_examples.rb b/spec/support/supplemental_files_controller_examples.rb index c79f1a44ad..cb1a5ac2f4 100644 --- a/spec/support/supplemental_files_controller_examples.rb +++ b/spec/support/supplemental_files_controller_examples.rb @@ -292,6 +292,34 @@ end end + context "caption treated as transcript" do + let(:transcript_param) { "treat_as_transcript_#{supplemental_file.id}".to_sym } + context "missing transcript tag" do + let(:supplemental_file) { FactoryBot.create(:supplemental_file, :with_caption_file, :with_caption_tag, label: 'label') } + it "adds transcript note to tags" do + expect { + put :update, params: { class_id => object.id, id: supplemental_file.id, transcript_param => 1, supplemental_file: valid_update_attributes, format: :html }, session: valid_session + }.to change { master_file.reload.supplemental_files.first.tags }.from(['caption']).to(['caption', 'transcript']) + end + end + context "with transcript tag" do + let(:supplemental_file) { FactoryBot.create(:supplemental_file, :with_transcript_file, tags: ['caption', 'transcript'], label: 'label') } + it "does not add more instances of transcript note" do + expect { + put :update, params: { class_id => object.id, id: supplemental_file.id, transcript_param => 1, supplemental_file: valid_update_attributes, format: :html }, session: valid_session + }.to not_change { master_file.reload.supplemental_files.first.tags }.from(['caption', 'transcript']) + end + end + context "removing transcript designation" do + let(:supplemental_file) { FactoryBot.create(:supplemental_file, :with_transcript_file, tags: ['caption', 'transcript'], label: 'label (machine generated)') } + it "removes transcript note from tags" do + expect { + put :update, params: { class_id => object.id, id: supplemental_file.id, supplemental_file: valid_update_attributes, format: :html }, session: valid_session + }.to change { master_file.reload.supplemental_files.first.tags }.from(['caption', 'transcript']).to(['caption']) + end + end + end + context "with invalid params" do it "returns a 400" do put :update, params: { class_id => object.id, id: supplemental_file.id, supplemental_file: invalid_update_attributes, format: :html }, session: valid_session From cd8ff2c8f44b95f79204673d8775145517bffc7e Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Fri, 5 Apr 2024 10:55:48 -0400 Subject: [PATCH 011/152] Ensure that captions as transcript format values are correct Captions are streamed out as VTT files so the format in the iiif manifest must be 'text/vtt'. Transcripts do not have this limitation and so an SRT caption file being treated as a transcript will retain the SRT formatting. In this case, the iiif manifest must have 'text/srt' as the format for the transcript annotation. --- app/models/iiif_canvas_presenter.rb | 12 +++++------ spec/models/iiif_canvas_presenter_spec.rb | 25 +++++++++++++++++------ 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/app/models/iiif_canvas_presenter.rb b/app/models/iiif_canvas_presenter.rb index 31d40420ca..fe0b170ed8 100644 --- a/app/models/iiif_canvas_presenter.rb +++ b/app/models/iiif_canvas_presenter.rb @@ -112,15 +112,15 @@ def supplementing_content_data(file) case tags when ['caption'] url = Rails.application.routes.url_helpers.captions_master_file_supplemental_file_url(master_file.id, file.id) - IIIFManifest::V3::AnnotationContent.new(body_id: url, **supplemental_attributes(file)) + IIIFManifest::V3::AnnotationContent.new(body_id: url, **supplemental_attributes(file, type: 'caption')) when ['transcript'] url = Rails.application.routes.url_helpers.transcripts_master_file_supplemental_file_url(master_file.id, file.id) - IIIFManifest::V3::AnnotationContent.new(body_id: url, **supplemental_attributes(file)) + IIIFManifest::V3::AnnotationContent.new(body_id: url, **supplemental_attributes(file, type: 'transcript')) when ['caption', 'transcript'] caption_url = Rails.application.routes.url_helpers.captions_master_file_supplemental_file_url(master_file.id, file.id) transcript_url = Rails.application.routes.url_helpers.transcripts_master_file_supplemental_file_url(master_file.id, file.id) - [IIIFManifest::V3::AnnotationContent.new(body_id: caption_url, **supplemental_attributes(file)), - IIIFManifest::V3::AnnotationContent.new(body_id: transcript_url, **supplemental_attributes(file))] + [IIIFManifest::V3::AnnotationContent.new(body_id: caption_url, **supplemental_attributes(file, type: 'caption')), + IIIFManifest::V3::AnnotationContent.new(body_id: transcript_url, **supplemental_attributes(file, type: 'transcript'))] else url = Rails.application.routes.url_helpers.master_file_supplemental_file_url(master_file.id, file.id) IIIFManifest::V3::AnnotationContent.new(body_id: url, **supplemental_attributes(file)) @@ -218,10 +218,10 @@ def manifest_attributes(quality, media_type) end end - def supplemental_attributes(file) + def supplemental_attributes(file, type: nil) if file.is_a?(SupplementalFile) label = file.tags.include?('machine_generated') ? file.label + ' (machine generated)' : file.label - format = if file.file.content_type == 'text/srt' && file.tags.include?('caption') + format = if file.file.content_type == 'text/srt' && type == 'caption' 'text/vtt' else file.file.content_type diff --git a/spec/models/iiif_canvas_presenter_spec.rb b/spec/models/iiif_canvas_presenter_spec.rb index c94be13778..c983d34f4f 100644 --- a/spec/models/iiif_canvas_presenter_spec.rb +++ b/spec/models/iiif_canvas_presenter_spec.rb @@ -269,14 +269,20 @@ expect(subject.any? { |content| content.body_id =~ /master_files\/#{master_file.id}\/captions/ }).to eq false end - context 'srt captions' do + context 'srt files' do let(:srt_caption_file) { FactoryBot.create(:supplemental_file, :with_caption_srt_file, :with_caption_tag) } - let(:supplemental_files) { [supplemental_file, transcript_file, caption_file, srt_caption_file] } - it 'sets format to "text/vtt"' do + let(:srt_transcript_file) { FactoryBot.create(:supplemental_file, :with_caption_srt_file, :with_transcript_tag) } + let(:supplemental_files) { [transcript_file, caption_file, srt_caption_file, srt_transcript_file] } + it 'sets caption format to "text/vtt"' do captions = subject.select { |s| s.body_id.include?('captions') } expect(captions.none? { |content| content.format == 'text/srt' }).to eq true expect(captions.all? { |content| content.format == 'text/vtt' }).to eq true end + it 'sets other formats to the original file content_type' do + supplemental_files = subject.reject { |s| s.body_id.include?('captions') } + expect(supplemental_files[0].format).to eq transcript_file.file.content_type + expect(supplemental_files[1].format).to eq srt_transcript_file.file.content_type + end end context 'legacy master file captions' do @@ -301,10 +307,17 @@ context 'caption being treated as a transcript' do let(:caption_file) { FactoryBot.create(:supplemental_file, :with_caption_file, tags: ['caption', 'transcript']) } + let(:srt_caption_file) { FactoryBot.create(:supplemental_file, :with_caption_srt_file, tags: ['caption', 'transcript']) } + let(:supplemental_files) { [caption_file, srt_caption_file] } - it 'returns a caption entry and a transcript entry' do - expect(subject.any? { |content| content.body_id =~ /supplemental_files\/#{caption_file.id}\/transcripts/ }).to eq true - expect(subject.any? { |content| content.body_id =~ /supplemental_files\/#{caption_file.id}\/captions/ }).to eq true + it 'returns a caption entry and a transcript entry with proper formats' do + captions = subject.select { |s| s.body_id.include?('captions') } + transcripts = subject.select { |s| s.body_id.include?('transcripts') } + expect(captions.count).to eq 2 + expect(transcripts.count).to eq 2 + expect(captions.all? { |content| content.format == 'text/vtt' }).to eq true + expect(transcripts.any? { |content| content.format == 'text/vtt' }).to eq true + expect(transcripts.any? { |content| content.format == 'text/srt' }).to eq true end end end From c46f8354653cfca6062f12f8088e90d04549a2be Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Fri, 5 Apr 2024 14:11:32 -0400 Subject: [PATCH 012/152] Use array of hashes for params instead of regex --- .../supplemental_files_controller.rb | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/app/controllers/supplemental_files_controller.rb b/app/controllers/supplemental_files_controller.rb index 676c7879c9..df91f555d5 100644 --- a/app/controllers/supplemental_files_controller.rb +++ b/app/controllers/supplemental_files_controller.rb @@ -151,13 +151,18 @@ def edit_structure_path end def edit_file_information - file_params = { machine_generated?: "machine_generated_#{params[:id]}".to_sym, caption_transcript?: "treat_as_transcript_#{params[:id]}".to_sym } - - file_params.each do |k,v| - tag = k.to_s.gsub(/(caption_)?(.*)(\?)/, '\2') - if params[v] && !@supplemental_file.send(k) + file_params = [ + { param: "machine_generated_#{params[:id]}".to_sym, tag: "machine_generated", method: :machine_generated? }, + { param: "treat_as_transcript_#{params[:id]}".to_sym, tag: "transcript", method: :caption_transcript? } + ] + + file_params.each do |v| + param_name = v[:param] + tag = v[:tag] + method = v[:method] + if params[param_name] && !@supplemental_file.send(method) @supplemental_file.tags += [tag] - elsif !params[v] && @supplemental_file.send(k) + elsif !params[param_name] && @supplemental_file.send(method) @supplemental_file.tags -= [tag] end end From 9269fbf2b69b2dd137a6fad2e33a3c680cde546b Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Wed, 3 Apr 2024 16:37:52 -0400 Subject: [PATCH 013/152] Add machine generated option to captions This commit moves the 'Treat as Transcript' and 'Machine Generated' checkboxes to their own row within the edit supplemental file form. --- app/assets/stylesheets/avalon.scss | 4 + app/assets/stylesheets/avalon/_form.scss | 5 + .../_supplemental_files_list.html.erb | 155 +++++++++--------- .../supplemental_files_controller_examples.rb | 2 +- 4 files changed, 91 insertions(+), 75 deletions(-) diff --git a/app/assets/stylesheets/avalon.scss b/app/assets/stylesheets/avalon.scss index 46d3ece523..1ca1a49888 100644 --- a/app/assets/stylesheets/avalon.scss +++ b/app/assets/stylesheets/avalon.scss @@ -949,6 +949,10 @@ h5.card-title { } } + div.supplemental-file-data.is-editing:not(.edit-item) + div.supplemental-file-data { + margin-top: 2.6rem; + } + .supplemental-file-form { float: left; width: 100%; diff --git a/app/assets/stylesheets/avalon/_form.scss b/app/assets/stylesheets/avalon/_form.scss index 28bf7dfda1..378c3299ba 100644 --- a/app/assets/stylesheets/avalon/_form.scss +++ b/app/assets/stylesheets/avalon/_form.scss @@ -80,6 +80,11 @@ $border-radius-base: 4px !default; margin-bottom: 2px; } +.form-group label.checkbox { + margin-left: 0.5rem !important; + padding-right: 1rem !important; +} + /* * Fix that shouldn't be needed to make sure that the text behaves like a regular * link diff --git a/app/views/media_objects/_supplemental_files_list.html.erb b/app/views/media_objects/_supplemental_files_list.html.erb index 76f74af561..9a464379cd 100644 --- a/app/views/media_objects/_supplemental_files_list.html.erb +++ b/app/views/media_objects/_supplemental_files_list.html.erb @@ -13,81 +13,88 @@ 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 section.supplemental_files_json.present? %> - <% files=tag.empty? ? section.supplemental_files(tag: nil) : section.supplemental_files(tag: tag) %> -
captions<% end %>"> - <% if tag == "caption" %> -
-
-
- -
-
- -
-
+<% if section.supplemental_files_json.present? %> + <% files=tag.empty? ? section.supplemental_files(tag: nil) : section.supplemental_files(tag: tag) %> +
captions<% end %>"> + <% if tag == "caption" %> +
+
+
+ +
+
+ +
+
+
+ <% end %> + <% files.each do |file| %> + <% next if tag == 'transcript' and file.tags.include?('caption') %> +
+ " class="display-item"><%= file.label %> + <%= form_for :supplemental_file, url: object_supplemental_file_path(section, file), remote: true, + html: { method: "put", class: "supplemental-file-form edit-item", id: "form-#{file.id}" }, + data: { file_id: file.id, masterfile_id: section.id } do |form| %> +
+
+ <%= form.text_field :label, id: "supplemental_file_input_#{section.id}_#{file.id}", value: file.label %> +
+ <% if tag == 'transcript' %> + + <%= label_tag "machine_generated_#{file.id}", class: "ml-3" do %> + <%= check_box_tag "machine_generated_#{file.id}", '1', file.machine_generated? %> + Machine Generated + <% end %> + + <% end %> + <% if tag == 'caption' %> +
+ <%= form.text_field :language, id: "supplemental_file_language_#{section.id}_#{file.id}", value: LanguageTerm.find(file.language).text, + class: "typeahead from-model form-control", + data: { model: 'languageTerm', validate: false } %> +
+
+
+ <%= label_tag "treat_as_transcript_#{file.id}", class: "checkbox", style: "white-space: nowrap; padding-left: 0.45rem;" do %> + <%= check_box_tag "treat_as_transcript_#{file.id}", '1', file.caption_transcript? %> + Treat as Transcript + <% end %>
- <% end %> - <% files.each do |file| %> - <% next if tag == 'transcript' and file.tags.include?('caption') %> -
- " class="display-item"><%= file.label %> - <%= form_for :supplemental_file, url: object_supplemental_file_path(section, file), remote: true, - html: { method: "put", class: "supplemental-file-form edit-item", id: "form-#{file.id}" }, - data: { file_id: file.id, masterfile_id: section.id } do |form| %> -
- <% col = tag == 'caption' ? 4 : 6 %> -
- <%= form.text_field :label, id: "supplemental_file_input_#{section.id}_#{file.id}", value: file.label %> -
- <% if tag == 'transcript' %> - - <%= label_tag "machine_generated_#{file.id}", class: "ml-3" do %> - <%= check_box_tag "machine_generated_#{file.id}", '1', file.machine_generated? %> - Machine Generated - <% end %> - - <% end %> - <% if tag == 'caption' %> -
- <%= form.text_field :language, id: "supplemental_file_language_#{section.id}_#{file.id}", value: LanguageTerm.find(file.language).text, - class: "typeahead from-model form-control", - data: { model: 'languageTerm', validate: false } %> -
- - <%= label_tag "treat_as_transcript_#{file.id}", class: "ml-3" do %> - <%= check_box_tag "treat_as_transcript_#{file.id}", '1', file.caption_transcript? %> - Treat as transcript - <% end %> - - <% end %> -
-
-
- <%= button_tag name: 'save_label', :class => "btn btn-outline btn-sm edit-item" do %> - Save - <% end %> - <%= button_tag name: 'cancel_edit_label', class:'btn btn-danger btn-sm edit-item', type: 'button' do%> - Cancel - <% end %> -
-
- <% end %> - - - - - -
- <%# Update button %> - <%= button_tag name: 'edit_label', class:'btn btn-outline btn-sm edit_label display-item', type: 'button' do %> - Edit - <% end %> - <%= link_to(object_supplemental_file_path(section, file), title: 'Remove', method: :delete, class: "btn btn-danger btn-sm file-remove btn-confirmation") do %> - Delete - <% end %> -
+
+ <%= label_tag "machine_generated_#{file.id}", class: "ml-3 checkbox", style: "white-space: nowrap; padding-left: 2.6rem;" do %> + <%= check_box_tag "machine_generated_#{file.id}", '1', file.machine_generated? %> + Machine Generated + <% end %>
+
+ <% end %> +
+
+
+ <%= button_tag name: 'save_label', :class => "btn btn-outline btn-sm edit-item" do %> + Save + <% end %> + <%= button_tag name: 'cancel_edit_label', class:'btn btn-danger btn-sm edit-item', type: 'button' do%> + Cancel <% end %>
- <% end %> +
+ <% end %> + + + + + +
+ <%# Update button %> + <%= button_tag name: 'edit_label', class:'btn btn-outline btn-sm edit_label display-item', type: 'button' do %> + Edit + <% end %> + <%= link_to(object_supplemental_file_path(section, file), title: 'Remove', method: :delete, class: "btn btn-danger btn-sm file-remove btn-confirmation") do %> + Delete + <% end %> +
+
+ <% end %> +
+<% end %> diff --git a/spec/support/supplemental_files_controller_examples.rb b/spec/support/supplemental_files_controller_examples.rb index cb1a5ac2f4..51612f0b81 100644 --- a/spec/support/supplemental_files_controller_examples.rb +++ b/spec/support/supplemental_files_controller_examples.rb @@ -264,7 +264,7 @@ end end - context "machine generated transcript" do + context "machine generated file" do let(:machine_param) { "machine_generated_#{supplemental_file.id}".to_sym } context "missing machine_generated tag" do let(:supplemental_file) { FactoryBot.create(:supplemental_file, :with_transcript_file, :with_transcript_tag, label: 'label') } From 006be19d71f14aa386eec324ce9bb165980df88a Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Thu, 4 Apr 2024 09:50:52 -0400 Subject: [PATCH 014/152] Increase gap between checkboxes and input fields --- app/assets/stylesheets/avalon.scss | 3 +++ app/views/media_objects/_supplemental_files_list.html.erb | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/avalon.scss b/app/assets/stylesheets/avalon.scss index 1ca1a49888..9b5f4a5fec 100644 --- a/app/assets/stylesheets/avalon.scss +++ b/app/assets/stylesheets/avalon.scss @@ -932,6 +932,9 @@ h5.card-title { .file-remove { display: none; } + .form-group { + margin-bottom: 0.2rem; + } display: flex; } margin-top: 0.75rem; diff --git a/app/views/media_objects/_supplemental_files_list.html.erb b/app/views/media_objects/_supplemental_files_list.html.erb index 9a464379cd..a6b6389ac4 100644 --- a/app/views/media_objects/_supplemental_files_list.html.erb +++ b/app/views/media_objects/_supplemental_files_list.html.erb @@ -36,7 +36,7 @@ Unless required by applicable law or agreed to in writing, software distributed html: { method: "put", class: "supplemental-file-form edit-item", id: "form-#{file.id}" }, data: { file_id: file.id, masterfile_id: section.id } do |form| %>
-
+
<%= form.text_field :label, id: "supplemental_file_input_#{section.id}_#{file.id}", value: file.label %>
<% if tag == 'transcript' %> @@ -48,7 +48,7 @@ Unless required by applicable law or agreed to in writing, software distributed <% end %> <% if tag == 'caption' %> -
+
<%= form.text_field :language, id: "supplemental_file_language_#{section.id}_#{file.id}", value: LanguageTerm.find(file.language).text, class: "typeahead from-model form-control", data: { model: 'languageTerm', validate: false } %> From c411320fa22486aa567326e393748306dc9205e2 Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Fri, 5 Apr 2024 15:20:43 -0400 Subject: [PATCH 015/152] Adjust spacing between edit fields --- app/assets/stylesheets/avalon.scss | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/avalon.scss b/app/assets/stylesheets/avalon.scss index 9b5f4a5fec..c22e44685b 100644 --- a/app/assets/stylesheets/avalon.scss +++ b/app/assets/stylesheets/avalon.scss @@ -953,7 +953,7 @@ h5.card-title { } div.supplemental-file-data.is-editing:not(.edit-item) + div.supplemental-file-data { - margin-top: 2.6rem; + margin-top: 2.5rem; } .supplemental-file-form { @@ -977,6 +977,12 @@ h5.card-title { .form-control { height: 25px; } + + &.captions { + div.supplemental-file-data.is-editing:last-child { + margin-bottom: 1.75rem; + } + } } } From e8688101c4efbb58f87e4b08c36a345758a31090 Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Thu, 28 Mar 2024 13:12:17 -0400 Subject: [PATCH 016/152] Use Ramp for embedded player --- app/assets/javascripts/application.js | 4 - app/assets/stylesheets/application.scss | 3 - app/assets/stylesheets/embed.scss | 116 ----------------- app/controllers/master_files_controller.rb | 8 -- app/javascript/components/EmbeddedRamp.jsx | 118 ++++++++++++++++++ app/javascript/components/MediaObjectRamp.jsx | 2 +- app/javascript/components/PlaylistRamp.jsx | 2 +- app/views/layouts/embed.html.erb | 6 +- app/views/master_files/_player.html.erb | 31 +---- app/views/modules/player/_section.html.erb | 20 --- .../modules/player/_video_js_element.html.erb | 117 ----------------- config/initializers/assets.rb | 2 - 12 files changed, 128 insertions(+), 301 deletions(-) delete mode 100644 app/assets/stylesheets/embed.scss create mode 100644 app/javascript/components/EmbeddedRamp.jsx delete mode 100644 app/views/modules/player/_section.html.erb delete mode 100644 app/views/modules/player/_video_js_element.html.erb diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 8e041b1aa6..19c78f1385 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -47,8 +47,4 @@ // include all of our vendored js //= require_tree ../../../vendor/assets/javascripts/. -// Require VideoJS and VideoJS quality selector for embedded player -//= require video.js/dist/video.min.js -//= require @silvermine/videojs-quality-selector/dist/js/silvermine-videojs-quality-selector.min.js - //= require_tree . diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 30153ba8ee..f4af972a20 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -55,6 +55,3 @@ @import 'avalon'; @import "datatables"; - -@import "video.js/dist/video-js.css"; -@import "@silvermine/videojs-quality-selector/dist/css/quality-selector.css"; diff --git a/app/assets/stylesheets/embed.scss b/app/assets/stylesheets/embed.scss deleted file mode 100644 index 1dfb32109b..0000000000 --- a/app/assets/stylesheets/embed.scss +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Copyright 2011-2024, 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 --- -*/ - -.video-container { - position: absolute; -} - -.video-js { - position: relative; - z-index: 0; -} - -.video-title { - position: absolute; - z-index: 1; - top: 5px; - left: 8px; -} - -.video-title .video-link { - color: white; - font-size:150%; -} - -.video-js.vjs-playing + .video-title { - display: none; -} - -// @silvermine/videojs-quality-selector -.vjs-menu { - li { - font-size: 1em; - } -} - -.video-js .vjs-control-bar { - /* Audio: Make the controlbar visible by default */ - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - /* Increase the control-bar icons/text size */ - font-size: 120%; -} - -/* Make player height minimum to the controls height so when we hide -video/poster area the controls are displayed correctly. */ -.video-js.vjs-audio { - min-height: 3.7em; -} - -.video-js .vjs-progress-control:hover .vjs-play-progress:after { - display: none; -} - -/* Show poster image when playback ends */ -.video-js.vjs-ended .vjs-poster { - display: block; -} - -.video-js .vjs-current-time { - display: block; -} - -/* Put playhead on top of markers */ -.video-js .vjs-play-progress:before { - z-index: 101; -} - -/* time-control elements */ -.video-js .vjs-time-control { - min-width: 0.5rem; - padding: 0 0.25rem; -} - -.vjs-time-divider { - display: block; -} - -.vjs-duration { - display: block !important; -} - -/* big-play button */ -.video-js .vjs-big-play-button { - margin-left: auto; - margin-right: auto; - height: 2.5rem; - width: 2.5rem; - line-height: 2.5rem; - border-radius: 50%; - scale: 2; - border: 0.1rem solid #fff; -} - -/* captions button selection */ -.captions-on { - border-bottom: 0.45rem ridge; -} - -.vjs-custom-external-link .vjs-icon-placeholder { - font-size: 120%; -} diff --git a/app/controllers/master_files_controller.rb b/app/controllers/master_files_controller.rb index 31f2011ffe..c1aa6c40b0 100644 --- a/app/controllers/master_files_controller.rb +++ b/app/controllers/master_files_controller.rb @@ -71,14 +71,6 @@ def show end def embed - if can? :read, @master_file - @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 - - @player_width = "100%" - @player_height = "100%" respond_to do |format| format.html do response.headers.delete "X-Frame-Options" diff --git a/app/javascript/components/EmbeddedRamp.jsx b/app/javascript/components/EmbeddedRamp.jsx new file mode 100644 index 0000000000..ca90f80112 --- /dev/null +++ b/app/javascript/components/EmbeddedRamp.jsx @@ -0,0 +1,118 @@ +/* + * Copyright 2011-2024, 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 --- +*/ + +import React from 'react'; +import { + IIIFPlayer, + MediaPlayer +} from '@samvera/ramp'; +import 'video.js/dist/video-js.css'; +import "@samvera/ramp/dist/ramp.css"; +import './Ramp.scss'; + +const Ramp = ({ + urls, + media_object_id +}) => { + const [manifestUrl, setManifestUrl] = React.useState(''); + const [startCanvasId, setStartCanvasId] = React.useState(); + const [startCanvasTime, setStartCanvasTime] = React.useState(); + let interval; + + React.useEffect(() => { + const { base_url, fullpath_url } = urls; + // Split the current path from the time fragment in the format .../:id?t=time + let [fullpath, start_time] = fullpath_url.split('?t='); + // Split the current path in the format /master_files/:mf_id/embed + let [_, __, mf_id, ___] = fullpath.split('/'); + // Build the manifest URL + let url = `${base_url}/media_objects/${media_object_id}/manifest.json`; + + // Set start Canvas ID and start time in the state for Ramp + setStartCanvasId( + mf_id && mf_id != undefined + ? `${base_url}/media_objects/${media_object_id}/manifest/canvas/${mf_id}` + : undefined + ); + setStartCanvasTime( + start_time && start_time != undefined + ? parseFloat(start_time) + : undefined + ); + setManifestUrl(url); + + interval = setInterval(addControls, 500); + + // Clear interval upon component unmounting + return () => clearInterval(interval); + }, []); + + const addControls = () => { + let player = document.getElementById('iiif-media-player'); + if (player && player.player) { + let embeddedPlayer = player.player + + // Player API handling + window.addEventListener('message', function(event) { + var command = event.data.command; + + if (command=='play') embeddedPlayer.play(); + else if (command=='pause') embeddedPlayer.pause(); + else if (command=='toggle_loop') { + embeddedPlayer.loop() ? embeddedPlayer.loop(false): embeddedPlayer.loop(true); + embeddedPlayer.autoplay() ? embeddedPlayer.autoplay(false) : embeddedPlayer.autoplay(true); + } + else if (command=='set_offset') embeddedPlayer.currentTime(event.data.offset); // time is in seconds + else if (command=='get_offset') event.source.postMessage({'command': 'currentTime','currentTime': embeddedPlayer.currentTime()}, event.origin); + }); + + /* + Quality selector extends outside iframe for audio items, so we need to disable that control + and rely on the quality automatically selected by the user's system. + */ + if (embeddedPlayer.isAudio()) { embeddedPlayer.controlBar.removeChild('qualitySelector'); } + + // Create button component for "View in Repository" and add to control bar + let repositoryUrl = Object.values(urls).join('/').replace('/embed', ''); + let position = embeddedPlayer.isAudio() ? embeddedPlayer.controlBar.children_.length : embeddedPlayer.controlBar.children_.length - 1; + var viewInRepoButton = embeddedPlayer.getChild('ControlBar').addChild('button', { + clickHandler: function(event) { + window.open(repositoryUrl, '_blank').focus(); + } + }, position); + + viewInRepoButton.addClass('vjs-custom-external-link'); + viewInRepoButton.controlText('View in Repository'); + + // Add button icon + document.querySelector('.vjs-custom-external-link .vjs-icon-placeholder').innerHTML = '' + + // This function only needs to run once, so we clear the interval here + clearInterval(interval); + } + }; + + return ( + + + + ); +}; + +export default Ramp; \ No newline at end of file diff --git a/app/javascript/components/MediaObjectRamp.jsx b/app/javascript/components/MediaObjectRamp.jsx index 8dd48fecb9..047ae20f8b 100644 --- a/app/javascript/components/MediaObjectRamp.jsx +++ b/app/javascript/components/MediaObjectRamp.jsx @@ -1,5 +1,5 @@ /* - * Copyright 2011-2023, The Trustees of Indiana University and Northwestern + * Copyright 2011-2024, 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. * diff --git a/app/javascript/components/PlaylistRamp.jsx b/app/javascript/components/PlaylistRamp.jsx index 78fe26ebe6..7bbd1a2e09 100644 --- a/app/javascript/components/PlaylistRamp.jsx +++ b/app/javascript/components/PlaylistRamp.jsx @@ -1,5 +1,5 @@ /* - * Copyright 2011-2023, The Trustees of Indiana University and Northwestern + * Copyright 2011-2024, 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. * diff --git a/app/views/layouts/embed.html.erb b/app/views/layouts/embed.html.erb index c1cc70d1ce..2d789578c8 100644 --- a/app/views/layouts/embed.html.erb +++ b/app/views/layouts/embed.html.erb @@ -13,10 +13,6 @@ 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 --- %> -<% content_for :page_styles do %> - <%= stylesheet_link_tag 'embed' %> -<% end %> - @@ -28,6 +24,8 @@ Unless required by applicable law or agreed to in writing, software distributed <%= favicon_link_tag %> <%= stylesheet_link_tag "application", media: "all" %> + <%= stylesheet_pack_tag 'application' %> + <%= javascript_pack_tag 'application' %> <%= yield :page_styles %> <%= yield :additional_head_content %> <%= render "modules/google_analytics" %> diff --git a/app/views/master_files/_player.html.erb b/app/views/master_files/_player.html.erb index 35d78c9349..d4ab0100e3 100644 --- a/app/views/master_files/_player.html.erb +++ b/app/views/master_files/_player.html.erb @@ -14,28 +14,9 @@ Unless required by applicable law or agreed to in writing, software distributed --- END LICENSE_HEADER BLOCK --- %> -<%= render partial: 'modules/player/section', locals: {section: @master_file, section_info: @stream_info, f_start: @f_start, f_end: @f_end} %> - -<% content_for :page_scripts do %> - - - - -<% end %> +<%= react_component("EmbeddedRamp", + { + urls: { base_url: request.protocol+request.host_with_port, fullpath_url: request.fullpath }, + media_object_id: @master_file.media_object_id + } +) %> diff --git a/app/views/modules/player/_section.html.erb b/app/views/modules/player/_section.html.erb deleted file mode 100644 index ccb5526d6e..0000000000 --- a/app/views/modules/player/_section.html.erb +++ /dev/null @@ -1,20 +0,0 @@ -<%# -Copyright 2011-2024, 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 section.present? && section.derivatives.present? && @master_file.present? %> - <%= render partial: 'modules/player/video_js_element', locals: {section: section, section_info: section_info, f_start: @f_start, f_end: @f_end} %> - <% end %> -
diff --git a/app/views/modules/player/_video_js_element.html.erb b/app/views/modules/player/_video_js_element.html.erb deleted file mode 100644 index 38e869a58b..0000000000 --- a/app/views/modules/player/_video_js_element.html.erb +++ /dev/null @@ -1,117 +0,0 @@ -<%# -Copyright 2011-2024, 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_meta_properties(section) do %> - <% control_bar_options = if section_info[:is_video] - { - "children": [ - 'playToggle', - 'volumePanel', - 'progressControl', - 'currentTimeDisplay', - 'timeDivider', - 'durationDisplay', - 'subsCapsButton', - 'qualitySelector' - ], - fullscreenToggle: section_info[:is_video] ? true : false - } - else - { - "children": [ - 'playToggle', - 'volumePanel', - 'progressControl', - 'currentTimeDisplay', - 'timeDivider', - 'durationDisplay' - ] - } - end %> - - <% @videojs_options = { - "autoplay": false, - "width": @player_width || 480, - "height": @player_height || 270, - "bigPlayButton": section_info[:is_video] ? true : false, - "poster": section_info[:is_video] ? section_info[:poster_image] : false, - "preload": "auto", - "controlBar": control_bar_options, - "userActions": { - hotkeys: true - } - }.compact.to_json %> - -
- - <% section_info[:stream_hls].each do |hls| %> - - <% end %> - <% if section_info[:caption_paths].present? %> - <% section_info[:caption_paths].each do |c| %> - label="<%= c[:label] %>" <% end %> srclang="<%= c[:language] %>" kind="subtitles" type="<%= c[:mime_type] %>" src="<%= c[:path] %>"> - <% end %> - <% end %> - - <% if section_info[:is_video] %> - - <% end %> -
-<% end %> - -<% content_for :page_scripts do %> - -<% end %> diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb index e2b627dbdb..47360bd41e 100644 --- a/config/initializers/assets.rb +++ b/config/initializers/assets.rb @@ -13,8 +13,6 @@ # folder are already added. # Rails.application.config.assets.precompile += %w( admin.js admin.css ) -Rails.application.config.assets.precompile += %w( embed.css ) - # MediaElement 4 files Rails.application.config.assets.precompile += %w( select2.min.js select2.min.css ) From 5e0ac6f88886a4c73a4893675f1f6d5bff2b06b6 Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Mon, 8 Apr 2024 13:14:36 -0400 Subject: [PATCH 017/152] Use an embed specific JS/CSS pack to reduce overhead on embeds --- .../components/{ => embeds}/EmbeddedRamp.jsx | 3 +- app/javascript/components/embeds/Ramp.scss | 40 +++++++++++++++++++ app/javascript/packs/application.js | 7 +++- app/javascript/packs/embed.js | 18 +++++++++ app/views/layouts/embed.html.erb | 8 ++-- 5 files changed, 69 insertions(+), 7 deletions(-) rename app/javascript/components/{ => embeds}/EmbeddedRamp.jsx (98%) create mode 100644 app/javascript/components/embeds/Ramp.scss create mode 100644 app/javascript/packs/embed.js diff --git a/app/javascript/components/EmbeddedRamp.jsx b/app/javascript/components/embeds/EmbeddedRamp.jsx similarity index 98% rename from app/javascript/components/EmbeddedRamp.jsx rename to app/javascript/components/embeds/EmbeddedRamp.jsx index ca90f80112..7846a0d998 100644 --- a/app/javascript/components/EmbeddedRamp.jsx +++ b/app/javascript/components/embeds/EmbeddedRamp.jsx @@ -86,7 +86,7 @@ const Ramp = ({ if (embeddedPlayer.isAudio()) { embeddedPlayer.controlBar.removeChild('qualitySelector'); } // Create button component for "View in Repository" and add to control bar - let repositoryUrl = Object.values(urls).join('/').replace('/embed', ''); + let repositoryUrl = Object.values(urls).join('/').replace('/embed', ''); let position = embeddedPlayer.isAudio() ? embeddedPlayer.controlBar.children_.length : embeddedPlayer.controlBar.children_.length - 1; var viewInRepoButton = embeddedPlayer.getChild('ControlBar').addChild('button', { clickHandler: function(event) { @@ -95,6 +95,7 @@ const Ramp = ({ }, position); viewInRepoButton.addClass('vjs-custom-external-link'); + viewInRepoButton.el_.setAttribute('style', 'cursor: pointer;'); viewInRepoButton.controlText('View in Repository'); // Add button icon diff --git a/app/javascript/components/embeds/Ramp.scss b/app/javascript/components/embeds/Ramp.scss new file mode 100644 index 0000000000..6dc816a220 --- /dev/null +++ b/app/javascript/components/embeds/Ramp.scss @@ -0,0 +1,40 @@ +/* + * Copyright 2011-2024, 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 --- +*/ + +[class*="ramp--"] { + font-family: Arial, Helvetica, sans-serif; +} + +.ramp--media_player { + .video-js .vjs-big-play-button { + left: 55% !important; + } + @media (max-width: 585px) { + .video-js .vjs-big-play-button { + scale: 1.5; + } + + .video-js .vjs-control-bar { + font-size: 90% !important; + } + + // reduce player height to match with adjusted font-size + // for smaller screens + .video-js.vjs-audio { + min-height: 2.9em; + } + } +} diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js index 37464816a0..f6046a0933 100644 --- a/app/javascript/packs/application.js +++ b/app/javascript/packs/application.js @@ -25,6 +25,11 @@ // console.log('Hello World from Webpacker') // Support component names relative to this directory: -var componentRequireContext = require.context("components", true) + +/* + * For some reason including the `embeds` directory in this `require.context` breaks + * the player. Filtering out the directory allows everything to operate as intended. + */ +var componentRequireContext = require.context("components", true, /^(?!embed)/) var ReactRailsUJS = require("react_ujs") ReactRailsUJS.useContext(componentRequireContext) diff --git a/app/javascript/packs/embed.js b/app/javascript/packs/embed.js new file mode 100644 index 0000000000..0a89e1c8ae --- /dev/null +++ b/app/javascript/packs/embed.js @@ -0,0 +1,18 @@ +/* + * Copyright 2011-2024, 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 --- +*/ +var embedRequireContext = require.context("components/embeds", false) +var ReactRailsUJS = require("react_ujs") +ReactRailsUJS.useContext(embedRequireContext) \ No newline at end of file diff --git a/app/views/layouts/embed.html.erb b/app/views/layouts/embed.html.erb index 2d789578c8..d2e7777297 100644 --- a/app/views/layouts/embed.html.erb +++ b/app/views/layouts/embed.html.erb @@ -22,10 +22,9 @@ Unless required by applicable law or agreed to in writing, software distributed <%= csrf_meta_tags %> - <%= favicon_link_tag %> - <%= stylesheet_link_tag "application", media: "all" %> - <%= stylesheet_pack_tag 'application' %> - <%= javascript_pack_tag 'application' %> + <%= favicon_link_tag %> + <%= stylesheet_pack_tag 'embed' %> + <%= javascript_pack_tag 'embed' %> <%= yield :page_styles %> <%= yield :additional_head_content %> <%= render "modules/google_analytics" %> @@ -33,7 +32,6 @@ Unless required by applicable law or agreed to in writing, software distributed <%= yield %> - <%= javascript_include_tag "application" %> <%= yield :page_scripts %> From bdb41bc046218042322b8ac13985fa6411061331 Mon Sep 17 00:00:00 2001 From: Dananji Withana Date: Wed, 10 Apr 2024 09:51:45 -0400 Subject: [PATCH 018/152] Display comments, tags for an empty playlist (#5773) * Display comments, tags for an empty playlist * Ramp build with related changes --- app/javascript/components/PlaylistRamp.jsx | 81 ++++++++++++---------- package.json | 2 +- yarn.lock | 6 +- 3 files changed, 48 insertions(+), 41 deletions(-) diff --git a/app/javascript/components/PlaylistRamp.jsx b/app/javascript/components/PlaylistRamp.jsx index 7bbd1a2e09..8d8da5aed6 100644 --- a/app/javascript/components/PlaylistRamp.jsx +++ b/app/javascript/components/PlaylistRamp.jsx @@ -61,12 +61,12 @@ const Ramp = ({ let url = `${base_url}/playlists/${playlist_id}/manifest.json`; if (token) url += `?token=${token}`; - let [fullpath, position] = fullpath_url.split('?position='); - let start_canvas = playlist_item_ids[position - 1] + let [_, position] = fullpath_url.split('?position='); + let start_canvas = playlist_item_ids[position - 1]; setStartCanvasId( start_canvas && start_canvas != undefined - ? `${base_url}/playlists/${playlist_id}/manifest/canvas/${start_canvas}` - : undefined + ? `${base_url}/playlists/${playlist_id}/manifest/canvas/${start_canvas}` + : undefined ); setManifestUrl(url); @@ -97,40 +97,43 @@ const Ramp = ({ return ( - - -

{activeItemTitle}

- {activeItemSummary &&
{activeItemSummary}
} -
- - - - - - - - - - Markers - - - - - - - - - - Source Item Details - - - - -
+ {playlist_item_ids?.lenght > 0 && ( + + +

{activeItemTitle}

+ {activeItemSummary &&
{activeItemSummary}
} +
+ + + + + + + + + + Markers + + + + + + + + + + Source Item Details + + + + +
+ )} @@ -154,7 +157,7 @@ const Ramp = ({ } - +
@@ -162,8 +165,12 @@ const Ramp = ({
-

Playlist Items

- + {playlist_item_ids?.length > 0 && ( + +

Playlist Items

+ +
+ )} diff --git a/package.json b/package.json index 09b1ea8f57..a2325d5cd3 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "@babel/plugin-proposal-object-rest-spread": "^7.20.7", "@babel/preset-react": "^7.0.0", "@babel/runtime": "7", - "@samvera/ramp": "https://github.com/samvera-labs/ramp.git#v3.1.3", + "@samvera/ramp": "https://github.com/samvera-labs/ramp.git", "babel-plugin-macros": "^3.1.0", "babel-plugin-transform-react-remove-prop-types": "^0.4.24", "buffer": "^6.0.3", diff --git a/yarn.lock b/yarn.lock index 35e82e9dc3..eeedbe696a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1440,9 +1440,9 @@ estree-walker "^2.0.2" picomatch "^2.3.1" -"@samvera/ramp@https://github.com/samvera-labs/ramp.git#v3.1.3": - version "3.1.3" - resolved "https://github.com/samvera-labs/ramp.git#6c233c2cd668856b6bce1ce63076455a9e58b0f2" +"@samvera/ramp@https://github.com/samvera-labs/ramp.git": + version "3.1.0" + resolved "https://github.com/samvera-labs/ramp.git#e3b34b978f8f8448317b3717db2ae52b359481da" dependencies: "@rollup/plugin-json" "^6.0.1" "@silvermine/videojs-quality-selector" "^1.2.4" From 8b551edb5026a4f7ee87a6e743563e3743ab750e Mon Sep 17 00:00:00 2001 From: dwithana Date: Fri, 12 Apr 2024 11:53:53 -0400 Subject: [PATCH 019/152] Ramp v3.1.3 tag for 7.7.2 release --- package.json | 2 +- yarn.lock | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index a2325d5cd3..09b1ea8f57 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "@babel/plugin-proposal-object-rest-spread": "^7.20.7", "@babel/preset-react": "^7.0.0", "@babel/runtime": "7", - "@samvera/ramp": "https://github.com/samvera-labs/ramp.git", + "@samvera/ramp": "https://github.com/samvera-labs/ramp.git#v3.1.3", "babel-plugin-macros": "^3.1.0", "babel-plugin-transform-react-remove-prop-types": "^0.4.24", "buffer": "^6.0.3", diff --git a/yarn.lock b/yarn.lock index eeedbe696a..35e82e9dc3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1440,9 +1440,9 @@ estree-walker "^2.0.2" picomatch "^2.3.1" -"@samvera/ramp@https://github.com/samvera-labs/ramp.git": - version "3.1.0" - resolved "https://github.com/samvera-labs/ramp.git#e3b34b978f8f8448317b3717db2ae52b359481da" +"@samvera/ramp@https://github.com/samvera-labs/ramp.git#v3.1.3": + version "3.1.3" + resolved "https://github.com/samvera-labs/ramp.git#6c233c2cd668856b6bce1ce63076455a9e58b0f2" dependencies: "@rollup/plugin-json" "^6.0.1" "@silvermine/videojs-quality-selector" "^1.2.4" From ddd23adffa113491d64aff9fdcf26eb39a2ecb58 Mon Sep 17 00:00:00 2001 From: dwithana Date: Fri, 12 Apr 2024 15:42:35 -0400 Subject: [PATCH 020/152] Fix typo in playlist ReactJS wrapper code --- app/javascript/components/PlaylistRamp.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/components/PlaylistRamp.jsx b/app/javascript/components/PlaylistRamp.jsx index 8d8da5aed6..d478bd8b70 100644 --- a/app/javascript/components/PlaylistRamp.jsx +++ b/app/javascript/components/PlaylistRamp.jsx @@ -102,7 +102,7 @@ const Ramp = ({ - {playlist_item_ids?.lenght > 0 && ( + {playlist_item_ids?.length > 0 && (

{activeItemTitle}

From 3832fb135677da843942d9f233fb8ffc243bfa01 Mon Sep 17 00:00:00 2001 From: dwithana Date: Fri, 12 Apr 2024 16:24:28 -0400 Subject: [PATCH 021/152] Fix a11y color contrast for SCSS variable in branding --- app/assets/stylesheets/blacklight.scss | 15 ++++++++++----- app/assets/stylesheets/branding.scss | 5 ++--- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/app/assets/stylesheets/blacklight.scss b/app/assets/stylesheets/blacklight.scss index 4e4bd3f9f5..31b148f440 100644 --- a/app/assets/stylesheets/blacklight.scss +++ b/app/assets/stylesheets/blacklight.scss @@ -18,11 +18,13 @@ @import 'blacklight/blacklight'; -.result-thumbnail img, img.result-thumbnail { +.result-thumbnail img, +img.result-thumbnail { max-width: 160px; } -.no-icon img, img.no-icon { +.no-icon img, +img.no-icon { max-height: 90px; } @@ -31,7 +33,8 @@ @extend .btn-outline; } -#sort-dropdown, #per_page-dropdown { +#sort-dropdown, +#per_page-dropdown { .dropdown-item.active { color: $dark; background-color: $white; @@ -44,7 +47,9 @@ // Access control update modal form in Bookmarks #update_access_control_form { - .item-discovery, .item-access, .special-access { + .item-discovery, + .item-access, + .special-access { legend { border-bottom: 1px solid #e5e5e5; } @@ -55,7 +60,7 @@ // Needed to override btn-secondary-outline .applied-filter .remove:hover { color: white !important; - background-color: #f44336 !important; + background-color: #d14242 !important; } .page-item span.page-link { diff --git a/app/assets/stylesheets/branding.scss b/app/assets/stylesheets/branding.scss index 0ce2e48e41..888eb3d32e 100644 --- a/app/assets/stylesheets/branding.scss +++ b/app/assets/stylesheets/branding.scss @@ -53,7 +53,7 @@ $lightblue: #31708f; $info: #fbb040; $success: #429453; $warning: #bf841b; -$danger: #f44336; +$danger: #d14242; /** * Bootstrap variable overrides @@ -118,8 +118,7 @@ $navbar-light-toggle-border-color: $white; // Inverted navbar toggle $navbar-inverse-toggle-hover-bg: #e6e6e6; $navbar-inverse-toggle-icon-bar-bg: $lightgray; -$navbar-inverse-toggle-border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) - rgba(0, 0, 0, 0.25); +$navbar-inverse-toggle-border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); $grid-gutter-width: 16px; From b3e1faf84ec894dd201188ab67e6a15e49c85f60 Mon Sep 17 00:00:00 2001 From: dwithana Date: Thu, 25 Apr 2024 11:12:44 -0400 Subject: [PATCH 022/152] Use Rivet's danger color in branding --- app/assets/stylesheets/blacklight.scss | 2 +- app/assets/stylesheets/branding.scss | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/blacklight.scss b/app/assets/stylesheets/blacklight.scss index 31b148f440..0615b155f4 100644 --- a/app/assets/stylesheets/blacklight.scss +++ b/app/assets/stylesheets/blacklight.scss @@ -60,7 +60,7 @@ img.no-icon { // Needed to override btn-secondary-outline .applied-filter .remove:hover { color: white !important; - background-color: #d14242 !important; + background-color: #df3603 !important; } .page-item span.page-link { diff --git a/app/assets/stylesheets/branding.scss b/app/assets/stylesheets/branding.scss index 888eb3d32e..f376a9bb55 100644 --- a/app/assets/stylesheets/branding.scss +++ b/app/assets/stylesheets/branding.scss @@ -53,7 +53,7 @@ $lightblue: #31708f; $info: #fbb040; $success: #429453; $warning: #bf841b; -$danger: #d14242; +$danger: #df3603; /** * Bootstrap variable overrides From 8a05e15d94f0d0c082d9066d13d826fb649e5310 Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Mon, 25 Mar 2024 15:19:56 -0400 Subject: [PATCH 023/152] Add handling of transcript files to BatchIngest --- lib/avalon/batch/entry.rb | 45 ++++++++++++------ lib/avalon/batch/manifest.rb | 34 ++++++++----- .../example_batch_ingest/batch_manifest.xlsx | Bin 6661 -> 6739 bytes spec/lib/avalon/batch/entry_spec.rb | 25 ++++++++-- 4 files changed, 73 insertions(+), 31 deletions(-) diff --git a/lib/avalon/batch/entry.rb b/lib/avalon/batch/entry.rb index 53b023c808..6076e4cf1a 100644 --- a/lib/avalon/batch/entry.rb +++ b/lib/avalon/batch/entry.rb @@ -176,20 +176,20 @@ def self.offset_valid?( offset ) true end - def self.attach_datastreams_to_master_file( master_file, filename, captions ) + def self.attach_datastreams_to_master_file( master_file, filename, datastreams ) structural_file = "#{filename}.structure.xml" if FileLocator.new(structural_file).exist? master_file.structuralMetadata.content=FileLocator.new(structural_file).reader master_file.structuralMetadata.original_name = structural_file end - captions.each do |c| - next unless c.present? && c[:caption_file].present? && FileLocator.new(c[:caption_file]).exist? - filename = c[:caption_file].split('/').last - label = c[:caption_label].presence || filename - language = c[:caption_language].present? ? caption_language(c[:caption_language]) : Settings.caption_default.language - supplemental_file = SupplementalFile.new(label: label, tags: ['caption'], language: language) - supplemental_file.file.attach(io: FileLocator.new(c[:caption_file]).reader, filename: filename) - supplemental_file.save + datastreams.each do |ds| + next unless ds.present? + supplemental_file = case ds.keys[0].to_s + when /caption.*/ + process_datastream(ds, 'caption') + when /transcript.*/ + process_datastream(ds, 'transcript') + end master_file.supplemental_files += [supplemental_file] end end @@ -202,8 +202,8 @@ def process! # master_file.save(validate: false) #required: need id before setting media_object # master_file.media_object = media_object files = self.class.gatherFiles(file_spec[:file]) - captions = gather_captions(file_spec).values - self.class.attach_datastreams_to_master_file(master_file, file_spec[:file], captions) + datastreams = gather_datastreams(file_spec).values + self.class.attach_datastreams_to_master_file(master_file, file_spec[:file], datastreams) master_file.setContent(files, dropbox_dir: media_object.collection.dropbox_absolute_path) # Overwrite files hash with working file paths to pass to matterhorn @@ -277,15 +277,32 @@ def self.caption_language(language) end private_class_method :caption_language + def self.process_datastream(datastream, type) + file, label, language = ["_file", "_label", "_language"].map { |item| item.prepend(type).to_sym } + if datastream[file].present? && FileLocator.new(datastream[file]).exist? + # Build out file metadata + filename = datastream[file].split('/').last + label = datastream[label].presence || filename + language = datastream[language].present? ? caption_language(datastream[language]) : Settings.caption_default.language + machine_generated = datastream[:machine_generated].present? ? 'machine_generated' : nil + # Create SupplementalFile + supplemental_file = SupplementalFile.new(label: label, tags: [type, machine_generated].compact, language: language) + supplemental_file.file.attach(io: FileLocator.new(datastream[file]).reader, filename: filename) + supplemental_file.save + supplemental_file + end + end + private_class_method :process_datastream + private def hidden !!opts[:hidden] end - def gather_captions(file) - [] unless file.keys.any? { |k| k.to_s.include?('caption') } - file.select { |f| f.to_s.include?('caption') } + def gather_datastreams(file) + [] unless file.keys.any? { |k| k.to_s.include?('caption') || k.to_s.include?('transcript') } + file.select { |f| f.to_s.include?('caption') || f.to_s.include?('transcript') } end end end diff --git a/lib/avalon/batch/manifest.rb b/lib/avalon/batch/manifest.rb index a904622df8..60948b768a 100644 --- a/lib/avalon/batch/manifest.rb +++ b/lib/avalon/batch/manifest.rb @@ -21,7 +21,8 @@ class Manifest extend Forwardable EXTENSIONS = ['csv','xls','xlsx','ods'] - FILE_FIELDS = [:file,:label,:offset,:skip_transcoding,:absolute_location,:date_digitized, :caption_file, :caption_label, :caption_language] + FILE_FIELDS = [:file,:label,:offset,:skip_transcoding,:absolute_location,:date_digitized, :caption_file, :caption_label, :caption_language, + :transcript_file, :transcript_label, :machine_generated] SKIP_FIELDS = [:collection] def_delegators :@entries, :each @@ -102,16 +103,25 @@ def true?(value) not (value.to_s =~ /^(y(es)?|t(rue)?)$/i).nil? end - def process_captions(field, content, values, i) + def supplementing_files(field, content, values, i) + type = case field + when /caption.*/ + 'caption' + when /transcript.*/ + 'transcript' + else + 'supplemental' + end + if field.to_s.include?('file') - @caption_count += 1 - @caption_key = "caption_#{@caption_count}".to_sym - content.last[@caption_key] = {} - # Set file path to caption file - content.last[@caption_key][field] = path_to(values[i]) + @type_count += 1 + @key = "#{type}_#{@type_count}".to_sym + content.last[@key] = {} + # Set file path to caption/transcript file + content.last[@key][field] = path_to(values[i]) end - # Set caption metadata fields - content.last[@caption_key][field] ||= values[i] + # Set caption/transcript metadata fields + content.last[@key][field] ||= values[i] end def create_entries! @@ -129,13 +139,13 @@ def create_entries! content=[] fields = Hash.new { |h,k| h[k] = [] } - @caption_count = 0 + @type_count = 0 @field_names.each_with_index do |f,i| unless f.blank? || SKIP_FIELDS.include?(f) || values[i].blank? if FILE_FIELDS.include?(f) content << {} if f == :file - if f.to_s.include?('caption') - process_captions(f, content, values, i) + if ['caption', 'transcript', 'machine_generated'].any? { |type| f.to_s.include?(type) } + supplementing_files(f, content, values, i) next end content.last[f] = f == :skip_transcoding ? true?(values[i]) : values[i] diff --git a/spec/fixtures/dropbox/example_batch_ingest/batch_manifest.xlsx b/spec/fixtures/dropbox/example_batch_ingest/batch_manifest.xlsx index 77063dee718f6293340a527ab95ffe8ee4a03771..fbccfa3ae2bd4cd0e3bfa1754fcc83e95d972066 100644 GIT binary patch delta 4083 zcmZ8kXH=8R77ZN)F|+_uh0sEmA{~PCj#5GuDH5djP9UO&qLcuFC_Nyk)KH}fh!W|Y zAiW9FLlF^t=v(Vu?>j%f@2r_Uv*ye>Gkf+!=@IG6Ci*0#i~veXO2EOBfU3)2B9e=L zjWm%Q@oxw1O~OTFg-#+7My%HrXJ?cxe>hsn@xSs>7S^R|BYA1NQUope`@ z(0P=`i5nI2i!-%l0T%X82A0s?jO`p;)0cHB1Ej$GnA&~Wi0e++5$>M1uMq18Uvm^) zVOWLnTzdJo2OZ^Cinu|MIK$pm1(gfz88jxnW0|AQMtQbK^yOMvRyV&gy_pX8h$g^Q zs{V(0AYG^I7O`n{au+E>7p17@BvJ}Y?U}anx@`I?Te_^^0AE$`1Yf{0)Am`4p^a6p z%kbxt!qvA|rGCW@&mwSAF@pmsw_y8OoY76H;{GkE4YL#(#kf<$Ac(2_Rbz$dvY|j` z_3p4anRM++s+i0?Garo#0)QU{VyVkdTFkleey~K&^f@e5TIwx*qmxb=y+QERL?O?t z>8D~`cl9cI`Oh=n?97f8yhU#PLm=57Xb3tb-Y;kzpej{xi zKRu`A!^2u0bnIOJheYhNTEkFb5)-$4;jZSbVOrSl=`}hYypmD^mgHX1GM*coM^U zWX3H=&UDe$hR1tR57);O))$`EFNjqo9xBpsO+*!DZaJQcjTJrQ&3hXh#RaB z0{}@Z0KmVhzUCo0$DguCVJSx84n41Lm%rufJ^!fiS1(QP3>A%-QK!v4`8Uio>-KIFLA;R#8gq zI?sYdZ%Z{W4g4ctOikHg*toZ|ZZXe9RIJCJJE+)*2EY=`C^Lrof=d|Uz z&mSJg?M@lkSpwa|;-~P0o1OOEj_>tV&~GzcWn`pqygP95evv5Zszg+Y%N{KaGw=(q zepbXd`wY~Wt&OQa5rasLch6C?zXui-+SJ{bTNGk`nyGp;<9oIi_P`#x8lbllnV*{$ z{1XI{z0%QYnay!PPrg`&^dWtDIFpZ1YEER-;BA#?5@VY%YMlQjX>zBy=*#3t0vD-% zIn3y;dAv)@_pFMTd3iXg=ERrpM$q0qqyvG>q`5$EnlM`H}$_Z-NF zFRzcQsk=I7>1jg0RaxC3T58P}#@v?ZpzQ*KX&dr94iv-R0?-%Y>rzyR*GGDi<1m`!7y*p`R@pVm}1n{ZM#e;`P)wud?r0M z959VksPS$s$%k!kTg$A;un{2@2_3UZ!+zl$a!FnVO6!xWka#?wl1<0P{lV=};Sc3n zFU(LuBnvydD&{CP3NlF#5~?U>orA|DRQKr>pA%i`(g--B<7J@R<~1_Zz$VF2(=;}n zqN?KLCbba}5_FGGQT`0R*rWytIu+a|h75V@bch&QZEwY`GvQ86N)_%D!|fsfqD(y+ zH%Lmto;k;G`>NV->&%p5@X1ZV{4;>kaPJuIz&6-L8wE9K>FF3TbR()pExBp#j%6_1 z?WV}i;4lg{mycm0+59}5NOTl<6jtykRv||C;x_hudesbk%{2>&MWY*sY&^ z40TR9xv%n!MSy05h5Xko7Vr+viGr>z&!&F5o+D?2(F5;&G%5ik39N4qq>e^PucL5E|K zm8r{AUy;FL@5Of3AN>AEyT(F1;OqVHXCp0W$)BnMOBGSGzxLrYhW4))Dn`y;zlIe~_? zNsw>p>zH?r&4}L3UWh`)9)ClK*ed|i*k!3H=j*F;&_^0nd;+O`3%vzMD3p9w1oJUx zt>?{PT|60IPg0_|zb@>Ymlso1kiM55W$NA}NcwrDXz+oYw(^Jl6EAjY#M~3)E-uXr zp)JzjAkJ=!;eGy~ush)({!)*le<7!b9fPFnyxbT`VUYgi8ye|vH?;QE+X__tsIo?% zQ7b>V75(Y>N@N>HKofM3cHuCjyAkWXMeg>wLx{3V)kWNFT9~bnN7>=^ozt@)L5hyP z94?ML*yGp=!aK4)0pPXKG=u{%kJI&N21zYMiEdZm+Z&NE<^&mdxds{W=r}2XMx;6R z4Ei+nWfpM9q{^Z!5fza8Y<$7OlHx35-n&3M3=T#O2$al@!8fu?nQq&9J`>ROtHU4m z91%ukl?78ww=?pPI>a`Hg_ExJ7ks5t($_2Va-nyC9-D z5SH3I2Tu#`&L!M!lV2XS$yuEG4!Yt+HsrI|VD!GGN{0)28b$x;tf>3f19bfhId}Ov zcFIIpcd04(+?JFIyi8ehxjUuOi8I=;rGlGx0f7=BF+|#fbUeDB zaqEs|n;@NT(}<&N0iwT-!LoBO>+~Hp$F1wKm-8kUz3Nosa`5ag_IsB-7Tn(VHHbm% z2ixmcx)aj$Wyy0D?viZt#D#bgZ^d5Kso2y2Qk?M*D3dxuU)6faI*MGP>!S7*(rPYc z<)t{+%hRrP+8mmc*E5E}o$tUzu;1oYHMBZO-MpmY7DdiI+3dUvV5& zR^W&7WuV5}w>X;YE1W`BaeA+IFyMB7lxGHd)_02D#`7JA@sUN{*(1d1E#_O_Q)9P| z(mIT8jny&cEOc4{u&kO=qp5CXcuy*@$nh%&mC}|t(V-%xSNNDb`M7kel-a@c7qZ4b z8%M6uMOkD{-*!1yZ4F^Uu^sBU+He$F6?1r&=z)2fH0ZtZr5tPv2n^CaI8!ZO%DBg%>6{HtPCREMTEn;U{?63T+6Jyi(ABj5H@?FVm&1hgmhB@XNC+V zlM!&yDh|(tlS3AgS=vyYyIxx-&)@Zq)VF>>=Lrb3ENxGF8t(4&?q2y_`{gz!y1f?< zBbv+qSNbr(Uq*M~JEL)Q^oY+!of5!i0#nLP=~5A+b1hWK%ojiv#{<0KB>urcF{1@@ zu;V+--wg3mR9u>RRwBSP?z{sm@cNQiX$rmWQ*DYDbzSz797xgynac4QEIDY&0(*2L zCDL;~^S2wql}JZ{HO|XC&!V*KfyyP)IOa;8r>Px$`;Al<4#>N@h?Zn;M=GpHJXLM9 zIu#>UruS@KCn!UYHY1A3z%>s{Mgn5j!f7Q&xzQn?gmVS--oT7C>{VLg(GPy`Za^FC zE406rp^ML1BO6+qwzT?c-7WcIMn~*;ZOMU@5Pk9pn{wCf*Mx)*FGgM7mo+vC#~lMYM<+XJz{OjhxgrmI8^74 zinTPFjV2J|dHu3I4#wr)ecOJ&`YoOk3UHqt-$PVX7M+ef>t^#_v(MYr=T@q0#M4%K z%9rQZLl=W`h>BQbu1*%8?hbr>qe}EUOC^afQGP@Kz%uY6RsVXwqTLw85r#rG2_{#u zZg?s;#Q=CcMBnyPnmC^j2W=6L>jPKLn2@Elx619Qu}e9m0+qz>UCI!l>uh%t|Cop z=j)Y1at2@X44FqlNIGsmL@-$awQnHm*TYMnjgKFC1_#7x>r&u_Kr!?Y`ig8>8%5;S zGJ*8ve4#-`y8Vv(v0lsdkBFpm9^v0+S= z|Gm0JnPvaT*?Y_iv=_1#_Ky}omotn0>6&F`BSNBoFkk2T4fr1)T+qRVUiixj(E2O_ ze=x?oCt<@EJ$x4lx%w9Z02o52un7IZ{C&0<|AqO6o?_wsgZW=(FXs7u%KotvG#e}T zAI#su0{;PAcx7m7R<1u0M6j|EFEOI47@5%JtTca`T3FeL-l3;h`N=()e~7%wOMp<4P^9+`3K0+xa_PMnDJmTWktRrs^kyK1j#8xvB2okaDJm#M zFo1|u6{HiCjud_9JLlbd-~QP-yWg2DGxMGKMit@}7|aZ*z-$0oT3UeU1GhQ`At2T1 z+@Jtd0{u2fepI|bJ5oB8B-XpxV>GMkyUS6)yOu;(Sk_4cqVzN4KnyKU*KjcuxJXE% zGo=buE9AbXVJ6n?ss(uil=26@dl?c{Y>-lW_AY`9dro_$-0J77e~Q>E7$Q;NjpHZ% zI5)kOVx#*4+3Y>>K>5|XP9dvuT1v3U4!3jV^zdY1Ti}^y{ww+K>aZWsyL?ZAPy!z; z)UT!qmyjp&R98f^4_lS5tS|DVF}!?em=Gm!(7FYl)L2F_eP%QtR6wr~iZGyD8IV$b z%b4|;Hf*X9s?~>KVKssaHa1>`4&kL5&9iE&Zd~`Vnk-Vy?pAloCePSD+jl_wx<}4F zsFL5-5~H;9i@*<+u@>}(>Z4R<`%CVRJ@2jUkBwN}?(8)Yme8Hor!iA(-N5wP)uxwk z(W3d`tQqua%fUQd;9<}!^p5%Twg~$no2UV+AbTF}P} zT1QG&-CQmeVRpk1X^eeE94*HulNG~{)yGuM<5m;eOcZNhtTCVC z=~m_USbD0Mgyralxlqf;<~VhS>K^S>?C6A-y`GX(oR72!JS^-{nxVAKybamgBsn#2 zR)4MAdR%ZZc_Dy@N(Ss&*~e=fcBfPn_}Q%Os1#xxUbJFEwlH{GTHkYK zUv4%q$GL{Yvk}U*!7`xtP7|t3(Ol1+oitLj<8@BF2sAUKp@jq}-I)Rb06-J~@UL8N zNT=rhlkB8g8cFPMz@!R1vh(tn&2V`%o(v-w3ki9Xh*=N3w@1uX}}0oI~ACilZ6J zO5#X2bdn+{D=ZZ_Kr1?@D1YGc0u$E%%G%(ZJvFleKg%*oejwGU@$zu>oOX;)(R=Pf z>YmYn^0cK8T&Oq{B71;o!W7uNe=G4R%#Uf+=+!vfFgf(nf*;>a#vZl$xW1aj@drBct3Eeh3AEne zK}pf>M_g_6CV4Ppgf8xDi-CNx{@I{+k>hlTJ(fA%V5g^Aal_$9MYY-Cq88<0O8gp}-D#9Ki2L+hmNz)1aFRKvn!z$U ze;j+^dU=?9^mryq(Mz>dH`Sn{ny6pPAHVVLU(4;vfL007e2_bY*{p9!Zr*;lg>V`L zlkR*{yB?;$7+(zcY4} zpEAKKTKA0|B7E>-br;?l7EsHVs)$`$;QXr|QxWG0Wp_%Sr1GozXq~h#Tr#6^keKvN zSb(gp$q>x{0*e?qdKC{wB&!Vq5H12LiAl-H%RB-m##-Ot{z&!!Zf=2ck+}o7KlX;e zVPX=Lb#o8y?=2AVb)5zZwm%vMY1TX2^XZLWhl1&j`aqifLsD=d3eGz{z462+n$N$} zj!$o<#r-LNu%B@&&|-fGq`4aPR0H3-FN->%ER=M1I_|`ldAZGhY+@j!yAJ4d&*4Oo z4d&*q10LNOZ#-*1`%K`0m+V}z16i=W2u73kp--y2Djv6cKkg%GExbqK+%ow?3kx?i zW;Zfc*@;1T912N0NtKPH?LU42(hUDx3W@RQsMjF(KX1|?V-DP+i-MIKH1MM5%SE}r z_Ia|7Xs<@qonC00WyD))ib2J<`{0Ayv^LMQJzsBZtt>RcFS@9;vmNWW9|A?W?ZrZ3 z(u?aLvCWZBZ@21os;k#X72h50k9pxgB*h+4Vr*>0R9KKzGqFvnu-^Z5&!#h+;U~p^ zO!4-Y=ElcA65=n9=9#a~)7KnZ8cG}wU5)-6!UzR-2IjRShdgo$v81 zbpocn$k2wkg3todQO7s^t_CH%;+NftPu6t}==HTF7p|YEjT#Z4S$+18xopzHQrrh3 zr`%fnaz7K4Yp|29G9S++TJ^)WcYSSn3U<*D*j_wgFi#o1hS>2<4!&jNW zheKQFk=$Ndj$OMbWhE+h0luT)`Q5Ujl9Dl<9;fPQgKseRQDMsZnIXf@v7%_jaR|WHcX9G@-sgULsUnxqFCq|v!?k~1 zS7OFlzhi(k@tO>gqEObIZ7mOCofm`7-;aSK@jJSjlV{9e)<40>Hv9FE2F0Je_W}m= zQ>B-#1GJHA3LyzSg;kZ&@n`VO!w}>xF~KBx7b*z!moWljhukq3d>UYl@33iMXS%4( z!irU<87!H7%^AZ^x^13GANmt3l)TcvT@t~R=VF`W4;J*;C!Od=jF-b<^tLG+TX*yI z92D#4qQ9*osvNJ$gWtw(5yA-C&;1D@uQ+?t*Y)I~Crwh!0_XUq?fg_+DkTtOk`3b2 z0j%dgsn{T8-3qRh7u^d)C;ILb&Bf<#s#+;^#aXP) zWzT4KhD!1zYvxk21uC4pLhF_B{7N-U-8d+5seh~}GU#p0HF{=QY#aYmM;Xgd!t3jH zP%QF`aCwKAy{DsJvcu*xuC%?Zw3Di(Nt;Vo2VXXs;);y|79QD%EfWMlgPZhHU@Vb$ zAd|!$9iOSk5j4Ry)rbbei1D^7f3Pw~n& zz%4ypakmt6MoTfFow94Db+^!WLri7@3rEyAQ9d(A8(lm}YY;E@+fKWr+$p9R$?UUi zy?(RO1rW6h7u0V2nh4^kIXU_CJGaA=?~m1-_B0Bx|I2L>HH#3|*z}bgtbJRuYPwB< zl3><|*yQg93JE1>Ok-S{G>AIMZ$5U4Qa+r$GUsvBu6i_)TM%`L6seKa>VNK~4y3QE zPsh7ChmiQgIto*t^iupPC(XI~Xg!m)(uX^aXy68Bp1&v|N0=YgvU9FpYvdHvKLUu77sSt9#SlUAfn?X@g&6N!r?Rt_`0DhasQ!O);ZgZjhT8zx0}m?ZbQBXxD1@R zll6eFzeue-`~&wvDLz1O+9fmVa_ri7em}o!THRAYC$1RQmRpd3Wu=;rd^6KI!^y&QI zdQ`S%zCQtsF#GV{D+QVn&1@slP(5tBT_9_Y6gW@Z|Db_OgOl^rhE4sr4=VZGlCNNJ zd-AOae$92w?K>nN;)s3iZKJYJ%u~SzUHOPuYF2%FHYmsyEU3AN-JsV(&8@BRinb*A4R(h7@m zI066w_f9{>4E9fzN&3z<_D5y*BA)!0iNjIoKWeiPrNVfsI_>^$+DXSK=|5Xi>|CHw zYEl8UFzE(6jPEz caption_file, :caption_label => 'Sheephead Captions', :caption_language => 'English' }} - let(:entry_files) { [{ file: File.join(testdir, filename), offset: '00:00:00.500', label: 'Quis quo', date_digitized: '2015-10-30', skip_transcoding: false, caption_1: caption }] } + let(:transcript) {{ :transcript_file => caption_file, :transcript_label => 'Sheephead Transcript', :machine_generated => 'yes' }} + let(:entry_files) { [{ file: File.join(testdir, filename), offset: '00:00:00.500', label: 'Quis quo', date_digitized: '2015-10-30', skip_transcoding: false, caption_1: caption, transcript_1: transcript }] } it 'adds captions to masterfile' do expect(master_file.supplemental_file_captions).to be_present + expect(master_file.supplemental_files(tag: 'transcript')).to be_present end end end @@ -193,9 +195,11 @@ let(:filename) { File.join(Rails.root, 'spec/fixtures/dropbox/example_batch_ingest/assets/sheephead_mountain.mov') } let(:caption_file) { File.join(Rails.root, 'spec/fixtures/dropbox/example_batch_ingest/assets/sheephead_mountain.mov.vtt')} let(:caption) { [{ :caption_file => caption_file, :caption_label => 'Sheephead Captions', :caption_language => 'English' }] } + let(:transcript) { [{ :transcript_file => caption_file, :transcript_label => 'Sheephead Transcript' }]} before do - Avalon::Batch::Entry.attach_datastreams_to_master_file(master_file, filename, caption) + datastream = caption + transcript + Avalon::Batch::Entry.attach_datastreams_to_master_file(master_file, filename, datastream) end it 'should attach structural metadata' do @@ -205,16 +209,27 @@ expect(master_file.supplemental_file_captions).to be_present end - context 'with multiple captions' do + context 'with multiple captions and transcripts' do let(:caption) { [{ :caption_file => caption_file, :caption_label => 'Sheephead Captions', :caption_language => 'english' }, { :caption_file => caption_file, :caption_label => 'Second Caption', :caption_language => 'fre' }] } - it 'should attach all captions to master file' do + let(:transcript) { [{ :transcript_file => caption_file, :transcript_label => 'Sheephead Transcript' }, + { :transcript_file => caption_file, :machine_generated => 'yes' }] } + it 'should attach all captions and transcripts to master file' do expect(master_file.supplemental_file_captions).to be_present expect(master_file.supplemental_file_captions.count).to eq 2 + expect(master_file.supplemental_files(tag: 'transcript')).to be_present + expect(master_file.supplemental_files(tag: 'transcript').count).to eq 2 + end + + it 'assigns metadata properly' do expect(master_file.supplemental_file_captions[0].label).to eq 'Sheephead Captions' expect(master_file.supplemental_file_captions[1].label).to eq 'Second Caption' expect(master_file.supplemental_file_captions[0].language).to eq 'eng' expect(master_file.supplemental_file_captions[1].language).to eq 'fre' + expect(master_file.supplemental_files(tag: 'transcript')[0].label).to eq 'Sheephead Transcript' + expect(master_file.supplemental_files(tag: 'transcript')[1].label).to eq 'sheephead_mountain.mov.vtt' + expect(master_file.supplemental_files(tag: 'machine_generated').count).to eq 1 + expect(master_file.supplemental_files(tag: 'transcript')[1].tags).to include 'machine_generated' end end end From 02d5be9a71e3b9119e459acee6df19a4e3085569 Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Mon, 15 Apr 2024 10:19:20 -0400 Subject: [PATCH 024/152] Catch SupplementalFile save errors and inform user --- app/jobs/ingest_batch_entry_job.rb | 4 ++ app/mailers/batch_registries_mailer.rb | 7 +++- ...atch_registration_finished_mailer.html.erb | 26 +++++++++++++ lib/avalon/batch/entry.rb | 33 ++++++++++------- lib/avalon/batch/manifest.rb | 22 +++++------ spec/lib/avalon/batch/entry_spec.rb | 37 +++++++++++++------ spec/mailers/batch_registries_mailer_spec.rb | 12 ++++++ 7 files changed, 102 insertions(+), 39 deletions(-) diff --git a/app/jobs/ingest_batch_entry_job.rb b/app/jobs/ingest_batch_entry_job.rb index 4ec4e94c40..66faa2e947 100644 --- a/app/jobs/ingest_batch_entry_job.rb +++ b/app/jobs/ingest_batch_entry_job.rb @@ -70,6 +70,10 @@ def process_success(batch_entry, entry) batch_entry.media_object_pid = entry.media_object.id batch_entry.complete = true batch_entry.current_status = 'Completed' + unless entry.errors.empty? + batch_entry.error = true + batch_entry.error_message = entry.errors.full_messages.to_sentence + end batch_entry.save! # Delete pre-existing media object MediaObject.find(old_media_object_id).destroy if old_media_object_id.present? && MediaObject.exists?(old_media_object_id) diff --git a/app/mailers/batch_registries_mailer.rb b/app/mailers/batch_registries_mailer.rb index 0c4cf5e896..04529f9a5f 100644 --- a/app/mailers/batch_registries_mailer.rb +++ b/app/mailers/batch_registries_mailer.rb @@ -40,10 +40,13 @@ def batch_registration_finished_mailer(batch_registry) @user = User.find(@batch_registry.user_id) email = @user.email unless @user.nil? email ||= Settings.email.notification + @processed_items = BatchEntries.where(batch_registries_id: @batch_registry.id, complete: true).order(position: :asc) + @supplemental_file_errors = @processed_items.select { |be| be.error == true }.compact @error_items = BatchEntries.where(batch_registries_id: @batch_registry.id, error: true).order(position: :asc) - @completed_items = BatchEntries.where(batch_registries_id: @batch_registry.id, complete: true).order(position: :asc) + @error_items = @error_items - @supplemental_file_errors if !@supplemental_file_errors.empty? + @completed_items = @processed_items.select { |be| be.error == false }.compact prefix = "Success:" - prefix = "Errors Present:" unless @error_items.empty? + prefix = "Errors Present:" unless (@error_items.empty? && @supplemental_file_errors.empty?) collection_text = Admin::Collection.find(@batch_registry.collection).name if Admin::Collection.exists?(@batch_registry.collection) collection_text ||= "Collection" diff --git a/app/views/batch_registries_mailer/batch_registration_finished_mailer.html.erb b/app/views/batch_registries_mailer/batch_registration_finished_mailer.html.erb index 3a9a6fb76b..90b8c97f9b 100644 --- a/app/views/batch_registries_mailer/batch_registration_finished_mailer.html.erb +++ b/app/views/batch_registries_mailer/batch_registration_finished_mailer.html.erb @@ -36,6 +36,32 @@ Unless required by applicable law or agreed to in writing, software distributed <% end %> <% end %> +<% unless @supplemental_file_errors.empty? %> +
+ The following rows of your spreadsheet completed with errors: +
+ + + + + + + <% @supplemental_file_errors.each do |sf_error| %> + + + <% media_object = MediaObject.where(id: sf_error.media_object_pid).first %> + <% if media_object %> + <% link_url = media_object.permalink %> + <% link_url = media_object_url(media_object) if link_url.blank? %> + + <% else %> + + <% end %> + + + <% end %> +
RowMedia Object IDError
<%= sf_error.position + 2 %> <%= link_to(sf_error.media_object_pid, link_url) %> Item (<%= sf_error.media_object_pid %>) was created but no longer exists <%= sf_error.error_message %>
+<% end %> <% unless @completed_items.empty? %>
The following rows of your spreadsheet successfully completed: diff --git a/lib/avalon/batch/entry.rb b/lib/avalon/batch/entry.rb index 6076e4cf1a..0b683ad7b5 100644 --- a/lib/avalon/batch/entry.rb +++ b/lib/avalon/batch/entry.rb @@ -182,6 +182,7 @@ def self.attach_datastreams_to_master_file( master_file, filename, datastreams ) master_file.structuralMetadata.content=FileLocator.new(structural_file).reader master_file.structuralMetadata.original_name = structural_file end + errors = [] datastreams.each do |ds| next unless ds.present? supplemental_file = case ds.keys[0].to_s @@ -190,8 +191,14 @@ def self.attach_datastreams_to_master_file( master_file, filename, datastreams ) when /transcript.*/ process_datastream(ds, 'transcript') end + if supplemental_file.nil? + errors += [ds.values[0].to_s.split('/').last] + next + end master_file.supplemental_files += [supplemental_file] end + + errors end def process! @@ -203,7 +210,8 @@ def process! # master_file.media_object = media_object files = self.class.gatherFiles(file_spec[:file]) datastreams = gather_datastreams(file_spec).values - self.class.attach_datastreams_to_master_file(master_file, file_spec[:file], datastreams) + supplemental_file_errors = self.class.attach_datastreams_to_master_file(master_file, file_spec[:file], datastreams) + @errors.add(:supplemental_files, "Problem saving caption or transcript files: #{supplemental_file_errors}") unless supplemental_file_errors.empty? master_file.setContent(files, dropbox_dir: media_object.collection.dropbox_absolute_path) # Overwrite files hash with working file paths to pass to matterhorn @@ -279,18 +287,17 @@ def self.caption_language(language) def self.process_datastream(datastream, type) file, label, language = ["_file", "_label", "_language"].map { |item| item.prepend(type).to_sym } - if datastream[file].present? && FileLocator.new(datastream[file]).exist? - # Build out file metadata - filename = datastream[file].split('/').last - label = datastream[label].presence || filename - language = datastream[language].present? ? caption_language(datastream[language]) : Settings.caption_default.language - machine_generated = datastream[:machine_generated].present? ? 'machine_generated' : nil - # Create SupplementalFile - supplemental_file = SupplementalFile.new(label: label, tags: [type, machine_generated].compact, language: language) - supplemental_file.file.attach(io: FileLocator.new(datastream[file]).reader, filename: filename) - supplemental_file.save - supplemental_file - end + return nil unless datastream[file].present? && FileLocator.new(datastream[file]).exist? + + # Build out file metadata + filename = datastream[file].split('/').last + label = datastream[label].presence || filename + language = datastream[language].present? ? caption_language(datastream[language]) : Settings.caption_default.language + machine_generated = datastream[:machine_generated].present? ? 'machine_generated' : nil + # Create SupplementalFile + supplemental_file = SupplementalFile.new(label: label, tags: [type, machine_generated].compact, language: language) + supplemental_file.file.attach(io: FileLocator.new(datastream[file]).reader, filename: filename) + supplemental_file.save ? supplemental_file : nil end private_class_method :process_datastream diff --git a/lib/avalon/batch/manifest.rb b/lib/avalon/batch/manifest.rb index 60948b768a..b7ced32ce3 100644 --- a/lib/avalon/batch/manifest.rb +++ b/lib/avalon/batch/manifest.rb @@ -104,18 +104,15 @@ def true?(value) end def supplementing_files(field, content, values, i) - type = case field - when /caption.*/ - 'caption' - when /transcript.*/ - 'transcript' - else - 'supplemental' - end - if field.to_s.include?('file') - @type_count += 1 - @key = "#{type}_#{@type_count}".to_sym + @key = case field + when /caption.*/ + @caption_count += 1 + "caption_#{@caption_count}".to_sym + when /transcript.*/ + @transcript_count += 1 + "transcript_#{@transcript_count}".to_sym + end content.last[@key] = {} # Set file path to caption/transcript file content.last[@key][field] = path_to(values[i]) @@ -139,7 +136,8 @@ def create_entries! content=[] fields = Hash.new { |h,k| h[k] = [] } - @type_count = 0 + @caption_count = 0 + @transcript_count = 0 @field_names.each_with_index do |f,i| unless f.blank? || SKIP_FIELDS.include?(f) || values[i].blank? if FILE_FIELDS.include?(f) diff --git a/spec/lib/avalon/batch/entry_spec.rb b/spec/lib/avalon/batch/entry_spec.rb index 2e137e9727..1dff8c3674 100644 --- a/spec/lib/avalon/batch/entry_spec.rb +++ b/spec/lib/avalon/batch/entry_spec.rb @@ -100,19 +100,19 @@ describe '#gatherFiles' do it 'should return a hash of files keyed with their quality' do - expect(Avalon::Batch::Entry.gatherFiles(filename)).to hash_match derivative_hash + expect(Avalon::Batch::Entry.gatherFiles(filename)).to hash_match derivative_hash end end describe '#derivativePaths' do it 'should return the paths to all derivative files that exist' do - expect(Avalon::Batch::Entry.derivativePaths(filename)).to eq derivative_paths + expect(Avalon::Batch::Entry.derivativePaths(filename)).to eq derivative_paths end end describe '#derivativePath' do it 'should insert supplied quality into filename' do - expect(Avalon::Batch::Entry.derivativePath(filename, 'low')).to eq filename_low + expect(Avalon::Batch::Entry.derivativePath(filename, 'low')).to eq filename_low end end end @@ -188,12 +188,22 @@ expect(master_file.supplemental_files(tag: 'transcript')).to be_present end end + + context 'invalid supplemental file' do + let(:caption_file) { File.join(Rails.root, 'spec/fixtures/dropbox/example_batch_ingest/assets/sheephead_mountain.mov.structure.xml') } + let(:caption) {{ :caption_file => caption_file, :caption_label => 'Sheephead Captions', :caption_language => 'English' }} + let(:entry_files) { [{ file: File.join(testdir, filename), offset: '00:00:00.500', label: 'Quis quo', date_digitized: '2015-10-30', skip_transcoding: false, caption_1: caption }] } + + it 'logs an error' do + expect(entry.errors).not_to be_empty + end + end end describe '#attach_datastreams_to_master_file' do let(:master_file) { FactoryBot.create(:master_file) } let(:filename) { File.join(Rails.root, 'spec/fixtures/dropbox/example_batch_ingest/assets/sheephead_mountain.mov') } - let(:caption_file) { File.join(Rails.root, 'spec/fixtures/dropbox/example_batch_ingest/assets/sheephead_mountain.mov.vtt')} + let(:caption_file) { File.join(Rails.root, 'spec/fixtures/dropbox/example_batch_ingest/assets/sheephead_mountain.mov.vtt') } let(:caption) { [{ :caption_file => caption_file, :caption_label => 'Sheephead Captions', :caption_language => 'English' }] } let(:transcript) { [{ :transcript_file => caption_file, :transcript_label => 'Sheephead Transcript' }]} @@ -206,7 +216,10 @@ expect(master_file.structuralMetadata.has_content?).to be_truthy end it 'should attach captions' do - expect(master_file.supplemental_file_captions).to be_present + expect(master_file.has_captions?).to eq true + end + it 'should attach transcripts' do + expect(master_file.has_transcripts?).to eq true end context 'with multiple captions and transcripts' do @@ -215,10 +228,10 @@ let(:transcript) { [{ :transcript_file => caption_file, :transcript_label => 'Sheephead Transcript' }, { :transcript_file => caption_file, :machine_generated => 'yes' }] } it 'should attach all captions and transcripts to master file' do - expect(master_file.supplemental_file_captions).to be_present + expect(master_file.has_captions?).to eq true expect(master_file.supplemental_file_captions.count).to eq 2 - expect(master_file.supplemental_files(tag: 'transcript')).to be_present - expect(master_file.supplemental_files(tag: 'transcript').count).to eq 2 + expect(master_file.has_transcripts?).to eq true + expect(master_file.supplemental_file_transcripts.count).to eq 2 end it 'assigns metadata properly' do @@ -226,10 +239,10 @@ expect(master_file.supplemental_file_captions[1].label).to eq 'Second Caption' expect(master_file.supplemental_file_captions[0].language).to eq 'eng' expect(master_file.supplemental_file_captions[1].language).to eq 'fre' - expect(master_file.supplemental_files(tag: 'transcript')[0].label).to eq 'Sheephead Transcript' - expect(master_file.supplemental_files(tag: 'transcript')[1].label).to eq 'sheephead_mountain.mov.vtt' - expect(master_file.supplemental_files(tag: 'machine_generated').count).to eq 1 - expect(master_file.supplemental_files(tag: 'transcript')[1].tags).to include 'machine_generated' + expect(master_file.supplemental_file_transcripts[0].label).to eq 'Sheephead Transcript' + expect(master_file.supplemental_file_transcripts[1].label).to eq 'sheephead_mountain.mov.vtt' + expect(master_file.supplemental_file_transcripts[0].tags).to_not include 'machine_generated' + expect(master_file.supplemental_file_transcripts[1].tags).to include 'machine_generated' end end end diff --git a/spec/mailers/batch_registries_mailer_spec.rb b/spec/mailers/batch_registries_mailer_spec.rb index 88391e176b..4e6b2abbad 100644 --- a/spec/mailers/batch_registries_mailer_spec.rb +++ b/spec/mailers/batch_registries_mailer_spec.rb @@ -87,6 +87,18 @@ expect(email).to have_body_text("Item (#{deleted_media_object.id}) was created but no longer exists") end + it 'indicates when a batch has completed with non-failing errors' do + FactoryBot.create(:batch_entries, batch_registries: batch_registries, media_object_pid: media_object.id, complete: true, error: true, error_message: "Problem Saving Supplemental File") + email = BatchRegistriesMailer.batch_registration_finished_mailer(batch_registries) + expect(email.to).to include(manager.email) + expect(email.subject).to include batch_registries.file_name + expect(email.subject).to include collection.name + expect(email).to have_body_text(batch_registries.file_name) + expect(email).to have_body_text("") + expect(email).to have_body_text(media_object.id) + expect(email).to have_body_text("Problem Saving Supplemental File") + end + it 'works when the collection has been deleted already' do collection.destroy email = BatchRegistriesMailer.batch_registration_finished_mailer(batch_registries) From a5dc0eccd54f37c3765cfdc08565fcce412fc6a9 Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Wed, 17 Apr 2024 09:16:27 -0400 Subject: [PATCH 025/152] Improve variable naming in .process_datastream --- lib/avalon/batch/entry.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/avalon/batch/entry.rb b/lib/avalon/batch/entry.rb index 0b683ad7b5..3203fe2b15 100644 --- a/lib/avalon/batch/entry.rb +++ b/lib/avalon/batch/entry.rb @@ -286,17 +286,17 @@ def self.caption_language(language) private_class_method :caption_language def self.process_datastream(datastream, type) - file, label, language = ["_file", "_label", "_language"].map { |item| item.prepend(type).to_sym } - return nil unless datastream[file].present? && FileLocator.new(datastream[file]).exist? + file_key, label_key, language_key = ["_file", "_label", "_language"].map { |item| item.prepend(type).to_sym } + return nil unless datastream[file_key].present? && FileLocator.new(datastream[file_key]).exist? # Build out file metadata - filename = datastream[file].split('/').last - label = datastream[label].presence || filename - language = datastream[language].present? ? caption_language(datastream[language]) : Settings.caption_default.language + filename = datastream[file_key].split('/').last + label = datastream[label_key].presence || filename + language = datastream[language_key].present? ? caption_language(datastream[language_key]) : Settings.caption_default.language machine_generated = datastream[:machine_generated].present? ? 'machine_generated' : nil # Create SupplementalFile supplemental_file = SupplementalFile.new(label: label, tags: [type, machine_generated].compact, language: language) - supplemental_file.file.attach(io: FileLocator.new(datastream[file]).reader, filename: filename) + supplemental_file.file.attach(io: FileLocator.new(datastream[file_key]).reader, filename: filename) supplemental_file.save ? supplemental_file : nil end private_class_method :process_datastream From 540259b07d4d9f21a610062d7d818186d1108c07 Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Mon, 29 Apr 2024 11:53:58 -0400 Subject: [PATCH 026/152] Move 'Has Captions' and 'Has Transcripts' to manager only facets --- app/controllers/catalog_controller.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/catalog_controller.rb b/app/controllers/catalog_controller.rb index be83708bda..8fd86e5493 100644 --- a/app/controllers/catalog_controller.rb +++ b/app/controllers/catalog_controller.rb @@ -88,8 +88,6 @@ class CatalogController < ApplicationController config.add_facet_field 'collection_ssim', label: 'Collection', limit: 5 config.add_facet_field 'unit_ssim', label: 'Unit', limit: 5 config.add_facet_field 'language_ssim', label: 'Language', limit: 5 - config.add_facet_field 'has_captions_bsi', label: 'Has Captions', helper_method: :display_has_caption_or_transcript - config.add_facet_field 'has_transcripts_bsi', label: 'Has Transcripts', helper_method: :display_has_caption_or_transcript # Hide these facets if not a Collection Manager config.add_facet_field 'workflow_published_sim', label: 'Published', limit: 5, if: Proc.new {|context, config, opts| Ability.new(context.current_user, context.user_session).can? :create, MediaObject}, group: "workflow" config.add_facet_field 'avalon_uploader_ssi', label: 'Created by', limit: 5, if: Proc.new {|context, config, opts| Ability.new(context.current_user, context.user_session).can? :create, MediaObject}, group: "workflow" @@ -101,6 +99,8 @@ class CatalogController < ApplicationController config.add_facet_field 'read_access_virtual_group_ssim', label: 'External Group', limit: 5, if: Proc.new {|context, config, opts| Ability.new(context.current_user, context.user_session).can? :create, MediaObject}, group: "workflow", helper_method: :vgroup_display config.add_facet_field 'date_digitized_ssim', label: 'Date Digitized', limit: 5, if: Proc.new {|context, config, opts| Ability.new(context.current_user, context.user_session).can? :create, MediaObject}, group: "workflow"#, partial: 'blacklight/hierarchy/facet_hierarchy' config.add_facet_field 'date_ingested_ssim', label: 'Date Ingested', limit: 5, if: Proc.new {|context, config, opts| Ability.new(context.current_user, context.user_session).can? :create, MediaObject}, group: "workflow" + config.add_facet_field 'has_captions_bsi', label: 'Has Captions', if: Proc.new {|context, config, opts| Ability.new(context.current_user, context.user_session).can? :create, MediaObject}, group: "workflow", helper_method: :display_has_caption_or_transcript + config.add_facet_field 'has_transcripts_bsi', label: 'Has Transcripts', if: Proc.new {|context, config, opts| Ability.new(context.current_user, context.user_session).can? :create, MediaObject}, group: "workflow", helper_method: :display_has_caption_or_transcript # Have BL send all facet field names to Solr, which has been the default # previously. Simply remove these lines if you'd rather use Solr request From 1d8dbae753996df3efa5cb479645b25dcadfd348 Mon Sep 17 00:00:00 2001 From: Chris Colvard Date: Fri, 3 May 2024 10:13:21 -0400 Subject: [PATCH 027/152] Use latest 3.x zookeeper Solr 9.6 was released earlier this week and appears to cause a compatibility issue with the version of zookeeper we were pinning to. --- .circleci/config.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index fc4cead7a9..61357bf13d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -20,7 +20,9 @@ jobs: - image: ualbertalib/docker-fcrepo4:4.7 environment: CATALINA_OPTS: '-Djava.awt.headless=true -Dfile.encoding=UTF-8 -server -Xms512m -Xmx1024m -XX:NewSize=256m -XX:MaxNewSize=256m -XX:PermSize=256m -XX:MaxPermSize=256m -XX:+DisableExplicitGC' - - image: zookeeper:3.4 + - image: zookeeper:3.9 + environment: + ZOO_ADMINSERVER_ENABLED: false - image: solr:9 environment: VERBOSE: yes From e2e8c7b63970f55ffb20c7ec4be7d55b49f9ef07 Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Thu, 2 May 2024 14:55:15 -0400 Subject: [PATCH 028/152] Allow download of high quality derivate by managers --- app/controllers/master_files_controller.rb | 46 ++++++++++++ app/services/file_locator.rb | 21 +++++- app/views/media_objects/_file_upload.html.erb | 5 ++ config/routes.rb | 3 + config/settings.yml | 3 + .../master_files_controller_spec.rb | 70 +++++++++++++++++++ spec/services/file_locator_spec.rb | 34 +++++++++ 7 files changed, 181 insertions(+), 1 deletion(-) diff --git a/app/controllers/master_files_controller.rb b/app/controllers/master_files_controller.rb index c1aa6c40b0..ac653225bc 100644 --- a/app/controllers/master_files_controller.rb +++ b/app/controllers/master_files_controller.rb @@ -326,6 +326,34 @@ def transcript send_data @supplemental_file.file.download, filename: @supplemental_file.file.filename.to_s, type: @supplemental_file.file.content_type, disposition: 'inline' end + def download_derivative + authorize! :manage, @master_file.media_object.collection + + begin + path = derivative_path + + unless FileLocator.new(path).exist? + flash[:error] = "Unable to find or access derivative file." + redirect_back(fallback_location: edit_media_object_path(@master_file.media_object)) + return + end + + case path + when /^s3:/ + # Use an AWS presigned URL to facilitate direct download of the derivative to avoid + # having to download the file to the server as a tmp file and then sending that to + # the client. Doing this reduces latency and server load. + redirect_to FileLocator::S3File.new(path).download_url + else + send_file path, filename: File.basename(path), disposition: 'attachment' + end + rescue => error + Rails.logger.error(error.class.to_s + ': ' + error.message + '\n' + error.backtrace.join('\n')) + flash[:error] = "A problem was encountered while attempting to download derivative file. Please contact your support person if this issue persists." + redirect_back(fallback_location: edit_media_object_path(@master_file.media_object)) + end + end + protected def set_masterfile if params[:id].blank? || (not MasterFile.exists?(params[:id])) @@ -392,4 +420,22 @@ def master_file_params def samples_per_frame Settings.waveform.sample_rate * Settings.waveform.finest_zoom / Settings.waveform.player_width end + + def derivative_path + derivative = @master_file.derivatives.find { |d| d.quality == "high" } + # There is no guarantee that the full path contained in the absolute_location attribute is accurate. + # However, the sub-path in location_url should be consistent between server moves. The location_url + # does not include the extension, so we retrieve the extension from absolute_location. + extension = File.extname(derivative.absolute_location) + location = derivative.location_url + extension + # Derivative files that have been moved from their original location/server should have been moved into + # the new root path for derivatives/encodings. Combining the subpath and extension from the existing + # record with the derivative path defined in our environment variables, we should be able to retrieve the + # derivative files, regardless of how many times they have been rehomed. + if Settings.encoding.derivative_bucket + File.join('s3://', Settings.encoding.derivative_bucket, location) + else + File.join(ENV["ENCODE_WORK_DIR"], location).to_s + end + end end diff --git a/app/services/file_locator.rb b/app/services/file_locator.rb index d26e401b9c..2b7db4077f 100644 --- a/app/services/file_locator.rb +++ b/app/services/file_locator.rb @@ -24,7 +24,7 @@ class S3File def initialize(uri) uri = Addressable::URI.parse(uri) @bucket = Addressable::URI.unencode(uri.host) - @key = Addressable::URI.unencode(ActiveEncode.sanitize_uri(uri)).sub(%r(^/*(.+)/*$),'\1') + @key = Addressable::URI.unencode(ActiveEncode.sanitize_uri(uri)).sub(%r(^/*(.+)/*$), '\1') end def object @@ -38,6 +38,25 @@ def local_file ensure @local_file.close end + + def download_url + # Minio: + # Because the request to the generated URL will be coming from an external context, we need + # to generate the presigned URL with a publicly accessible endpoint. To accomplish this, we + # create a new client definition using the public_host address and use that for access to the + # object. + if Settings.minio.present? && Settings.minio.public_host.present? + client = Aws::S3::Client.new(endpoint: Settings.minio.public_host, + access_key_id: Settings.minio.access, + secret_access_key: Settings.minio.secret, + region: ENV["AWS_REGION"]) + download_object = Aws::S3::Object.new(bucket_name: bucket, key: key, client: client) + end + # Other AWS implementations should be fine with the default client so we can use the default object. + download_object ||= object + # Presigned URL is set to expire in 1 hour. Is that too long? + download_object.presigned_url(:get, expires_in: 3600, response_content_disposition: "attachment; filename=#{File.basename(key)}") + end end def initialize(source) diff --git a/app/views/media_objects/_file_upload.html.erb b/app/views/media_objects/_file_upload.html.erb index 1a0348623a..e5e93bb6ee 100644 --- a/app/views/media_objects/_file_upload.html.erb +++ b/app/views/media_objects/_file_upload.html.erb @@ -91,6 +91,11 @@ Unless required by applicable law or agreed to in writing, software distributed Move + <% if Settings.derivative.allow_download && (current_ability.can? :manage, @media_object.collection || current_ability.is_administrator?) %> + + <%= link_to "Download", download_derivative_master_file_path(section.id), class: 'btn btn-sm btn-outline', target: '_self' %> + + <% end %>
- - -<% content_for :page_scripts do %> - -<% end %> diff --git a/app/views/media_objects/_item_view.html.erb b/app/views/media_objects/_item_view.html.erb index 80cf5cf86c..efb7f1794e 100644 --- a/app/views/media_objects/_item_view.html.erb +++ b/app/views/media_objects/_item_view.html.erb @@ -61,11 +61,20 @@ Unless required by applicable law or agreed to in writing, software distributed // When viewing video on smaller devices scroll to page content to fully // display the video player $(document).ready(function () { + const mediaObjectId = <%= @media_object.id.to_json.html_safe %>; const sectionIds = <%= @media_object.ordered_master_file_ids.to_json.html_safe %>; const transcriptSections = <%= @media_object.sections_with_files(tag: 'transcript').to_json.html_safe %>; - let scrollInterval = setInterval(autoScroll, 500); - let timeCheck = setInterval(initTranscriptCheck, 500); + // Enable action buttons after derivative is loaded + setInterval(initActionButtons, 500); + function initActionButtons() { + let player = document.getElementById('iiif-media-player'); + if (player) { + addActionButtonListeners(player, mediaObjectId, sectionIds); + } + } + + let scrollInterval = setInterval(autoScroll, 500); function autoScroll () { const isVideo = <%= @currentStream ? @currentStreamInfo[:is_video] : false %>; const player = document.getElementsByTagName('video'); @@ -81,6 +90,7 @@ Unless required by applicable law or agreed to in writing, software distributed } } + let timeCheck = setInterval(initTranscriptCheck, 500); function initTranscriptCheck() { let player = document.getElementById('iiif-media-player'); if(player) { diff --git a/app/views/media_objects/_share.html.erb b/app/views/media_objects/_share.html.erb index 893bec7dbf..3b2139e0b6 100644 --- a/app/views/media_objects/_share.html.erb +++ b/app/views/media_objects/_share.html.erb @@ -23,62 +23,3 @@ Unless required by applicable law or agreed to in writing, software distributed <%= render_conditional_partials :share, section:'tab-content' %>
-<% content_for :page_scripts do %> - - - -<% end %> diff --git a/app/views/media_objects/_thumbnail.html.erb b/app/views/media_objects/_thumbnail.html.erb index 41e6cf7999..bf8fae99aa 100644 --- a/app/views/media_objects/_thumbnail.html.erb +++ b/app/views/media_objects/_thumbnail.html.erb @@ -15,7 +15,7 @@ Unless required by applicable law or agreed to in writing, software distributed %>
-
@@ -41,107 +41,3 @@ Unless required by applicable law or agreed to in writing, software distributed
- -<% content_for :page_scripts do %> - -<% end %> diff --git a/app/views/media_objects/_timeline.html.erb b/app/views/media_objects/_timeline.html.erb index c857e4344b..223c8c3340 100644 --- a/app/views/media_objects/_timeline.html.erb +++ b/app/views/media_objects/_timeline.html.erb @@ -37,118 +37,3 @@ Unless required by applicable law or agreed to in writing, software distributed
- -<% content_for :page_scripts do %> - - - -<% end %> From 17609156b8895af56624efa8faa94a94eb659907 Mon Sep 17 00:00:00 2001 From: Mason Ballengee <68433277+masaball@users.noreply.github.com> Date: Wed, 8 May 2024 09:19:13 -0400 Subject: [PATCH 035/152] Default app_controller.raise_on_connection_error to false Co-authored-by: Chris Colvard --- config/settings.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/settings.yml b/config/settings.yml index a26318f7e7..315d171cda 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -46,7 +46,7 @@ solr: snitch: app_controller: solr_and_fedora: - raise_on_connection_error: true + raise_on_connection_error: false app_job: solr_and_fedora: raise_on_connection_error: true From c4802f7084626618c721cade4aa18d16603efecf Mon Sep 17 00:00:00 2001 From: cjcolvar Date: Wed, 8 May 2024 09:20:37 -0400 Subject: [PATCH 036/152] Allow setting default headers in fedora.yml config --- .../initializers/active_fedora_reindexing.rb | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/config/initializers/active_fedora_reindexing.rb b/config/initializers/active_fedora_reindexing.rb index f6ef76d087..1a35688041 100644 --- a/config/initializers/active_fedora_reindexing.rb +++ b/config/initializers/active_fedora_reindexing.rb @@ -1,4 +1,24 @@ ActiveFedora::Fedora.class_eval do + def header_options + @config[:headers] + end + + def authorized_connection + options = {} + options[:ssl] = ssl_options if ssl_options + options[:request] = request_options if request_options + options[:headers] = header_options if header_options + Faraday.new(host, options) do |conn| + conn.response :encoding # use Faraday::Encoding middleware + conn.adapter Faraday.default_adapter # net/http + if Gem::Version.new(Faraday::VERSION) < Gem::Version.new('2') + conn.request :basic_auth, user, password + else + conn.request :authorization, :basic, user, password + end + end + end + def ntriples_connection authorized_connection.tap { |conn| conn.headers['Accept'] = 'application/n-triples' } end From f9cfe170cf58491b2e414e4ada302bfe6b05b518 Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Wed, 8 May 2024 14:29:41 -0400 Subject: [PATCH 037/152] Add ability to chunk transcript files --- app/models/supplemental_file.rb | 10 ++++ config/initializers/avalon.rb | 1 + lib/avalon/transcript_parser.rb | 42 ++++++++++++++++ lib/parse_docx.rb | 29 +++++++++++ spec/fixtures/chunk_test.docx | Bin 0 -> 6215 bytes spec/fixtures/chunk_test.srt | 46 ++++++++++++++++++ spec/fixtures/chunk_test.txt | 66 ++++++++++++++++++++++++++ spec/fixtures/chunk_test.vtt | 48 +++++++++++++++++++ spec/models/supplemental_file_spec.rb | 44 +++++++++++++++++ 9 files changed, 286 insertions(+) create mode 100644 lib/avalon/transcript_parser.rb create mode 100644 lib/parse_docx.rb create mode 100644 spec/fixtures/chunk_test.docx create mode 100644 spec/fixtures/chunk_test.srt create mode 100644 spec/fixtures/chunk_test.txt create mode 100644 spec/fixtures/chunk_test.vtt diff --git a/app/models/supplemental_file.rb b/app/models/supplemental_file.rb index 34ae5df198..24bc103326 100644 --- a/app/models/supplemental_file.rb +++ b/app/models/supplemental_file.rb @@ -12,6 +12,8 @@ # specific language governing permissions and limitations under the License. # --- END LICENSE_HEADER BLOCK --- +require 'avalon/transcript_parser' + class SupplementalFile < ApplicationRecord has_one_attached :file @@ -65,4 +67,12 @@ def self.convert_from_srt(srt) "WEBVTT\n\n#{conversion}".strip end + + # private + def self.segment_transcript transcript + normalized_transcript = Avalon::TranscriptParser.normalize_transcript(transcript) + chunked_transcript = normalized_transcript.split(/\n\n+/) + + chunked_transcript.map(&:strip).map { |cue| cue.gsub("\n", " ") }.compact + end end diff --git a/config/initializers/avalon.rb b/config/initializers/avalon.rb index d565fffa9d..4a5c97684e 100644 --- a/config/initializers/avalon.rb +++ b/config/initializers/avalon.rb @@ -1,6 +1,7 @@ require 'string_additions' require 'avalon/errors' require 'day_hour_string' +require 'parse_docx' # Loads configuration information from the YAML file and then sets up the # dropbox # diff --git a/lib/avalon/transcript_parser.rb b/lib/avalon/transcript_parser.rb new file mode 100644 index 0000000000..0a0187d3af --- /dev/null +++ b/lib/avalon/transcript_parser.rb @@ -0,0 +1,42 @@ +# Copyright 2011-2024, 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 Avalon + module TranscriptParser + TEXT_TYPE = ['text/vtt', 'text/srt', 'text/plain', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'] + # Passed in transcript_file must be instance of ActiveStorage::Attached::One + def self.transcript_plaintext transcript_file + mime_type = transcript_file.content_type + # Transcripts can have arbitrary files imported. We need to verify that the transcript + # is a text based file before attempting processing. + return unless TEXT_TYPE.include? mime_type + # Docx files are a zip file containing XML files so require specialized handling to retrieve the content. + plaintext = Zip::File.open_buffer(transcript_file.download).parse_docx if mime_type == 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' + # SRT and VTT files are plaintext, so it is sufficient to just download the content without additional handling. + plaintext ||= transcript_file.download + end + + # Passed in transcript_file must be instance of ActiveStorage::Attached::One + def self.normalize_transcript transcript_file + plaintext = self.transcript_plaintext(transcript_file) + return if plaintext.blank? + + normalized_plaintext = plaintext.gsub("\r\n", "\n") + # We only need the time cues and associated text content indexed so we remove the subtitle + # numbers and the VTT header, if present. + headerless_plaintext = normalized_plaintext.gsub(/WEBVTT.+?(?=\d{2}:\d{2}:\d{2})/m, "") + headerless_plaintext.gsub(/\d+\n(?=\d{2}:\d{2}:\d{2})/, "") + end + end +end \ No newline at end of file diff --git a/lib/parse_docx.rb b/lib/parse_docx.rb new file mode 100644 index 0000000000..01803217a3 --- /dev/null +++ b/lib/parse_docx.rb @@ -0,0 +1,29 @@ +# Copyright 2011-2024, 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 ParseDocx + def parse_docx + # https://github.com/ruby-docx/docx/blob/master/lib/docx/document.rb#L38-42 + document = self.glob('word/document*.xml').first + raise Errno::ENOENT if document.nil? + document_xml = document.get_input_stream.read + doc = Nokogiri::XML(document_xml) + + # https://github.com/ruby-docx/docx/blob/c5bcb57b3d21fead105f1c7af8d3881dd31674cc/lib/docx/document.rb#L66 + paragraphs = doc.xpath('//w:document//w:body/w:p') + + paragraphs.map(&:content).join("\n") + end +end +Zip::File.prepend(ParseDocx) \ No newline at end of file diff --git a/spec/fixtures/chunk_test.docx b/spec/fixtures/chunk_test.docx new file mode 100644 index 0000000000000000000000000000000000000000..1c3ffc141ddf34beacb1d285068b64724e151332 GIT binary patch literal 6215 zcmaJ_1z3}9+a{!?Q$a#NQUPg%iGXyB5Q)(Y7~KuR2$j((-Jx_!2uL$p8b)^rNH_lA z7xMal?|bcdc5KI<^WJ@(S3K8!0_9NekRV}VVj_(s=V~C`GMwwrdiG#T2aYE?2eqF@=gD!;fd|fZXtzB~*CKkGRKw+-2w|hiIj3Y9B@s`9RE_a8$A9ig0R$#ERZ~8qV zIE%^;xQ!b8&>=QiN5Z8HWo>eR3LKyH~0gQ@5#P~)JlwULk z=5^V&HKFKSndJM1RuH8{YDUJyjM7#a{)u(KG%9Eg|55w$qO*WU1$E$Uxym#gDTg6pjUU z-PUzuwvki!3yqbI3&4F+sutn~ACDx)sE%^WRvt#zyPKWo-Xo5DVNtf;*xnAt9jvxI zQyZxE1L^_^kL{u7;ld+5Pq$mpgtUvAr!reRg{Acx@3lYEN+@mz)3Y6v7nJo3hxSDk zZPNNtT={vU-mt#*q#i}!nsvs1WS#gn>qbrvjy6`mi0>=KzOICT`RQED34|I(#9#$X z;F;QS_)HbDgH5Lb%*%rwx*htK2B0`3aw%RwFhOWu!@9Ngawt!3%d`_R* zKW!PEz0P?IM343kjtpa`wGhlDG0z_Fo5PJ0GWC2v*A5Z{9H%_&naO!Y5RqdoVas-i#Fbpt#D@iEcK1LH(qdj{ zUcT_!;o{KKI}g({?22^ z6IxZ{gpNWZqelA`S(IzC(Yp~NQYEU08-8SH=g9RE#npAE@Hc@7gT}QEsxW0#=Z z{=U8|w+(nFrnmUVO->-f6LO^h4)op{>zNcv9hqUlMiy1eN&Fl+x!swR>J{)+-jqM% zDj}Ym1Myt`l!bmwXEDX_gc$ySlDp(vW?Y`n%;|*b!Ax9Pj!5M~v9iaqjw=2z>ml!; z`e1Ql0%3k>2Ah@R)T5?j`Q+z6EkItO6F;SP@Xs8srau$9R?!1L!s?oZI9{X2-ch^* zV-mI7BSNpVpNLI$IxGLNz6-F%eSc&~-@9x)|52Pnr7A1T&OV`iT^?VguU08N7F%p_ zGf0Ynf9~Z<#6{82jyVOGc?aT_a^(2)`^1ko<`x{0^SSY&4M!xMHfDQ!?pK@BmDTR9 zE{EN{_JYL151ki`qPGv|_-z(J1HflyQ!xGbtG!KCZkHDseJ%<4=X(T#yGL(xZ{V_aDq>he#hehA-n2f3=d&R( zg%Y_Q=54~1jCcrL9K4Bp)U@4U=Qg`5)-|3E^sGHNbMFWQiOR~$w4E0rwnXthN&VFyC2ic887c^&@O5Uu1npkDp?y|t!4V+BnYPQ>PU>| zOT^}k`f5;o(B$SYz}uBcF$LAG!BGUO7KVS5nlV_h?VH852Q7JYdJ_&-WghfNc3_4& z5m;<$u?+yDf_fC*hlMbPF1Q)_i(Sfk3rP~JWo8J$3ycW9b&IhXKkBOXIc!A3t0}0M zz+Sc-peu9e?7;LVd8`Jo0=5!$Kc2(1A9GEYU9wIIuli!GPyfKkFeyeie3IY`t?>?Q z#?4?>420KKC0TthI-3>vv=8zE5UX?%*1UkVVQE);Hag2=rtef?x95Flft zS69X%crNW+y?L1Tqtb<~25Onq`cQ)2SDV%n9xSh8E~sGe*aA-5;XoSlG}%H<3}ga_ zPnPgfLNx6a*TAgX0-xGOvpr`u??b4G<&3Q#Qq&NoFAXRdunv;F(l&pE4G9(%LfrF1 zXT@B~^hd6l>H=3FVwWOQU{qk^&#&E9otRxJ=j@_Z-WNbX^iGG-)-wD zzpkSiZ6x9QdWIjZJ3%>glAVF}56I-%*l*e{I14yp~d!SeDj5@bv7yE4hXkNHIxYT zwLyDloIpV2D_o|5uRQ8-$~zL6q;5?4a_)#>J~0yD$xD@pS7}5wQdm>w%mB8=w>pjh z=)54jGFN$9UwjLWKNjEg^En~d61ZUgDIg9#`uC2 zhrp_mNs^0laFKd?KJE!sChhQ*BuRUv>WkQ9iO~sZN7tXzA@=0V4>9ish|SALK5;R? zWX=NeS_VJ>e1I*CVgGc`GF~wfDBUM~K@5OsY@=&=g`AAI>X;K*%yf%VoI0sJfACYI*d;?$=wA?LAdC+XFW??gH4 z(luea=^r=7Ink5DDCp3?A2lb0J*<`ImE)IJ|I#!MF07*htbJN3#M_blh+*D2oV`rs zebxw-SdNbT0rQgdsAi_p1e9G^t(1OvJ~hR$T5R2Vais;Co~f^C@H*ahe{|)< zsPp9~zSd+n{IuM(pZGbW_nYH|(}{uk!h@5pskHeEn*N%wRgsEIi*K#E=+sJ25%m(U zcj|U!hKs<Lxiai$PY{v!ahSD;UFLq z`|>>l7{&Cg4xS<~jmP9(PST6u(X@ey^|wm-?*}cgSvgPT71X&{E6Aca)gZ`>>QW#i z?$V@gA`6FTm~9*Q5rfhT7S)EVdFG(d79!$6&-nRj)jWoeOF^q=%*)`;{iLip+4uv4 zr6So4oal8xNVEokz3uBTu|||&xBA)H{N(Zo@GHC>lcw!@j$!2;aCFw?0(WFt^~VYHeu-oPh4wa zO2rs_{TwKcYwKN(5(egqe)ay9SSfiUp8FmQ%2vUKRX-?7nl87xR{`XH#wO{4aQP^- zd-A4yTm@2h5q_{ExQHu@KM!^)2XCVm3}rkA|8X3UoW!Zg==r{7{fHz!lHsWIiBV_~vE*V7d0^&RZx>}{6{eol zWUaOvz6KqWh+)b_$RkQ>M1}@u$+zUPldZD!7$WrNvCcBj!bab%A1rcB?p@(_cqP#Y z4f873a)+ayo47%;+s`(Ku6VQYtoWOn-Lr>EJ6x~^7QGu8i*a|$!I1BHRj#wOm$)-; zraxw#$q&!StpWRT*!^Jrww-P z=lrQ*2UY+|i^4`{6T}PUFux4%+RGTBOWd1!*rBK59J(H-C9YMJzdvCR{ihyGY^)vC z3=A!QNeK18CPfr3g8Fk(g`L3wKQ2l#L!Ojqzd4M%;8?3cXFl$Ln8wfS;S|$;TpNYS z$1C;xHbQT3bhjeflnV}A)yx9E4Ys#gW$V2T8~dy!1Fc3@B4qOt{kA_bQo&{sB%`Jq zrs5o9tzs@`)XGO8DmiS}qoF`WyUbyuAoT?j&mLraIMuiPT$>O~-sr-ld#hXHlR-BAksO=A18U`i7M3J(B1;~?cS(I`6+e#U z_<-6>r>qy;fZ<$n0$FD4Ix@BWc0eFGyarJgAUYXQ0(>;8li4j?ILHNz4TElrz7bFQA!^FEW9tmyPmGb@K9s>}rM|t?FA?Vrmke>`2L-kEyD` zw1f80qgGof(JdhkkQk>L}NG$EEO zcDJW~wNJnvpp!48>AOF;1_hZIMf$8rO6!m=fv-Nygs&S%_w$o^LhC2VjR~Rk*~9y2 z%q!eRFO0Hz6ia`Uu%8OlB+Ym$C^mp7(S|ZAzxBEi7ZXZ1Y5lfa>2Qb%<)Mu z1$~Y}iM;19|DMG%t=NtzA=xbj5XtGe5`5_T7_aOSHroQ*7|tN9rk&?}s6~U%17;CS z>rhE&@T)dMUeMsvJJTPApNy`Ep5)qP)J+s3SN-gG%)hoX5S0^ZE$|d$XIJMdRuySm z7In{jOAh**E0V$;+6IZGr23J@6U190%fh!VNOWl{9908?E`*YTR@9sDUy=pyciINs z>(C(3Zo-3%LW1;LwY@D@-l(>JwObnQpTOIy+D(Q0HoEJ-`hVKypYYrA(2ex=+sdz7 z-+$r%7UKRy-xgtR((1Rd+_{DREhzoDz-`&$MyvU4$k!zOlXCOtO1Gz%zw$tU{y#*I zKjF8V{mqE;+f?!X4gWpx{0Y9@ziyrfzs=}+E5E$>%hTac{O!8^>)v~>m-~gk-Shs0 h-_EPQMuC*%f0jB>4(*x~BqZ$Xm)CWeD@bp?{U4g!!}0(C literal 0 HcmV?d00001 diff --git a/spec/fixtures/chunk_test.srt b/spec/fixtures/chunk_test.srt new file mode 100644 index 0000000000..861fad6a53 --- /dev/null +++ b/spec/fixtures/chunk_test.srt @@ -0,0 +1,46 @@ +1 +00:00:01,200 --> 00:00:21,000 +[music] + +2 +00:00:22,200 --> 00:00:26,600 +Just before lunch one day, a puppet show +was put on at school. + +3 +00:00:26,700 --> 00:00:31,500 +It was called "Mister Bungle Goes to Lunch". + +4 +00:00:31,600 --> 00:00:34,500 +It was fun to watch. + +5 +00:00:36,100 --> 00:00:41,300 +In the puppet show, Mr. Bungle came to the +boys' room on his way to lunch. + +6 +00:00:41,400 --> 00:00:46,200 +He looked at his hands. His hands were dirty +and his hair was messy. + +7 +00:00:46,300 --> 00:00:51,100 +But Mr. Bungle didn't stop to wash his hands +or comb his hair. + +8 +00:00:51,200 --> 00:00:54,900 +He went right to lunch. + +9 +00:00:57,900 --> 00:01:05,700 +Then, instead of getting into line at the +lunchroom, Mr. Bungle pushed everyone aside +and went right to the front. + +10 +00:01:06,000 --> 00:01:11,800 +Even though this made the children laugh, +no one thought that was a fair thing to do. diff --git a/spec/fixtures/chunk_test.txt b/spec/fixtures/chunk_test.txt new file mode 100644 index 0000000000..db2ef1a0a9 --- /dev/null +++ b/spec/fixtures/chunk_test.txt @@ -0,0 +1,66 @@ +DAVID CROCKETT. + + + +CHAPTER I. + +Parentage and CJiildJwod. + +The Emigrant. Crossing the Alleghanies. The boundless Wilder- +ness. The Hut on the Holston. Life's Necessaries. The +Massacre. Birth of David Crockett. Peril of the Boys. +Anecdote. Removal to Greenville ; to Cove Creek. Increased +Emigration. Loss of the Mill. The Tavern. Engagement +with the Drover.-^Adventures in the Wilderness.-^ Virtual Cap- +tivity. The Escape. The Return. The Runaway. New Ad- +yentures. + +A LITTLE more than a hundred years ago, a poor +man, by the name of Crockett, embarked on board +an emigrant-ship, in Ireland, for the New World. +He was in the humblest station in life. But very- +little is known respecting his uneventful career, +excepting its tragical close. His family consisted of +a wife and three or four children. Just before he +sailed, or on the Atlantic passage, a son was born, to + + + +8 DAVID CROCKETT. + +whom he gave the name of John. The family +probably landed in Philadelphia, and dwelt some- +where in Pennsylvania, for a year or two, in one of +those slab shanties, with which all are familiar as +the abodes of the poorest class of Irish emigrants. + +After a year or two, Crockett, with his little +family, crossed the almost pathless Alleghanies. +Father, mother, and children trudged along through +the rugged defiles and over the rocky cliffs, on foot. +Probably a single pack-horse conveyed their few +household goods. The hatchet and the rifle were +the only means of obtaining food, shelter, and even +clothing. With the hatchet, in an hour or two, a +comfortable camp could be constructed, which would +protect them from wind and rain. The camp-fire, +cheering the darkness of the night, drying their +often wet garments, and warming their chilled +limbs with its genial glow, enabled them to enjoy +that almost greatest of earthly luxuries, peaceful +sleep. + +The rifle supplied them with food. The fattest +of turkeys and the most tender steaks of venison, +roasted upon forked sticks, which they held in their +hands over the coals, feasted their voracious appe- +tites. This, to them, was almost sumptuous food. +The skin of the deer, by a rapid and simple pro- +cess of tanning, supplied them with moccasons, and + + + +PARENTAGE AND CHILDHOOD. 9 + +afforded material for the repair of their tattered +garments. diff --git a/spec/fixtures/chunk_test.vtt b/spec/fixtures/chunk_test.vtt new file mode 100644 index 0000000000..6ff31aa8b3 --- /dev/null +++ b/spec/fixtures/chunk_test.vtt @@ -0,0 +1,48 @@ +WEBVTT + +1 +00:00:01.200 --> 00:00:21.000 +[music] + +2 +00:00:22.200 --> 00:00:26.600 +Just before lunch one day, a puppet show +was put on at school. + +3 +00:00:26.700 --> 00:00:31.500 +It was called "Mister Bungle Goes to Lunch". + +4 +00:00:31.600 --> 00:00:34.500 +It was fun to watch. + +5 +00:00:36.100 --> 00:00:41.300 +In the puppet show, Mr. Bungle came to the +boys' room on his way to lunch. + +6 +00:00:41.400 --> 00:00:46.200 +He looked at his hands. His hands were dirty +and his hair was messy. + +7 +00:00:46.300 --> 00:00:51.100 +But Mr. Bungle didn't stop to wash his hands +or comb his hair. + +8 +00:00:51.200 --> 00:00:54.900 +He went right to lunch. + +9 +00:00:57.900 --> 00:01:05.700 +Then, instead of getting into line at the +lunchroom, Mr. Bungle pushed everyone aside +and went right to the front. + +10 +00:01:06.000 --> 00:01:11.800 +Even though this made the children laugh, +no one thought that was a fair thing to do. diff --git a/spec/models/supplemental_file_spec.rb b/spec/models/supplemental_file_spec.rb index da66391a37..96e7930a5d 100644 --- a/spec/models/supplemental_file_spec.rb +++ b/spec/models/supplemental_file_spec.rb @@ -98,4 +98,48 @@ expect(SupplementalFile.convert_from_srt(input)).to eq output end end + + describe '.segment_transcript' do + subject { SupplementalFile.segment_transcript(file.file) } + + context 'plain text' do + let(:file) { FactoryBot.create(:supplemental_file, file: fixture_file_upload(Rails.root.join('spec', 'fixtures', 'chunk_test.txt'), 'text/plain')) } + it 'splits the text by paragraph' do + expect(subject).to be_a Array + expect(subject.length).to eq 11 + expect(subject.all? { |s| s.is_a?(String) }).to eq true + expect(subject[3]).to include "The Emigrant. Crossing the Alleghanies. The boundless Wilder- ness. The Hut on the Holston. Life's Necessaries." + end + end + + context 'docx' do + let(:file) { FactoryBot.create(:supplemental_file, file: fixture_file_upload(Rails.root.join('spec', 'fixtures', 'chunk_test.docx'), 'application/vnd.openxmlformats-officedocument.wordprocessingml.document')) } + it 'splits the text by paragraph' do + expect(subject).to be_a Array + expect(subject.length).to eq 11 + expect(subject.all? { |s| s.is_a?(String) }).to eq true + expect(subject[3]).to include "The Emigrant. Crossing the Alleghanies. The boundless Wilder- ness. The Hut on the Holston. Life's Necessaries." + end + end + + context 'vtt' do + let(:file) { FactoryBot.create(:supplemental_file, file: fixture_file_upload(Rails.root.join('spec', 'fixtures', 'chunk_test.vtt'), 'text/vtt')) } + it 'splits the text by time cue' do + expect(subject).to be_a Array + expect(subject.length).to eq 10 + expect(subject.all? { |s| s.is_a?(String) }).to eq true + expect(subject[0]).to eq "00:00:01.200 --> 00:00:21.000 [music]" + end + end + + context 'srt' do + let(:file) { FactoryBot.create(:supplemental_file, file: fixture_file_upload(Rails.root.join('spec', 'fixtures', 'chunk_test.srt'), 'text/srt')) } + it 'splits the text by time cue' do + expect(subject).to be_a Array + expect(subject.length).to eq 10 + expect(subject.all? { |s| s.is_a?(String) }).to eq true + expect(subject[0]).to eq "00:00:01,200 --> 00:00:21,000 [music]" + end + end + end end From 35a1f6dd2c68618bdfc158e056cae2aa0e27fefe Mon Sep 17 00:00:00 2001 From: cjcolvar Date: Fri, 5 Apr 2024 12:08:41 -0400 Subject: [PATCH 038/152] Proof of Concept --- app/models/master_file.rb | 1 + app/models/search_builder.rb | 35 ++++++++++++++++++- .../catalog/_index_media_object.html.erb | 6 ++++ docker-compose.yml | 3 ++ solr/conf/schema.xml | 3 +- 5 files changed, 46 insertions(+), 2 deletions(-) diff --git a/app/models/master_file.rb b/app/models/master_file.rb index 1a6d4e1240..47f966eeee 100644 --- a/app/models/master_file.rb +++ b/app/models/master_file.rb @@ -524,6 +524,7 @@ def to_solr *args solr_doc['status_code_ssi'] = status_code solr_doc['operation_ssi'] = operation solr_doc['error_ssi'] = error + solr_doc['transcript_tim'] = supplemental_file_transcripts.map {|transcript| transcript.file.download} end end diff --git a/app/models/search_builder.rb b/app/models/search_builder.rb index a521bb9677..e84b3b6ce8 100644 --- a/app/models/search_builder.rb +++ b/app/models/search_builder.rb @@ -20,7 +20,7 @@ class SearchBuilder < Blacklight::SearchBuilder class_attribute :avalon_solr_access_filters_logic self.avalon_solr_access_filters_logic = [:only_published_items, :limit_to_non_hidden_items] - self.default_processor_chain += [:only_wanted_models] + self.default_processor_chain += [:only_wanted_models, :term_frequency_counts] def only_wanted_models(solr_parameters) solr_parameters[:fq] ||= [] @@ -46,4 +46,37 @@ def add_access_controls_to_solr_params(solr_parameters) Rails.logger.debug("Solr parameters: #{solr_parameters.inspect}") end end + + # NOT working -> need to aggregate in all_text_timv instead? + def search_section_transcripts(solr_parameters) + return unless solr_parameters[:q].present? + + terms = solr_parameters[:q].split + term_subquery = terms.map { |term| "transcript_tim:#{term}" }.join(" OR ") + solr_parameters[:q] += " {!join to=id from=isPartOf_ssim}(has_model_ssim:MasterFile AND (#{term_subquery}))" + end + + def term_frequency_counts(solr_parameters) + return unless solr_parameters[:q].present? + + # List of fields for displaying on search results (Blacklight index fields) + fl = ['id', 'has_model_ssim', 'title_tesi', 'date_issued_ssi', 'creator_ssim', 'abstract_ssi', 'duration_ssi', 'section_id_ssim'] + + # Add a field for matching child sections + #solr_parameters[:defType] = "lucene" + fl << "sections:[subquery]" + solr_parameters["sections.q"] = "{!terms f=isPartOf_ssim v=$row.id}" + solr_parameters["sections.defType"] = "lucene" + sections_fl = ['id'] + + # Add fields for each term in the query + terms = solr_parameters[:q].split + terms.each_with_index do |term, i| + fl << "metadata_tf_#{i}:termfreq(mods_tesim,#{term})" + fl << "structure_tf_#{i}:termfreq(section_label_tesim,#{term})" + sections_fl << "transcript_tf_#{i}:termfreq(transcript_tim,#{term})" + end + solr_parameters[:fl] = fl.join(',') + solr_parameters["sections.fl"] = sections_fl.join(',') + end end diff --git a/app/views/catalog/_index_media_object.html.erb b/app/views/catalog/_index_media_object.html.erb index 8f1a73f68f..d92fbd24db 100644 --- a/app/views/catalog/_index_media_object.html.erb +++ b/app/views/catalog/_index_media_object.html.erb @@ -23,4 +23,10 @@ Unless required by applicable law or agreed to in writing, software distributed
<%= field_presenter.values.join(', ') %>
<% end %> <% end %> + <% if params[:q].present? %> +
Found in:
+
metadata (<%= doc_presenter.document.to_h.sum {|k,v| k =~ /metadata_tf_/ ? v : 0 } %>) + , transcript (<%= doc_presenter.document["sections"]["docs"].sum {|s| s.sum {|k,v| k =~ /transcript_tf_/ ? v : 0 }} %>) + , sections (<%= doc_presenter.document.to_h.sum {|k,v| k =~ /structure_tf_/ ? v : 0 } %>)
+ <% end %> diff --git a/docker-compose.yml b/docker-compose.yml index a3a3a8b389..be134b888c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -54,8 +54,11 @@ services: - solr-precreate - avalon - /opt/solr/avalon_conf + ports: + - '8983:8983' networks: internal: + external: solr-test: <<: *solr volumes: diff --git a/solr/conf/schema.xml b/solr/conf/schema.xml index 19697ecd05..b7b9f215f3 100644 --- a/solr/conf/schema.xml +++ b/solr/conf/schema.xml @@ -347,9 +347,10 @@ + From 72ed8708871dbf0fdf250577d16ed6fac5ef5a64 Mon Sep 17 00:00:00 2001 From: cjcolvar Date: Fri, 19 Apr 2024 12:44:45 -0400 Subject: [PATCH 039/152] WIP - highlighting experiments --- app/models/master_file.rb | 6 +++++- app/models/search_builder.rb | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/app/models/master_file.rb b/app/models/master_file.rb index 47f966eeee..7a1cee9a2e 100644 --- a/app/models/master_file.rb +++ b/app/models/master_file.rb @@ -524,7 +524,7 @@ def to_solr *args solr_doc['status_code_ssi'] = status_code solr_doc['operation_ssi'] = operation solr_doc['error_ssi'] = error - solr_doc['transcript_tim'] = supplemental_file_transcripts.map {|transcript| transcript.file.download} + solr_doc['transcript_tsim'] = supplemental_file_transcripts.map {|transcript| segment_transcript(transcript.file.download)}.flatten end end @@ -549,6 +549,10 @@ def self.calculate_working_file_path(old_path) protected + def segment_transcript transcript + transcript.split(/\n\n+/).map(&:strip).map { |cue| cue.gsub!("\n", " ") }.compact! + end + def mediainfo Mediainfo.new(FileLocator.new(file_location).location, headers: @auth_header) end diff --git a/app/models/search_builder.rb b/app/models/search_builder.rb index e84b3b6ce8..95576f2dbd 100644 --- a/app/models/search_builder.rb +++ b/app/models/search_builder.rb @@ -52,7 +52,7 @@ def search_section_transcripts(solr_parameters) return unless solr_parameters[:q].present? terms = solr_parameters[:q].split - term_subquery = terms.map { |term| "transcript_tim:#{term}" }.join(" OR ") + term_subquery = terms.map { |term| "transcript_tsim:#{term}" }.join(" OR ") solr_parameters[:q] += " {!join to=id from=isPartOf_ssim}(has_model_ssim:MasterFile AND (#{term_subquery}))" end @@ -74,7 +74,7 @@ def term_frequency_counts(solr_parameters) terms.each_with_index do |term, i| fl << "metadata_tf_#{i}:termfreq(mods_tesim,#{term})" fl << "structure_tf_#{i}:termfreq(section_label_tesim,#{term})" - sections_fl << "transcript_tf_#{i}:termfreq(transcript_tim,#{term})" + sections_fl << "transcript_tf_#{i}:termfreq(transcript_tsim,#{term})" end solr_parameters[:fl] = fl.join(',') solr_parameters["sections.fl"] = sections_fl.join(',') From 57c97c0bf3fcb25422b81683a118ef8691e7aa09 Mon Sep 17 00:00:00 2001 From: cjcolvar Date: Mon, 22 Apr 2024 17:56:20 -0400 Subject: [PATCH 040/152] Proof of Concept - index transcripts as independent documents and update queries accordingly --- .../supplemental_files_controller.rb | 2 +- app/models/master_file.rb | 2 +- app/models/search_builder.rb | 17 +++++--- app/models/supplemental_file.rb | 40 +++++++++++++++++++ .../catalog/_index_media_object.html.erb | 2 +- ...0346_add_parent_id_to_supplemental_file.rb | 5 +++ db/schema.rb | 5 ++- solr/conf/schema.xml | 2 +- 8 files changed, 64 insertions(+), 11 deletions(-) create mode 100644 db/migrate/20240424200346_add_parent_id_to_supplemental_file.rb diff --git a/app/controllers/supplemental_files_controller.rb b/app/controllers/supplemental_files_controller.rb index df91f555d5..bb20375b40 100644 --- a/app/controllers/supplemental_files_controller.rb +++ b/app/controllers/supplemental_files_controller.rb @@ -34,7 +34,7 @@ def create # FIXME: move filedata to permanent location raise Avalon::BadRequest, "Missing required parameters" unless supplemental_file_params[:file] - @supplemental_file = SupplementalFile.new(label: supplemental_file_params[:label], tags: supplemental_file_params[:tags]) + @supplemental_file = SupplementalFile.new(label: supplemental_file_params[:label], tags: supplemental_file_params[:tags], parent_id: @object.id) begin @supplemental_file.attach_file(supplemental_file_params[:file]) rescue StandardError, LoadError => e diff --git a/app/models/master_file.rb b/app/models/master_file.rb index 7a1cee9a2e..f237e6a489 100644 --- a/app/models/master_file.rb +++ b/app/models/master_file.rb @@ -524,7 +524,7 @@ def to_solr *args solr_doc['status_code_ssi'] = status_code solr_doc['operation_ssi'] = operation solr_doc['error_ssi'] = error - solr_doc['transcript_tsim'] = supplemental_file_transcripts.map {|transcript| segment_transcript(transcript.file.download)}.flatten + #solr_doc['transcript_ids_ssim'] = supplemental_file_transcripts.map { |file| file.to_global_id.to_s } end end diff --git a/app/models/search_builder.rb b/app/models/search_builder.rb index 95576f2dbd..89cbddf8e6 100644 --- a/app/models/search_builder.rb +++ b/app/models/search_builder.rb @@ -20,7 +20,7 @@ class SearchBuilder < Blacklight::SearchBuilder class_attribute :avalon_solr_access_filters_logic self.avalon_solr_access_filters_logic = [:only_published_items, :limit_to_non_hidden_items] - self.default_processor_chain += [:only_wanted_models, :term_frequency_counts] + self.default_processor_chain += [:only_wanted_models, :term_frequency_counts, :search_section_transcripts] def only_wanted_models(solr_parameters) solr_parameters[:fq] ||= [] @@ -47,13 +47,14 @@ def add_access_controls_to_solr_params(solr_parameters) end end - # NOT working -> need to aggregate in all_text_timv instead? def search_section_transcripts(solr_parameters) return unless solr_parameters[:q].present? terms = solr_parameters[:q].split term_subquery = terms.map { |term| "transcript_tsim:#{term}" }.join(" OR ") - solr_parameters[:q] += " {!join to=id from=isPartOf_ssim}(has_model_ssim:MasterFile AND (#{term_subquery}))" + solr_parameters[:defType] = "lucene" + solr_parameters[:df] = "id,title_tesi,section_label_tesim,all_text_timv" + solr_parameters[:q] += " {!join to=id from=isPartOf_ssim}{!join to=id from=isPartOf_ssim }#{term_subquery}" end def term_frequency_counts(solr_parameters) @@ -63,20 +64,26 @@ def term_frequency_counts(solr_parameters) fl = ['id', 'has_model_ssim', 'title_tesi', 'date_issued_ssi', 'creator_ssim', 'abstract_ssi', 'duration_ssi', 'section_id_ssim'] # Add a field for matching child sections - #solr_parameters[:defType] = "lucene" fl << "sections:[subquery]" solr_parameters["sections.q"] = "{!terms f=isPartOf_ssim v=$row.id}" solr_parameters["sections.defType"] = "lucene" sections_fl = ['id'] + transcripts_fl = ['id'] # Add fields for each term in the query terms = solr_parameters[:q].split terms.each_with_index do |term, i| fl << "metadata_tf_#{i}:termfreq(mods_tesim,#{term})" fl << "structure_tf_#{i}:termfreq(section_label_tesim,#{term})" - sections_fl << "transcript_tf_#{i}:termfreq(transcript_tsim,#{term})" + fl << "transcript_tf_#{i}" + sections_fl << "transcript_tf_#{i}" + transcripts_fl << "transcript_tf_#{i}:termfreq(transcript_tsim,#{term})" end solr_parameters[:fl] = fl.join(',') + sections_fl << "transcripts:[subquery]" solr_parameters["sections.fl"] = sections_fl.join(',') + solr_parameters["sections.transcripts.fl"] = transcripts_fl.join(',') + solr_parameters["sections.transcripts.defType"] = "lucene" + solr_parameters["sections.transcripts.q"] = "{!terms f=isPartOf_ssim v=$row.id}{!join to=id from=isPartOf_ssim}" end end diff --git a/app/models/supplemental_file.rb b/app/models/supplemental_file.rb index 34ae5df198..9d8960292d 100644 --- a/app/models/supplemental_file.rb +++ b/app/models/supplemental_file.rb @@ -19,9 +19,15 @@ class SupplementalFile < ApplicationRecord validates :tags, array_inclusion: ['transcript', 'caption', 'machine_generated', '', nil] validates :language, inclusion: { in: LanguageTerm.map.keys } validate :validate_file_type, if: :caption? + validates :parent_id, presence: true serialize :tags, Array + # Need to prepend so this runs before the callback added by `has_one_attached` above + # See https://github.com/rails/rails/issues/37304 + after_create_commit :update_index, prepend: true + after_update :update_index + def validate_file_type errors.add(:file_type, "Uploaded file is not a recognized captions file") unless ['text/vtt', 'text/srt'].include? file.content_type end @@ -65,4 +71,38 @@ def self.convert_from_srt(srt) "WEBVTT\n\n#{conversion}".strip end + + def update_index + ActiveFedora::SolrService.add(to_solr, softCommit: true) + end + + # Creates a solr document hash for the {#object} + # @return [Hash] the solr document + def to_solr + solr_doc = {} + solr_doc[ActiveFedora.id_field.to_sym] = to_global_id.to_s + ActiveFedora.index_field_mapper.set_field(solr_doc, 'system_create', c_time, :stored_sortable) + ActiveFedora.index_field_mapper.set_field(solr_doc, 'system_modified', m_time, :stored_sortable) + solr_doc[ActiveFedora::QueryResultBuilder::HAS_MODEL_SOLR_FIELD] = "SupplementalFile" + solr_doc["mime_type_ssi"] = mime_type + solr_doc["label_ssi"] = label + solr_doc["language_ssi"] = language + solr_doc["transcript_tsim"] = segment_transcript(file.download) if tags.include?("transcript") + solr_doc["isPartOf_ssim"] = [parent_id] + solr_doc + end + + private + + def c_time + created_at&.to_datetime || DateTime.now + end + + def m_time + updated_at&.to_datetime || DateTime.now + end + + def segment_transcript transcript + transcript.split(/\n\n+/).map(&:strip).map { |cue| cue.gsub!("\n", " ") }.compact + end end diff --git a/app/views/catalog/_index_media_object.html.erb b/app/views/catalog/_index_media_object.html.erb index d92fbd24db..51563a7b8a 100644 --- a/app/views/catalog/_index_media_object.html.erb +++ b/app/views/catalog/_index_media_object.html.erb @@ -26,7 +26,7 @@ Unless required by applicable law or agreed to in writing, software distributed <% if params[:q].present? %>
Found in:
metadata (<%= doc_presenter.document.to_h.sum {|k,v| k =~ /metadata_tf_/ ? v : 0 } %>) - , transcript (<%= doc_presenter.document["sections"]["docs"].sum {|s| s.sum {|k,v| k =~ /transcript_tf_/ ? v : 0 }} %>) + , transcript (<%= doc_presenter.document["sections"]["docs"].sum { |d| d["transcripts"]["docs"].sum {|s| s.sum {|k,v| k =~ /transcript_tf_/ ? v : 0 }}} %>) , sections (<%= doc_presenter.document.to_h.sum {|k,v| k =~ /structure_tf_/ ? v : 0 } %>)
<% end %> diff --git a/db/migrate/20240424200346_add_parent_id_to_supplemental_file.rb b/db/migrate/20240424200346_add_parent_id_to_supplemental_file.rb new file mode 100644 index 0000000000..c44a6925a4 --- /dev/null +++ b/db/migrate/20240424200346_add_parent_id_to_supplemental_file.rb @@ -0,0 +1,5 @@ +class AddParentIdToSupplementalFile < ActiveRecord::Migration[7.0] + def change + add_column :supplemental_files, :parent_id, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index 9f58285cc2..19b78c1c20 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[7.0].define(version: 2023_05_16_142233) do +ActiveRecord::Schema[7.0].define(version: 2024_04_24_200346) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -48,7 +48,7 @@ t.string "content_type" t.text "metadata" t.bigint "byte_size", null: false - t.string "checksum", null: false + t.string "checksum" t.datetime "created_at", precision: nil, null: false t.string "service_name", null: false t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true @@ -240,6 +240,7 @@ t.datetime "updated_at", precision: nil, null: false t.string "tags" t.string "language" + t.string "parent_id" end create_table "timelines", force: :cascade do |t| diff --git a/solr/conf/schema.xml b/solr/conf/schema.xml index b7b9f215f3..0b00252980 100644 --- a/solr/conf/schema.xml +++ b/solr/conf/schema.xml @@ -333,7 +333,7 @@ - id + id 00:00:05.000\r Example captions" + expect(caption_transcript.to_solr[ "transcript_tsim" ]).to be_a Array + expect(transcript.to_solr[ "transcript_tsim" ][0]).to eq "WEBVTT FILE\r \r 1\r 00:00:03.500 --> 00:00:05.000\r Example captions" + end + + it "should not solrize non-transcripts" do + expect(caption.to_solr[ "transcript_tsim" ]).to be nil + end + end + describe 'language' do it 'should validate valid language' do subject.language = 'eng' diff --git a/spec/support/supplemental_file_shared_examples.rb b/spec/support/supplemental_file_shared_examples.rb index 7df8578f9f..23e4376ac9 100644 --- a/spec/support/supplemental_file_shared_examples.rb +++ b/spec/support/supplemental_file_shared_examples.rb @@ -15,7 +15,7 @@ RSpec.shared_examples 'an object that has supplemental files' do let(:object) { FactoryBot.build(described_class.model_name.singular.to_sym) } let(:supplemental_file) { FactoryBot.create(:supplemental_file) } - let(:transcript_file) { FactoryBot.create(:supplemental_file, :with_transcript_tag) } + let(:transcript_file) { FactoryBot.create(:supplemental_file, :with_transcript_tag, :with_transcript_file) } let(:caption_file) { FactoryBot.create(:supplemental_file, :with_caption_file, tags: ['caption', 'machine_generated']) } let(:supplemental_files) { [supplemental_file, transcript_file, caption_file] } let(:supplemental_files_json) { supplemental_files.map(&:to_global_id).map(&:to_s).to_s } diff --git a/spec/support/supplemental_files_controller_examples.rb b/spec/support/supplemental_files_controller_examples.rb index 51612f0b81..b711ef6c0e 100644 --- a/spec/support/supplemental_files_controller_examples.rb +++ b/spec/support/supplemental_files_controller_examples.rb @@ -121,7 +121,9 @@ end let(:tags) { ["transcript"] } + let(:uploaded_file) { fixture_file_upload('/captions.vtt', 'text/vtt') } let(:valid_create_attributes_with_tags) { valid_create_attributes.merge(tags: tags) } + it "creates a SupplementalFile with tags for #{object_class}" do expect { post :create, params: { class_id => object.id, supplemental_file: valid_create_attributes_with_tags, format: :json }, session: valid_session From 4b416b19a12f053fc1720256c01cfce357cc8bde Mon Sep 17 00:00:00 2001 From: cjcolvar Date: Fri, 10 May 2024 17:03:06 -0400 Subject: [PATCH 043/152] Simplify transcript existence check query using scope --- app/models/search_builder.rb | 4 ++-- app/models/supplemental_file.rb | 3 +++ spec/models/supplemental_file_spec.rb | 18 ++++++++++++++++++ 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/app/models/search_builder.rb b/app/models/search_builder.rb index 9f8bca00fa..df4a1b3ddd 100644 --- a/app/models/search_builder.rb +++ b/app/models/search_builder.rb @@ -48,7 +48,7 @@ def add_access_controls_to_solr_params(solr_parameters) end def search_section_transcripts(solr_parameters) - return unless solr_parameters[:q].present? && SupplementalFile.all.any? { |sf| sf.transcript? } + return unless solr_parameters[:q].present? && SupplementalFile.with_tag('transcript').any? terms = solr_parameters[:q].split term_subquery = terms.map { |term| "transcript_tsim:#{term}" }.join(" OR ") @@ -62,7 +62,7 @@ def term_frequency_counts(solr_parameters) # Any search or filtering using a `q` parameter when transcripts are not present fails because # the transcript_tsim field does not get created. We need to only add the transcript searching # when transcripts are present. - transcripts_present = SupplementalFile.all.any? { |sf| sf.transcript? } + transcripts_present = SupplementalFile.with_tag('transcript').any? # List of fields for displaying on search results (Blacklight index fields) fl = ['id', 'has_model_ssim', 'title_tesi', 'date_issued_ssi', 'creator_ssim', 'abstract_ssi', 'duration_ssi', 'section_id_ssim'] diff --git a/app/models/supplemental_file.rb b/app/models/supplemental_file.rb index 698dab3188..da227d5e58 100644 --- a/app/models/supplemental_file.rb +++ b/app/models/supplemental_file.rb @@ -15,6 +15,8 @@ class SupplementalFile < ApplicationRecord has_one_attached :file + scope :with_tag, ->(tag_filter) { where("tags LIKE ?", "%\n- #{tag_filter}\n%") } + # TODO: the empty tag should represent a generic supplemental file validates :tags, array_inclusion: ['transcript', 'caption', 'machine_generated', '', nil] validates :language, inclusion: { in: LanguageTerm.map.keys } @@ -107,6 +109,7 @@ def m_time end def segment_transcript transcript + return unless transcript.present? transcript.split(/\n\n+/).map(&:strip).map { |cue| cue.gsub!("\n", " ") }.compact end end diff --git a/spec/models/supplemental_file_spec.rb b/spec/models/supplemental_file_spec.rb index 96d79b6a35..471380c73b 100644 --- a/spec/models/supplemental_file_spec.rb +++ b/spec/models/supplemental_file_spec.rb @@ -46,6 +46,24 @@ end end + describe 'scopes' do + describe 'with_tag' do + let!(:subject) { FactoryBot.create(:supplemental_file, tags: ['transcript', 'machine_generated']) } + let!(:other_transcript) { FactoryBot.create(:supplemental_file, tags: ['transcript']) } + let!(:another_file) { FactoryBot.create(:supplemental_file, :with_caption_file, tags: ['caption']) } + + it 'filters for a single tag' do + expect(SupplementalFile.with_tag('transcript')).to include(subject,other_transcript) + expect(SupplementalFile.with_tag('transcript').count).to eq 2 + end + + it 'filters for multiple tags' do + expect(SupplementalFile.with_tag('transcript').with_tag('machine_generated').first).to eq subject + expect(SupplementalFile.with_tag('transcript').with_tag('machine_generated').count).to eq 1 + end + end + end + describe '#to_solr' do # TODO: Update tests once chunking is in place let(:caption) { FactoryBot.create(:supplemental_file, :with_caption_file, :with_caption_tag) } From e9f1241e23914030321d28c7adda127cbfc7fe78 Mon Sep 17 00:00:00 2001 From: cjcolvar Date: Mon, 13 May 2024 11:01:49 -0400 Subject: [PATCH 044/152] Wrap user query in edismax subquery and solr escape all user-supplied query fragments including single quotes --- app/models/search_builder.rb | 11 +++++------ config/initializers/policy_aware_modification.rb | 15 +++++++++++++++ spec/models/search_builder_spec.rb | 3 +-- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/app/models/search_builder.rb b/app/models/search_builder.rb index df4a1b3ddd..18880c0bd1 100644 --- a/app/models/search_builder.rb +++ b/app/models/search_builder.rb @@ -51,10 +51,9 @@ def search_section_transcripts(solr_parameters) return unless solr_parameters[:q].present? && SupplementalFile.with_tag('transcript').any? terms = solr_parameters[:q].split - term_subquery = terms.map { |term| "transcript_tsim:#{term}" }.join(" OR ") + term_subquery = terms.map { |term| "transcript_tsim:#{RSolr.solr_escape(term)}" }.join(" OR ") solr_parameters[:defType] = "lucene" - solr_parameters[:df] = "id,title_tesi,section_label_tesim,all_text_timv" - solr_parameters[:q] += " {!join to=id from=isPartOf_ssim}{!join to=id from=isPartOf_ssim }#{term_subquery}" + solr_parameters[:q] = "({!edismax v=\"#{RSolr.solr_escape(solr_parameters[:q])}\"}) {!join to=id from=isPartOf_ssim}{!join to=id from=isPartOf_ssim}#{term_subquery}" end def term_frequency_counts(solr_parameters) @@ -77,11 +76,11 @@ def term_frequency_counts(solr_parameters) # Add fields for each term in the query terms = solr_parameters[:q].split terms.each_with_index do |term, i| - fl << "metadata_tf_#{i}:termfreq(mods_tesim,#{term})" - fl << "structure_tf_#{i}:termfreq(section_label_tesim,#{term})" + fl << "metadata_tf_#{i}:termfreq(mods_tesim,#{RSolr.solr_escape(term)})" + fl << "structure_tf_#{i}:termfreq(section_label_tesim,#{RSolr.solr_escape(term)})" fl << "transcript_tf_#{i}" if transcripts_present sections_fl << "transcript_tf_#{i}" if transcripts_present - transcripts_fl << "transcript_tf_#{i}:termfreq(transcript_tsim,#{term})" if transcripts_present + transcripts_fl << "transcript_tf_#{i}:termfreq(transcript_tsim,#{RSolr.solr_escape(term)})" if transcripts_present end solr_parameters[:fl] = fl.join(',') diff --git a/config/initializers/policy_aware_modification.rb b/config/initializers/policy_aware_modification.rb index 40e175ef9f..02094bcf11 100644 --- a/config/initializers/policy_aware_modification.rb +++ b/config/initializers/policy_aware_modification.rb @@ -11,6 +11,21 @@ def apply_group_permissions(permission_types, ability = current_ability) end end +# Override to also escape single quote ' +module RSolr + # backslash escape characters that have special meaning to Solr query parser + # per http://lucene.apache.org/core/4_0_0/queryparser/org/apache/lucene/queryparser/classic/package-summary.html#Escaping_Special_Characters + # + - & | ! ( ) { } [ ] ^ " ~ * ? : \ / + # see also http://svn.apache.org/repos/asf/lucene/dev/tags/lucene_solr_4_9_1/solr/solrj/src/java/org/apache/solr/client/solrj/util/ClientUtils.java + # escapeQueryChars method + # @return [String] str with special chars preceded by a backslash + def self.solr_escape(str) + # note that the gsub will parse the escaped backslashes, as will the ruby code sending the query to Solr + # so the result sent to Solr is ultimately a single backslash in front of the particular character + str.gsub(/([+\-&|!\(\)\{\}\[\]\^"'~\*\?:\\\/])/, '\\\\\1') + end +end + module Hydra::AccessControlsEnforcement def escape_filter(key, value) [key, escape_value(value)].join(':') diff --git a/spec/models/search_builder_spec.rb b/spec/models/search_builder_spec.rb index 4961bacdd2..c62f3e06aa 100644 --- a/spec/models/search_builder_spec.rb +++ b/spec/models/search_builder_spec.rb @@ -46,8 +46,7 @@ it "should add seaction transcript searching to the solr query" do subject.search_section_transcripts(solr_parameters) expect(solr_parameters[:defType]).to eq "lucene" - expect(solr_parameters[:df]).to eq "id,title_tesi,section_label_tesim,all_text_timv" - expect(solr_parameters[:q]).to eq "Example {!join to=id from=isPartOf_ssim}{!join to=id from=isPartOf_ssim }transcript_tsim:Example" + expect(solr_parameters[:q]).to eq "({!edismax v=\"Example\"}) {!join to=id from=isPartOf_ssim}{!join to=id from=isPartOf_ssim}transcript_tsim:Example" end end end From f9003cc4d6b1dd11ad324e3c7ee0720d9f5cab21 Mon Sep 17 00:00:00 2001 From: cjcolvar Date: Mon, 13 May 2024 11:43:49 -0400 Subject: [PATCH 045/152] Revert changes exposing solr to localhost --- docker-compose.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index be134b888c..a3a3a8b389 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -54,11 +54,8 @@ services: - solr-precreate - avalon - /opt/solr/avalon_conf - ports: - - '8983:8983' networks: internal: - external: solr-test: <<: *solr volumes: From ee8d7f31a100ef4f46f90753b769bfd76db661f6 Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Mon, 13 May 2024 14:30:30 -0400 Subject: [PATCH 046/152] Improve timed text parsing and adjust modules This commit is primarily concerned with adding parsing/normalization to hopefully handle any valid VTT file. SRT have much less options but are also run through the same parsing chain to ensure consistent output. --- app/models/supplemental_file.rb | 4 +- config/initializers/avalon.rb | 1 - lib/{parse_docx.rb => avalon/docx_file.rb} | 26 ++++++----- lib/avalon/transcript_parser.rb | 54 ++++++++++++++++------ spec/fixtures/chunk_test.vtt | 26 +++++++++-- spec/models/supplemental_file_spec.rb | 22 ++++++--- 6 files changed, 96 insertions(+), 37 deletions(-) rename lib/{parse_docx.rb => avalon/docx_file.rb} (56%) diff --git a/app/models/supplemental_file.rb b/app/models/supplemental_file.rb index 24bc103326..acd2cf1c80 100644 --- a/app/models/supplemental_file.rb +++ b/app/models/supplemental_file.rb @@ -70,9 +70,9 @@ def self.convert_from_srt(srt) # private def self.segment_transcript transcript - normalized_transcript = Avalon::TranscriptParser.normalize_transcript(transcript) + normalized_transcript = Avalon::TranscriptParser.new(transcript).normalize_transcript chunked_transcript = normalized_transcript.split(/\n\n+/) - chunked_transcript.map(&:strip).map { |cue| cue.gsub("\n", " ") }.compact + chunked_transcript.map(&:strip).map { |cue| cue.gsub("\n", " ").squeeze(' ') }.compact end end diff --git a/config/initializers/avalon.rb b/config/initializers/avalon.rb index 4a5c97684e..d565fffa9d 100644 --- a/config/initializers/avalon.rb +++ b/config/initializers/avalon.rb @@ -1,7 +1,6 @@ require 'string_additions' require 'avalon/errors' require 'day_hour_string' -require 'parse_docx' # Loads configuration information from the YAML file and then sets up the # dropbox # diff --git a/lib/parse_docx.rb b/lib/avalon/docx_file.rb similarity index 56% rename from lib/parse_docx.rb rename to lib/avalon/docx_file.rb index 01803217a3..8d8b93cfab 100644 --- a/lib/parse_docx.rb +++ b/lib/avalon/docx_file.rb @@ -12,18 +12,20 @@ # specific language governing permissions and limitations under the License. # --- END LICENSE_HEADER BLOCK --- -module ParseDocx - def parse_docx - # https://github.com/ruby-docx/docx/blob/master/lib/docx/document.rb#L38-42 - document = self.glob('word/document*.xml').first - raise Errno::ENOENT if document.nil? - document_xml = document.get_input_stream.read - doc = Nokogiri::XML(document_xml) +require 'zip' - # https://github.com/ruby-docx/docx/blob/c5bcb57b3d21fead105f1c7af8d3881dd31674cc/lib/docx/document.rb#L66 - paragraphs = doc.xpath('//w:document//w:body/w:p') +module Avalon + class DocxFile + def initialize(io) + zip_file = Zip::File.open_buffer(io) + document = zip_file.glob('word/document*.xml').first + raise Errno::ENOENT if document.nil? + document_xml = document.get_input_stream.read + @doc = Nokogiri::XML(document_xml) + end - paragraphs.map(&:content).join("\n") + def unformatted_text + @doc.xpath('//w:document//w:body/w:p').map(&:content).join("\n") + end end -end -Zip::File.prepend(ParseDocx) \ No newline at end of file +end \ No newline at end of file diff --git a/lib/avalon/transcript_parser.rb b/lib/avalon/transcript_parser.rb index 0a0187d3af..5980964783 100644 --- a/lib/avalon/transcript_parser.rb +++ b/lib/avalon/transcript_parser.rb @@ -12,31 +12,59 @@ # specific language governing permissions and limitations under the License. # --- END LICENSE_HEADER BLOCK --- +require 'avalon/docx_file' + module Avalon - module TranscriptParser + class TranscriptParser TEXT_TYPE = ['text/vtt', 'text/srt', 'text/plain', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'] + # Passed in transcript_file must be instance of ActiveStorage::Attached::One - def self.transcript_plaintext transcript_file - mime_type = transcript_file.content_type + def initialize(transcript_file) + @mime_type = transcript_file.content_type + @transcript = transcript_file.download + end + + def transcript_plaintext # Transcripts can have arbitrary files imported. We need to verify that the transcript # is a text based file before attempting processing. - return unless TEXT_TYPE.include? mime_type + return unless TEXT_TYPE.include? @mime_type # Docx files are a zip file containing XML files so require specialized handling to retrieve the content. - plaintext = Zip::File.open_buffer(transcript_file.download).parse_docx if mime_type == 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' + plaintext = Avalon::DocxFile.new(@transcript).unformatted_text if @mime_type == 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' # SRT and VTT files are plaintext, so it is sufficient to just download the content without additional handling. - plaintext ||= transcript_file.download + plaintext ||= @transcript end - # Passed in transcript_file must be instance of ActiveStorage::Attached::One - def self.normalize_transcript transcript_file - plaintext = self.transcript_plaintext(transcript_file) + def normalize_transcript + plaintext = transcript_plaintext return if plaintext.blank? normalized_plaintext = plaintext.gsub("\r\n", "\n") - # We only need the time cues and associated text content indexed so we remove the subtitle - # numbers and the VTT header, if present. - headerless_plaintext = normalized_plaintext.gsub(/WEBVTT.+?(?=\d{2}:\d{2}:\d{2})/m, "") - headerless_plaintext.gsub(/\d+\n(?=\d{2}:\d{2}:\d{2})/, "") + normalized_transcript = normalize_timed_text(normalized_plaintext) if @mime_type == 'text/vtt' || @mime_type == 'text/srt' + normalized_transcript ||= normalized_plaintext + end + + def normalize_timed_text(plaintext) + # Remove WEBVTT header and anything before the first time cue. + # `m` flag denotes a multiline regex, so the wildcard will match newline characters. + # Match everything before the first timecue. `.*?` is a non-greedy wildcard matcher, + # so it will only match until the first time cue. Without the `?`, it will match everything + # up to the last time cue. + headers_removed = plaintext.gsub(/WEBVTT.*?(?=\d{2,}*:*\d{2}:\d{2})/m, "") + # Remove subtitle identifiers + # Match arbitrary text followed by a single new line character, with a lookahead to check + # the next bit of text is a time cue. + identifiers_removed = headers_removed.gsub(/.+\n(?=\d{2,}*:*\d{2}:\d{2})/, "") + # Remove inline styling. + # Match the full time cue line including everything until the new line character, + # putting the time cue in a capture group. Then replace the full line with the captured time cue. + styling_removed = identifiers_removed.gsub(/(\d{2,}*:*\d{2}:\d{2}\.\d{3} --> \d{2,}*:*\d{2}:\d{2}\.\d{3}).+/, '\1') + # Remove body notes + # Match the NOTE designator and its associated text until the next double line break. + # Notes can be multiline, so we need to check for two newline characters to find the end. + notes_removed = styling_removed.gsub(/NOTE.*?(?=\n\n)/m, "") + # Remove HTML tags because VTT files can include HTML as part of their payload and we + # do not want that represented in the index. + notes_removed.gsub(/<\/?[^>]*>/, '') end end end \ No newline at end of file diff --git a/spec/fixtures/chunk_test.vtt b/spec/fixtures/chunk_test.vtt index 6ff31aa8b3..6130f7d022 100644 --- a/spec/fixtures/chunk_test.vtt +++ b/spec/fixtures/chunk_test.vtt @@ -1,14 +1,31 @@ WEBVTT +STYLE +::cue { + color: black; + font-size: 40px; +} + +NOTE This is all a test + +STYLE +::cue(b) { + color: red; +} + 1 -00:00:01.200 --> 00:00:21.000 -[music] +00:00:01.200 --> 00:00:21.000 position:50%,line-left align:center size: 40% +[music] -2 +2 - Speech Begins 00:00:22.200 --> 00:00:26.600 Just before lunch one day, a puppet show was put on at school. +NOTE +Multiline comment +support testing + 3 00:00:26.700 --> 00:00:31.500 It was called "Mister Bungle Goes to Lunch". @@ -17,6 +34,9 @@ It was called "Mister Bungle Goes to Lunch". 00:00:31.600 --> 00:00:34.500 It was fun to watch. +NOTE More multiline comment +testing + 5 00:00:36.100 --> 00:00:41.300 In the puppet show, Mr. Bungle came to the diff --git a/spec/models/supplemental_file_spec.rb b/spec/models/supplemental_file_spec.rb index 96e7930a5d..de5f9f9a76 100644 --- a/spec/models/supplemental_file_spec.rb +++ b/spec/models/supplemental_file_spec.rb @@ -108,7 +108,7 @@ expect(subject).to be_a Array expect(subject.length).to eq 11 expect(subject.all? { |s| s.is_a?(String) }).to eq true - expect(subject[3]).to include "The Emigrant. Crossing the Alleghanies. The boundless Wilder- ness. The Hut on the Holston. Life's Necessaries." + expect(subject[3]).to include "The Emigrant. Crossing the Alleghanies. The boundless Wilder- ness. The Hut on the Holston. Life's Necessaries." end end @@ -118,17 +118,27 @@ expect(subject).to be_a Array expect(subject.length).to eq 11 expect(subject.all? { |s| s.is_a?(String) }).to eq true - expect(subject[3]).to include "The Emigrant. Crossing the Alleghanies. The boundless Wilder- ness. The Hut on the Holston. Life's Necessaries." + expect(subject[3]).to include "The Emigrant. Crossing the Alleghanies. The boundless Wilder- ness. The Hut on the Holston. Life's Necessaries." end end context 'vtt' do let(:file) { FactoryBot.create(:supplemental_file, file: fixture_file_upload(Rails.root.join('spec', 'fixtures', 'chunk_test.vtt'), 'text/vtt')) } + let(:parsed_text) { [ + "00:00:01.200 --> 00:00:21.000 [music]", + "00:00:22.200 --> 00:00:26.600 Just before lunch one day, a puppet show was put on at school.", + '00:00:26.700 --> 00:00:31.500 It was called "Mister Bungle Goes to Lunch".', + "00:00:31.600 --> 00:00:34.500 It was fun to watch.", + "00:00:36.100 --> 00:00:41.300 In the puppet show, Mr. Bungle came to the boys' room on his way to lunch.", + "00:00:41.400 --> 00:00:46.200 He looked at his hands. His hands were dirty and his hair was messy.", + "00:00:46.300 --> 00:00:51.100 But Mr. Bungle didn't stop to wash his hands or comb his hair.", + "00:00:51.200 --> 00:00:54.900 He went right to lunch.", + "00:00:57.900 --> 00:01:05.700 Then, instead of getting into line at the lunchroom, Mr. Bungle pushed everyone aside and went right to the front.", + "00:01:06.000 --> 00:01:11.800 Even though this made the children laugh, no one thought that was a fair thing to do." + ] } + it 'splits the text by time cue' do - expect(subject).to be_a Array - expect(subject.length).to eq 10 - expect(subject.all? { |s| s.is_a?(String) }).to eq true - expect(subject[0]).to eq "00:00:01.200 --> 00:00:21.000 [music]" + expect(subject).to match_array parsed_text end end From 04c2be702b908f94bf517c62d0703028f70a29a0 Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Mon, 13 May 2024 16:06:45 -0400 Subject: [PATCH 047/152] Rename methods and fix tests --- app/models/supplemental_file.rb | 5 +++-- lib/avalon/transcript_parser.rb | 19 ++++++++++++------- spec/models/supplemental_file_spec.rb | 27 +++++++++++++++++---------- 3 files changed, 32 insertions(+), 19 deletions(-) diff --git a/app/models/supplemental_file.rb b/app/models/supplemental_file.rb index 7e03fa460e..25a2473740 100644 --- a/app/models/supplemental_file.rb +++ b/app/models/supplemental_file.rb @@ -100,9 +100,10 @@ def to_solr solr_doc end - # Should this be a private method? def segment_transcript transcript - normalized_transcript = Avalon::TranscriptParser.new(transcript).normalize_transcript + normalized_transcript = Avalon::TranscriptParser.new(transcript).normalized_text + return unless normalized_transcript.present? + chunked_transcript = normalized_transcript.split(/\n\n+/) chunked_transcript.map(&:strip).map { |cue| cue.gsub("\n", " ").squeeze(' ') }.compact diff --git a/lib/avalon/transcript_parser.rb b/lib/avalon/transcript_parser.rb index 5980964783..7c6a3a3f4b 100644 --- a/lib/avalon/transcript_parser.rb +++ b/lib/avalon/transcript_parser.rb @@ -24,18 +24,21 @@ def initialize(transcript_file) @transcript = transcript_file.download end - def transcript_plaintext + def plaintext # Transcripts can have arbitrary files imported. We need to verify that the transcript # is a text based file before attempting processing. return unless TEXT_TYPE.include? @mime_type - # Docx files are a zip file containing XML files so require specialized handling to retrieve the content. - plaintext = Avalon::DocxFile.new(@transcript).unformatted_text if @mime_type == 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' - # SRT and VTT files are plaintext, so it is sufficient to just download the content without additional handling. - plaintext ||= @transcript + + unless @plaintext + # Docx files are a zip file containing XML files so require specialized handling to retrieve the content. + @plaintext = Avalon::DocxFile.new(@transcript).unformatted_text if @mime_type == 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' + # SRT and VTT files are plaintext, so it is sufficient to just download the content without additional handling. + @plaintext ||= @transcript + end + @plaintext end - def normalize_transcript - plaintext = transcript_plaintext + def normalized_text return if plaintext.blank? normalized_plaintext = plaintext.gsub("\r\n", "\n") @@ -43,6 +46,8 @@ def normalize_transcript normalized_transcript ||= normalized_plaintext end + private + def normalize_timed_text(plaintext) # Remove WEBVTT header and anything before the first time cue. # `m` flag denotes a multiline regex, so the wildcard will match newline characters. diff --git a/spec/models/supplemental_file_spec.rb b/spec/models/supplemental_file_spec.rb index b9ae1aad6a..a34afee6bf 100644 --- a/spec/models/supplemental_file_spec.rb +++ b/spec/models/supplemental_file_spec.rb @@ -48,8 +48,8 @@ describe 'scopes' do describe 'with_tag' do - let!(:subject) { FactoryBot.create(:supplemental_file, tags: ['transcript', 'machine_generated']) } - let!(:other_transcript) { FactoryBot.create(:supplemental_file, tags: ['transcript']) } + let!(:subject) { FactoryBot.create(:supplemental_file, :with_transcript_file, tags: ['transcript', 'machine_generated']) } + let!(:other_transcript) { FactoryBot.create(:supplemental_file, :with_transcript_file, tags: ['transcript']) } let!(:another_file) { FactoryBot.create(:supplemental_file, :with_caption_file, tags: ['caption']) } it 'filters for a single tag' do @@ -72,9 +72,9 @@ it "should solrize transcripts" do expect(transcript.to_solr[ "transcript_tsim" ]).to be_a Array - expect(transcript.to_solr[ "transcript_tsim" ][0]).to eq "WEBVTT FILE\r \r 1\r 00:00:03.500 --> 00:00:05.000\r Example captions" + expect(transcript.to_solr[ "transcript_tsim" ][0]).to eq "00:00:03.500 --> 00:00:05.000 Example captions" expect(caption_transcript.to_solr[ "transcript_tsim" ]).to be_a Array - expect(transcript.to_solr[ "transcript_tsim" ][0]).to eq "WEBVTT FILE\r \r 1\r 00:00:03.500 --> 00:00:05.000\r Example captions" + expect(transcript.to_solr[ "transcript_tsim" ][0]).to eq "00:00:03.500 --> 00:00:05.000 Example captions" end it "should not solrize non-transcripts" do @@ -135,10 +135,10 @@ end end - describe '.segment_transcript' do - subject { SupplementalFile.segment_transcript(file.file) } + describe '#segment_transcript' do + subject { file.segment_transcript(file.file) } - context 'plain text' do + context 'plain text file' do let(:file) { FactoryBot.create(:supplemental_file, file: fixture_file_upload(Rails.root.join('spec', 'fixtures', 'chunk_test.txt'), 'text/plain')) } it 'splits the text by paragraph' do expect(subject).to be_a Array @@ -148,7 +148,7 @@ end end - context 'docx' do + context 'docx file' do let(:file) { FactoryBot.create(:supplemental_file, file: fixture_file_upload(Rails.root.join('spec', 'fixtures', 'chunk_test.docx'), 'application/vnd.openxmlformats-officedocument.wordprocessingml.document')) } it 'splits the text by paragraph' do expect(subject).to be_a Array @@ -158,7 +158,7 @@ end end - context 'vtt' do + context 'vtt file' do let(:file) { FactoryBot.create(:supplemental_file, file: fixture_file_upload(Rails.root.join('spec', 'fixtures', 'chunk_test.vtt'), 'text/vtt')) } let(:parsed_text) { [ "00:00:01.200 --> 00:00:21.000 [music]", @@ -178,7 +178,7 @@ end end - context 'srt' do + context 'srt file' do let(:file) { FactoryBot.create(:supplemental_file, file: fixture_file_upload(Rails.root.join('spec', 'fixtures', 'chunk_test.srt'), 'text/srt')) } it 'splits the text by time cue' do expect(subject).to be_a Array @@ -187,5 +187,12 @@ expect(subject[0]).to eq "00:00:01,200 --> 00:00:21,000 [music]" end end + + context 'non-text file' do + let(:file) { FactoryBot.create(:supplemental_file, :with_attached_file) } + it 'returns nil' do + expect(subject).to be nil + end + end end end From 07ddaa26b1b84a59223b6dad0a0aaeccc1ee8108 Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Tue, 14 May 2024 10:57:40 -0400 Subject: [PATCH 048/152] Remove unneeded method and comment --- app/models/master_file.rb | 4 ---- spec/models/search_builder_spec.rb | 2 +- spec/models/supplemental_file_spec.rb | 1 - 3 files changed, 1 insertion(+), 6 deletions(-) diff --git a/app/models/master_file.rb b/app/models/master_file.rb index ebd0a9513c..1a6d4e1240 100644 --- a/app/models/master_file.rb +++ b/app/models/master_file.rb @@ -548,10 +548,6 @@ def self.calculate_working_file_path(old_path) protected - def segment_transcript transcript - transcript.split(/\n\n+/).map(&:strip).map { |cue| cue.gsub!("\n", " ") }.compact! - end - def mediainfo Mediainfo.new(FileLocator.new(file_location).location, headers: @auth_header) end diff --git a/spec/models/search_builder_spec.rb b/spec/models/search_builder_spec.rb index c62f3e06aa..926504df4c 100644 --- a/spec/models/search_builder_spec.rb +++ b/spec/models/search_builder_spec.rb @@ -43,7 +43,7 @@ # the conditional in the model could see. Running the create in a :before block works. before { FactoryBot.create(:supplemental_file, :with_transcript_file, :with_transcript_tag) } - it "should add seaction transcript searching to the solr query" do + it "should add section transcript searching to the solr query" do subject.search_section_transcripts(solr_parameters) expect(solr_parameters[:defType]).to eq "lucene" expect(solr_parameters[:q]).to eq "({!edismax v=\"Example\"}) {!join to=id from=isPartOf_ssim}{!join to=id from=isPartOf_ssim}transcript_tsim:Example" diff --git a/spec/models/supplemental_file_spec.rb b/spec/models/supplemental_file_spec.rb index a34afee6bf..5155ef9435 100644 --- a/spec/models/supplemental_file_spec.rb +++ b/spec/models/supplemental_file_spec.rb @@ -65,7 +65,6 @@ end describe '#to_solr' do - # TODO: Update tests once chunking is in place let(:caption) { FactoryBot.create(:supplemental_file, :with_caption_file, :with_caption_tag) } let(:transcript) { FactoryBot.create(:supplemental_file, :with_transcript_file, :with_transcript_tag) } let(:caption_transcript) { FactoryBot.create(:supplemental_file, :with_caption_file, tags: ['caption', 'transcript']) } From e58544068c42f5773b7ab9a36cef64c606759edf Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Tue, 14 May 2024 14:32:08 -0400 Subject: [PATCH 049/152] Fix thumbnail display in search results --- app/models/search_builder.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/search_builder.rb b/app/models/search_builder.rb index 18880c0bd1..bf823cb468 100644 --- a/app/models/search_builder.rb +++ b/app/models/search_builder.rb @@ -64,7 +64,7 @@ def term_frequency_counts(solr_parameters) transcripts_present = SupplementalFile.with_tag('transcript').any? # List of fields for displaying on search results (Blacklight index fields) - fl = ['id', 'has_model_ssim', 'title_tesi', 'date_issued_ssi', 'creator_ssim', 'abstract_ssi', 'duration_ssi', 'section_id_ssim'] + fl = ['id', 'has_model_ssim', 'title_tesi', 'date_issued_ssi', 'creator_ssim', 'abstract_ssi', 'duration_ssi', 'section_id_ssim', 'avalon_resource_type_ssim'] # Add a field for matching child sections fl << "sections:[subquery]" From 164e5fea8c0846cde57580721a912cae8750a389 Mon Sep 17 00:00:00 2001 From: Dananji Withana Date: Wed, 15 May 2024 10:55:42 -0400 Subject: [PATCH 050/152] Fix iiif validator errors for media object manifest generation (#5810) * Fix iiif validator errors for media object manifest generation * Fix audio canvas height,width, and thumbnail url * Fix failing test * Use asset_host for thumbnail id serialization instead of thumbnail endpoint Co-authored-by: Chris Colvard * Fix failing tests for assets --------- Co-authored-by: Chris Colvard --- app/models/iiif_canvas_presenter.rb | 8 ++++++-- app/models/iiif_manifest_presenter.rb | 2 +- config/initializers/default_host.rb | 1 + spec/helpers/application_helper_spec.rb | 6 +++--- spec/models/iiif_canvas_presenter_spec.rb | 20 ++++++++++++++++++++ 5 files changed, 31 insertions(+), 6 deletions(-) diff --git a/app/models/iiif_canvas_presenter.rb b/app/models/iiif_canvas_presenter.rb index f260d6f576..53688984bd 100644 --- a/app/models/iiif_canvas_presenter.rb +++ b/app/models/iiif_canvas_presenter.rb @@ -70,12 +70,16 @@ def placeholder_content elsif section_processing?(@master_file) IIIFManifest::V3::DisplayContent.new(nil, label: I18n.t('media_object.conversion_msg'), + width: 1280, + height: 720, type: 'Text', format: 'text/plain') else support_email = Settings.email.support IIIFManifest::V3::DisplayContent.new(nil, label: I18n.t('errors.missing_derivatives_error') % [support_email, support_email], + width: 1280, + height: 720, type: 'Text', format: 'text/plain') end @@ -213,8 +217,8 @@ def parse_hour_min_sec(s) def manifest_attributes(quality, media_type) media_hash = { label: quality, - width: master_file.width.to_i, - height: master_file.height.to_i, + width: (master_file.width || '1280').to_i, + height: (master_file.height || MasterFile::AUDIO_HEIGHT).to_i, duration: stream_info[:duration], type: media_type, format: 'application/x-mpegURL' diff --git a/app/models/iiif_manifest_presenter.rb b/app/models/iiif_manifest_presenter.rb index eb3292b897..6933fc6903 100644 --- a/app/models/iiif_manifest_presenter.rb +++ b/app/models/iiif_manifest_presenter.rb @@ -59,7 +59,7 @@ def thumbnail def ranges [ IiifManifestRange.new( - label: { '@none'.to_sym => media_object.title }, + label: { 'none' => [media_object.title] }, items: file_set_presenters.collect(&:range) ) ] diff --git a/config/initializers/default_host.rb b/config/initializers/default_host.rb index 61103d9106..f963ab0689 100644 --- a/config/initializers/default_host.rb +++ b/config/initializers/default_host.rb @@ -17,6 +17,7 @@ Rails.application.routes.default_url_options.merge!( server_options ) ActionMailer::Base.default_url_options.merge!( server_options ) ApplicationController.default_url_options = server_options + ActionController::Base.asset_host = URI("#{Settings.domain.protocol}://#{Settings.domain.host}:#{Settings.domain.port}").to_s end # Required for rails 6+ diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index 8f517d29e9..e09724ed26 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -129,15 +129,15 @@ end it "should return audio icon" do doc = {"avalon_resource_type_ssim" => ['Sound Recording', 'Sound Recording'] } - expect(helper.image_for(doc).start_with?('/assets/audio_icon')).to be_truthy + expect(helper.image_for(doc).start_with?("#{root_url}assets/audio_icon")).to be_truthy end it "should return video icon" do doc = {"avalon_resource_type_ssim" => ['Moving Image'] } - expect(helper.image_for(doc).start_with?('/assets/video_icon')).to be_truthy + expect(helper.image_for(doc).start_with?("#{root_url}assets/video_icon")).to be_truthy end it "should return hybrid icon" do doc = {"avalon_resource_type_ssim" => ['Moving Image', 'Sound Recording'] } - expect(helper.image_for(doc).start_with?('/assets/hybrid_icon')).to be_truthy + expect(helper.image_for(doc).start_with?("#{root_url}assets/hybrid_icon")).to be_truthy end it "should return nil when only unprocessed video" do doc = {"section_id_ssim" => ['1'], "avalon_resource_type_ssim" => [] } diff --git a/spec/models/iiif_canvas_presenter_spec.rb b/spec/models/iiif_canvas_presenter_spec.rb index cfe4f83dcb..609371b172 100644 --- a/spec/models/iiif_canvas_presenter_spec.rb +++ b/spec/models/iiif_canvas_presenter_spec.rb @@ -56,12 +56,22 @@ it 'has format' do expect(subject.format).to eq "application/x-mpegURL" end + + it 'has height and width' do + expect(subject.width).to eq 1280 + expect(subject.height).to eq 40 + end end context 'when video file' do it 'has format' do expect(subject.format).to eq "application/x-mpegURL" end + + it 'has height and width' do + expect(subject.width).to eq 1024 + expect(subject.height).to eq 768 + end end end @@ -185,6 +195,11 @@ it 'has label' do expect(subject.label).to eq I18n.t('errors.missing_derivatives_error') % [Settings.email.support, Settings.email.support] end + + it 'has height and width' do + expect(subject.width).to eq 1280 + expect(subject.height).to eq 720 + end end context 'when master file is processing' do @@ -201,6 +216,11 @@ it 'has label' do expect(subject.label).to eq I18n.t('media_object.conversion_msg') end + + it 'has height and width' do + expect(subject.width).to eq 1280 + expect(subject.height).to eq 720 + end end end From 276249f92654d77bd014ffb11722d2dbe1d0b007 Mon Sep 17 00:00:00 2001 From: Chris Colvard Date: Wed, 15 May 2024 16:00:16 -0400 Subject: [PATCH 051/152] Allow for Settings.domain to be a string and not hash --- config/initializers/default_host.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/initializers/default_host.rb b/config/initializers/default_host.rb index f963ab0689..ec661b1fc8 100644 --- a/config/initializers/default_host.rb +++ b/config/initializers/default_host.rb @@ -17,7 +17,7 @@ Rails.application.routes.default_url_options.merge!( server_options ) ActionMailer::Base.default_url_options.merge!( server_options ) ApplicationController.default_url_options = server_options - ActionController::Base.asset_host = URI("#{Settings.domain.protocol}://#{Settings.domain.host}:#{Settings.domain.port}").to_s + ActionController::Base.asset_host = URI("#{server_options[:protocol]}://#{server_options[:host]}:#{server_options[:port]}").to_s end # Required for rails 6+ From 6abfb28315463af9a667e9e67dfa937c8dbe7962 Mon Sep 17 00:00:00 2001 From: charumitraravi <52535309+charumitraravi@users.noreply.github.com> Date: Thu, 16 May 2024 12:57:37 -0400 Subject: [PATCH 052/152] fixed test failures, added collections smoke tests, added cypress_dev config (#5814) --- spec/cypress/cypress_dev.config.js | 28 +++++++ .../cypress/integration/auth_identity_spec.js | 4 +- spec/cypress/integration/collections_spec.js | 83 +++++++++++++++++++ spec/cypress/integration/homepage_spec.js | 4 +- spec/cypress/integration/media_object_spec.js | 5 +- spec/cypress/integration/navigation_spec.js | 5 +- 6 files changed, 121 insertions(+), 8 deletions(-) create mode 100644 spec/cypress/cypress_dev.config.js create mode 100644 spec/cypress/integration/collections_spec.js diff --git a/spec/cypress/cypress_dev.config.js b/spec/cypress/cypress_dev.config.js new file mode 100644 index 0000000000..616dff6f41 --- /dev/null +++ b/spec/cypress/cypress_dev.config.js @@ -0,0 +1,28 @@ +const { defineConfig } = require("cypress"); + +module.exports = defineConfig({ + env: { + "USERS_ADMINISTRATOR_EMAIL": "archivist1@example.com", + "USERS_ADMINISTRATOR_PASSWORD": "archivist1", + "USERS_USER_EMAIL":"chravi@iu.edu", + "USERS_USER_PASSWORD": "Test1234!", + "MEDIA_OBJECT_ID": "fj236208t", + "MEDIA_OBJECT_TITLE":"Beginning Responsibility: Lunchroom Manners", + "SEARCH_COLLECTION":"7.7 regression test", + }, + downloadsFolder: "spec/cypress/downloads", + fixturesFolder: "spec/cypress/fixtures", + screenshotsFolder: "spec/cypress/screenshots", + videosFolder: "spec/cypress/videos", + browser: process.env.BROWSER || 'electron', // + e2e: { + setupNodeEvents(on, config) { + // implement node event listeners here + }, + baseUrl: "https://avalon-dev.dlib.indiana.edu/", + supportFile: "spec/cypress/support/e2e.js", + specPattern: "spec/cypress/integration/**/*.js" + }, + + +}); diff --git a/spec/cypress/integration/auth_identity_spec.js b/spec/cypress/integration/auth_identity_spec.js index 6e100fddd2..a2fde335ba 100644 --- a/spec/cypress/integration/auth_identity_spec.js +++ b/spec/cypress/integration/auth_identity_spec.js @@ -19,7 +19,7 @@ context('Authentication', () => { // Error when creating duplicate user it('.duplicate_user_error()', () => { cy.visit('/users/sign_up') - cy.get('form').within(() => { + cy.get('form.new_user').within(() => { cy.get('#user_username').type('test1').should('have.value', 'test1') // Only yield inputs within form cy.get('#user_email').type('test1@example.com').should('have.value', 'test1@example.com') // Only yield inputs within form cy.get('#user_password').type('test1') // Only yield textareas within form @@ -28,7 +28,7 @@ context('Authentication', () => { cy.get('input[name=commit]').last().click() cy.visit('/users/sign_up') - cy.get('form').within(() => { + cy.get('form.new_user').within(() => { cy.get('#user_username').type('test1').should('have.value', 'test1') // Only yield inputs within form cy.get('#user_email').type('test1@example.com').should('have.value', 'test1@example.com') // Only yield inputs within form cy.get('#user_password').type('test1') // Only yield textareas within form diff --git a/spec/cypress/integration/collections_spec.js b/spec/cypress/integration/collections_spec.js new file mode 100644 index 0000000000..e1fd4a199e --- /dev/null +++ b/spec/cypress/integration/collections_spec.js @@ -0,0 +1,83 @@ +/* + * Copyright 2011-2024, 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 --- +*/ + +context('Collections', () => { + //Since it takes a while for a newly created collection to reflect in search, we are using static search data + const search_collection = Cypress.env('SEARCH_COLLECTION') + const collection_title = `Automation collection title ${Math.floor(Math.random() * 10000) + 1}` + + + // checks navigation to Browse + it('Verify whether an admin user is able to create a collection - @T553cda51', () => { + cy.login('administrator') + cy.visit('/') + cy.get('#manageDropdown').click() + cy.contains('Manage Content').click() + cy.contains('Create Collection').click() + //Create dynamic data below + cy.get('#admin_collection_name').type(collection_title).should('have.value', collection_title) + cy.get('#admin_collection_description').type("Collection desc").should('have.value', 'Collection desc') + cy.get('#admin_collection_contact_email').type("admin@example.com").should('have.value', 'admin@example.com') + cy.get('#admin_collection_website_url').type("https://www.google.com").should('have.value', 'https://www.google.com') + cy.get('#admin_collection_website_label').type("test label").should('have.value', 'test label') + cy.get('input[value="Create Collection"]').click() + // Handle the alert + Cypress.on('uncaught:exception', (err, runnable) => { + // Return false to prevent Cypress from failing the test due to the issue: + //"TypeError: The following error originated from your application code, not from Cypress." + //Cannot read properties of undefined (reading 'success') + return false; + }); + //Assert title and edit collection button. Can add more assertions here if required. + cy.contains('h2', collection_title).should('be.visible') + cy.contains('button', 'Edit Collection Info').should('exist') + + //Can add a Tearoff code here: delete the created collection using DELETE request for data cleanup + + }) + + it("Verify whether the user is able to search for Collections-'@Tf7cefb09", () => { + cy.login('administrator') + cy.visit('/') + cy.get('a[href="/collections"]').click() + //Using an existing collection for this test case for now, since it takes a while for the newly created test case to get reflected + // Generate a random index to slice the title + const startIndex = Math.floor(Math.random() * (search_collection.length-3)); + const sliceLength = Math.floor(Math.random() * (search_collection.length - startIndex)) + 1; // Random slice length + //slice a random portion of the collection title as the search keyword to ensure variablitity in testing + const search_keyword = search_collection.slice(startIndex, startIndex + sliceLength) + cy.get('input[placeholder="Search collections..."]').type(search_keyword).should('have.value', search_keyword) + cy.screenshot('search'); + cy.get('.card-body').contains('a', search_collection); + +}) + + + it('Verify deleting a collection - @T959a56df', () => { + cy.login('administrator') + cy.visit('/') + cy.get('#manageDropdown').click() + cy.contains('Manage Content').click() + cy.contains('a', collection_title).closest('tr').find('.btn-danger').click(); + //May require adding steps to select a collection to move the existing items, when dealing with non empty collections + cy.get('input[value="Yes, I am sure"]').click() + cy.contains('h1', 'My Collections') + //May need to update this assertion to ensire that this is valid during pagination of collections. Another alternative would be to check via API or search My collections + cy.contains('a', collection_title).should('not.exist'); + + }) + +}) diff --git a/spec/cypress/integration/homepage_spec.js b/spec/cypress/integration/homepage_spec.js index e6932aa708..8c7745b81c 100644 --- a/spec/cypress/integration/homepage_spec.js +++ b/spec/cypress/integration/homepage_spec.js @@ -66,7 +66,7 @@ context('Homepage', () => { it('.describe_sign_in_page() - click on a DOM element', () => { cy.visit('/') cy.get('a[href*="/users/sign_in"] ').first().click() - cy.contains('Email / Password').click() + cy.contains('Username or email').click() cy.contains('Username or email') cy.contains('Password') cy.contains('Sign up') @@ -86,7 +86,7 @@ context('Homepage', () => { // is able to create new account it('.create_new_account()', () => { cy.visit('/users/sign_up') - cy.get('form').within(() => { + cy.get('form.new_user').within(() => { cy.get('#user_username').type('Sumith').should('have.value', 'Sumith') // Only yield inputs within form cy.get('#user_email').type('sumith3@example.com').should('have.value', 'sumith3@example.com') // Only yield inputs within form cy.get('#user_password').type('sumith3') // Only yield textareas within form diff --git a/spec/cypress/integration/media_object_spec.js b/spec/cypress/integration/media_object_spec.js index 9ffcd96228..e2b168c21f 100644 --- a/spec/cypress/integration/media_object_spec.js +++ b/spec/cypress/integration/media_object_spec.js @@ -17,6 +17,7 @@ context('Media objects', () => { const media_object_id = Cypress.env('MEDIA_OBJECT_ID') + const media_object_title = Cypress.env('MEDIA_OBJECT_TITLE') // can visit a media object it('.visit_media_object()', () => { @@ -24,11 +25,11 @@ context('Media objects', () => { // The below code is hard-coded for a media object url. This needs to be changed with a valid object URL later for each website. cy.visit('/media_objects/' + media_object_id) cy.contains('Unknown item').should('not.exist') - cy.contains('Beginning Responsibility: Lunchroom Manners') + cy.contains(media_object_title) cy.contains('Main contributor') cy.contains('Date') // This below line is to play the video. If the video is not playable, this might return error. In that case, comment the below code. - cy.get('#mep_0').click() + cy.get('button[title="Play"]').click() }) // Open multiple media objects in different tabs and play it. diff --git a/spec/cypress/integration/navigation_spec.js b/spec/cypress/integration/navigation_spec.js index 4f5276194a..4e6fb4bfe9 100644 --- a/spec/cypress/integration/navigation_spec.js +++ b/spec/cypress/integration/navigation_spec.js @@ -74,7 +74,8 @@ context('Navigations', () => { // Search - is able to enter keyword and perform search it('.search()', () => { cy.visit('/') - cy.get('#searchField').type('lunchroom').should('have.value', 'lunchroom') // Only yield inputs within form - cy.get('#global-search-submit').click() + cy.get("li[class='nav-item'] a[class='nav-link']").click() + cy.get("input.global-search-input[placeholder='Search this site']").first().type('lunchroom').should('have.value', 'lunchroom') // Only yield inputs within form + cy.get('button.global-search-submit').first().click() }) }) From 7dc38503be928e2defb5cea7b29c5650d06820fe Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Thu, 23 May 2024 14:54:17 -0400 Subject: [PATCH 053/152] Implement IIIF Content Search service --- app/controllers/master_files_controller.rb | 9 +- lib/avalon/transcript_parser.rb | 8 ++ lib/avalon/transcript_search.rb | 98 +++++++++++++++++++ .../master_files_controller_spec.rb | 32 ++++++ spec/lib/avalon/transcript_search.rb | 75 ++++++++++++++ 5 files changed, 221 insertions(+), 1 deletion(-) create mode 100644 lib/avalon/transcript_search.rb create mode 100644 spec/lib/avalon/transcript_search.rb diff --git a/app/controllers/master_files_controller.rb b/app/controllers/master_files_controller.rb index 29037a1c2d..9a3a11132d 100644 --- a/app/controllers/master_files_controller.rb +++ b/app/controllers/master_files_controller.rb @@ -13,6 +13,7 @@ # --- END LICENSE_HEADER BLOCK --- # require 'avalon/controller/controller_behavior' +require 'avalon/transcript_search' include SecurityHelper @@ -355,7 +356,7 @@ def download_derivative end def search - # TODO: Build search service + render json: search_response_json end protected @@ -442,4 +443,10 @@ def derivative_path File.join(ENV["ENCODE_WORK_DIR"], location).to_s end end + +private + + def search_response_json + Avalon::TranscriptSearch.new(query: params[:q], master_file: @master_file, request_url: request.url).iiif_content_search.to_json + end end diff --git a/lib/avalon/transcript_parser.rb b/lib/avalon/transcript_parser.rb index 7c6a3a3f4b..1ed09e90db 100644 --- a/lib/avalon/transcript_parser.rb +++ b/lib/avalon/transcript_parser.rb @@ -46,6 +46,14 @@ def normalized_text normalized_transcript ||= normalized_plaintext end + # Separate time cue and text content from a single line of timed text to facilitate result formatting of transcript searches. + def self.extract_single_time_cue(timed_text) + split_text = timed_text.match(/(\d{2,}*:?\d{2}:\d{2}\.?,?\d{3} --> \d{2,}*:?\d{2}:\d{2}\.?,?\d{3})(.*)/) + time_cue = split_text[1].sub(',', '.').gsub(/\s-->\s/, ',') + text = split_text[2].strip + [time_cue, text] + end + private def normalize_timed_text(plaintext) diff --git a/lib/avalon/transcript_search.rb b/lib/avalon/transcript_search.rb new file mode 100644 index 0000000000..e06f45fa3a --- /dev/null +++ b/lib/avalon/transcript_search.rb @@ -0,0 +1,98 @@ +# Copyright 2011-2024, 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 'avalon/transcript_parser' + +module Avalon + class TranscriptSearch + attr_reader :query, :master_file, :request_url + + def initialize(query:, master_file:, request_url: nil) + @query = query + @master_file = master_file + @request_url = request_url + end + + def perform_search + terms = query.split + term_subquery = terms.map { |term| "transcript_tsim:#{RSolr.solr_escape(term)}" }.join(" OR ") + ActiveFedora::SolrService.get("isPartOf_ssim:#{master_file.id} AND #{term_subquery}", + "fl": "id,mime_type_ssi", + "hl": true, + "hl.fl": "transcript_tsim", + "hl.snippets": 1000000, + "hl.fragsize": 0, + "hl.method": "original") + end + + def iiif_content_search + results = perform_search + + { + "@context": "http://iiif.io/api/search/2/context.json", + id: request_url || "#{Rails.application.routes.url_helpers.search_master_file_url(master_file.id)}?q=#{query}", + type: "AnnotationPage", + items: items_builder(results) + } + end + + private + + def items_builder search_results + formatted_response = [] + search_results["highlighting"].each do |result| + transcript_id = result.first.split('/').last.to_i + @mime_type = search_results["response"]["docs"].filter { |doc| doc["id"] == result.first }.first["mime_type_ssi"] + @canvas = "#{Rails.application.routes.url_helpers.media_object_url(master_file.media_object_id).to_s}/manifest/canvas/#{master_file.id}" + @target = Rails.application.routes.url_helpers.transcripts_master_file_supplemental_file_url(master_file.id, transcript_id) + + text_matches = result[1]["transcript_tsim"] + + formatted_response += process_items(text_matches) + end + + formatted_response + end + + def process_items(matches) + formatted_matches = [] + + matches.each do |cue| + if @mime_type == 'text/vtt' || @mime_type == 'text/srt' + time_cue, text = Avalon::TranscriptParser.extract_single_time_cue(cue) + end + + text ||= cue + + formatted_matches += [format_item(text, @target, time_cue: time_cue)] + end + + formatted_matches + end + + def format_item(result, target, time_cue: nil) + { + id: "#{@canvas}/search/#{SecureRandom.uuid}", + type: "Annotation", + motivation: "supplementing", + body: { + type: "TextualBody", + value: result, + format: 'text/plain' + }, + target: time_cue ? "#{target}#t=#{time_cue}" : target + } + end + end +end \ No newline at end of file diff --git a/spec/controllers/master_files_controller_spec.rb b/spec/controllers/master_files_controller_spec.rb index f9bb63cc61..6500216700 100644 --- a/spec/controllers/master_files_controller_spec.rb +++ b/spec/controllers/master_files_controller_spec.rb @@ -819,4 +819,36 @@ class << file end end end + + describe "search" do + let(:transcript_1) { FactoryBot.create(:supplemental_file, :with_transcript_tag, parent_id: parent_master_file.id, file: fixture_file_upload(Rails.root.join('spec', 'fixtures', 'chunk_test.vtt'), 'text/vtt')) } + let(:transcript_2) { FactoryBot.create(:supplemental_file, :with_transcript_tag, parent_id: parent_master_file.id, file: fixture_file_upload(Rails.root.join('spec', 'fixtures', 'chunk_test.txt'), 'text/plain')) } + let(:other_transcript) { FactoryBot.create(:supplemental_file, :with_transcript_tag, parent_id: other_master_file.id, file: fixture_file_upload(Rails.root.join('spec', 'fixtures', 'chunk_test.txt'), 'text/plain')) } + let(:parent_master_file) { FactoryBot.create(:master_file, :with_media_object) } + let(:other_master_file) { FactoryBot.create(:master_file, :with_media_object)} + + before :each do + parent_master_file.supplemental_files += [transcript_1, transcript_2] + other_master_file.supplemental_files += [other_transcript] + parent_master_file.save + other_master_file.save + end + + it "returns a list of matches from all of a master file's transcripts" do + get('search', params: { id: parent_master_file.id, q: 'before' } ) + result = JSON.parse(response.body) + items = result["items"] + expect(items.count).to eq 2 + expect(items[0]["body"]["value"]).to eq "Just before lunch one day, a puppet show was put on at school." + expect(items[0]["target"]).to eq "#{Rails.application.routes.url_helpers.transcripts_master_file_supplemental_file_url(parent_master_file.id, transcript_1.id)}#t=00:00:22.200,00:00:26.600" + expect(items[1]["body"]["value"]).to eq "A LITTLE more than a hundred years ago, a poor man, by the name of Crockett, embarked on board an emigrant-ship, in Ireland, for the New World. He was in the humblest station in life. But very- little is known respecting his uneventful career, excepting its tragical close. His family consisted of a wife and three or four children. Just before he sailed, or on the Atlantic passage, a son was born, to" + expect(items[1]["target"]).to eq Rails.application.routes.url_helpers.transcripts_master_file_supplemental_file_url(parent_master_file.id, transcript_2.id) + end + + it 'does not return matches from other master files' do + get('search', params: { id: parent_master_file.id, q: 'before' } ) + result = JSON.parse(response.body) + expect(result['items'].any? { |item| item["id"].include?(other_master_file.id) }).to eq false + end + end end diff --git a/spec/lib/avalon/transcript_search.rb b/spec/lib/avalon/transcript_search.rb new file mode 100644 index 0000000000..3dbcc48d92 --- /dev/null +++ b/spec/lib/avalon/transcript_search.rb @@ -0,0 +1,75 @@ +# Copyright 2011-2024, 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' +require 'avalon/transcript_search' + +describe Avalon::TranscriptSearch do + subject { described_class.new(query: 'before', master_file: parent_master_file) } + + let(:transcript) { FactoryBot.create(:supplemental_file, :with_transcript_tag, parent_id: parent_master_file.id, file: fixture_file_upload(Rails.root.join('spec', 'fixtures', 'chunk_test.txt'), 'text/plain')) } + let(:parent_master_file) { FactoryBot.create(:master_file, :with_media_object) } + + before :each do + parent_master_file.supplemental_files += [transcript] + parent_master_file.save + end + + describe '#perform_search' do + it 'returns a solr document of transcript chunks with the matching term' do + search = subject.perform_search + global_id = transcript.to_global_id.to_s + expect(search['response']['docs'].first['id']).to eq global_id + expect(search['response']['docs'].first['mime_type_ssi']).to eq transcript.file.content_type + expect(search['highlighting'][global_id]['transcript_tsim']).to be_a Array + expect(search['highlighting'][global_id]['transcript_tsim'].count).to eq 1 + expect(search['highlighting'][global_id]['transcript_tsim'].first).to include 'before' + end + + context 'with multiple terms in the query' do + subject { described_class.new(query: 'before lunch', master_file: parent_master_file) } + let(:transcript) { FactoryBot.create(:supplemental_file, :with_transcript_tag, parent_id: parent_master_file.id, file: fixture_file_upload(Rails.root.join('spec', 'fixtures', 'chunk_test.vtt'), 'text/vtt')) } + + it 'returns a solr document of transcript chunks with any of the matching terms' do + search = subject.perform_search + global_id = transcript.to_global_id.to_s + # solr search is case insensitive, so we convert results to lower case for simpler testing + expect(search['highlighting'][global_id]['transcript_tsim'].all? { |h| h.downcase.include?('before') || h.downcase.include?('lunch') }).to eq true + end + end + end + + describe '#iiif_content_search' do + it 'returns results in compliance with IIIF Content Search 2.0' do + allow(SecureRandom).to receive(:uuid).and_return('abc1234') + search = subject.iiif_content_search + expect(search["@context".to_sym]).to eq "http://iiif.io/api/search/2/context.json" + expect(search[:id]).to eq "#{Rails.application.routes.url_helpers.search_master_file_url(parent_master_file.id)}?q=before" + expect(search[:type]).to eq "AnnotationPage" + expect(search[:items]).to be_present + items = search[:items] + expect(items).to be_a Array + item = items.first + expect(item[:id]).to eq "#{Rails.application.routes.url_helpers.media_object_url(parent_master_file.media_object_id)}/manifest/canvas/#{parent_master_file.id}/search/abc1234" + expect(item[:type]).to eq 'Annotation' + expect(item[:motivation]).to eq 'supplementing' + expect(item[:body]).to be_present + expect(item[:body][:type]).to eq 'TextualBody' + expect(item[:body][:value]).to be_a String + expect(item[:body][:value]).to include 'before' + expect(item[:body][:format]).to eq 'text/plain' + expect(item[:target]).to eq Rails.application.routes.url_helpers.transcripts_master_file_supplemental_file_url(parent_master_file.id, transcript.id) + end + end +end \ No newline at end of file From 0094e507fe3d478b05e531bec50a3139bfe90193 Mon Sep 17 00:00:00 2001 From: Chris Colvard Date: Fri, 24 May 2024 09:08:26 -0400 Subject: [PATCH 054/152] Update cypress_dev.config.js --- spec/cypress/cypress_dev.config.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/cypress/cypress_dev.config.js b/spec/cypress/cypress_dev.config.js index 616dff6f41..e181c90d40 100644 --- a/spec/cypress/cypress_dev.config.js +++ b/spec/cypress/cypress_dev.config.js @@ -4,8 +4,8 @@ module.exports = defineConfig({ env: { "USERS_ADMINISTRATOR_EMAIL": "archivist1@example.com", "USERS_ADMINISTRATOR_PASSWORD": "archivist1", - "USERS_USER_EMAIL":"chravi@iu.edu", - "USERS_USER_PASSWORD": "Test1234!", + "USERS_USER_EMAIL":"user1@example.com", + "USERS_USER_PASSWORD": "testing_user1", "MEDIA_OBJECT_ID": "fj236208t", "MEDIA_OBJECT_TITLE":"Beginning Responsibility: Lunchroom Manners", "SEARCH_COLLECTION":"7.7 regression test", From 6e10bbea7007ce27f4f9777cc4597d4ac50a8b8e Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Fri, 24 May 2024 10:21:06 -0400 Subject: [PATCH 055/152] Use local instead of instance variables This commit also adds a couple tests to make sure timed text is handled properly in the iiif_content_search method. --- lib/avalon/transcript_search.rb | 18 +++++++++--------- spec/lib/avalon/transcript_search.rb | 14 ++++++++++++++ 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/lib/avalon/transcript_search.rb b/lib/avalon/transcript_search.rb index e06f45fa3a..83f70bc163 100644 --- a/lib/avalon/transcript_search.rb +++ b/lib/avalon/transcript_search.rb @@ -53,37 +53,37 @@ def items_builder search_results formatted_response = [] search_results["highlighting"].each do |result| transcript_id = result.first.split('/').last.to_i - @mime_type = search_results["response"]["docs"].filter { |doc| doc["id"] == result.first }.first["mime_type_ssi"] - @canvas = "#{Rails.application.routes.url_helpers.media_object_url(master_file.media_object_id).to_s}/manifest/canvas/#{master_file.id}" - @target = Rails.application.routes.url_helpers.transcripts_master_file_supplemental_file_url(master_file.id, transcript_id) + mime_type = search_results["response"]["docs"].filter { |doc| doc["id"] == result.first }.first["mime_type_ssi"] + canvas = "#{Rails.application.routes.url_helpers.media_object_url(master_file.media_object_id).to_s}/manifest/canvas/#{master_file.id}" + target = Rails.application.routes.url_helpers.transcripts_master_file_supplemental_file_url(master_file.id, transcript_id) text_matches = result[1]["transcript_tsim"] - formatted_response += process_items(text_matches) + formatted_response += process_items(text_matches, mime_type, canvas, target) end formatted_response end - def process_items(matches) + def process_items(matches, mime_type, canvas_url, target_url) formatted_matches = [] matches.each do |cue| - if @mime_type == 'text/vtt' || @mime_type == 'text/srt' + if mime_type == 'text/vtt' || mime_type == 'text/srt' time_cue, text = Avalon::TranscriptParser.extract_single_time_cue(cue) end text ||= cue - formatted_matches += [format_item(text, @target, time_cue: time_cue)] + formatted_matches += [format_item(text, canvas_url, target_url, time_cue: time_cue)] end formatted_matches end - def format_item(result, target, time_cue: nil) + def format_item(result, canvas, target, time_cue: nil) { - id: "#{@canvas}/search/#{SecureRandom.uuid}", + id: "#{canvas}/search/#{SecureRandom.uuid}", type: "Annotation", motivation: "supplementing", body: { diff --git a/spec/lib/avalon/transcript_search.rb b/spec/lib/avalon/transcript_search.rb index 3dbcc48d92..c6776df281 100644 --- a/spec/lib/avalon/transcript_search.rb +++ b/spec/lib/avalon/transcript_search.rb @@ -71,5 +71,19 @@ expect(item[:body][:format]).to eq 'text/plain' expect(item[:target]).to eq Rails.application.routes.url_helpers.transcripts_master_file_supplemental_file_url(parent_master_file.id, transcript.id) end + + context 'transcript with timed text' do + let(:transcript) { FactoryBot.create(:supplemental_file, :with_transcript_tag, parent_id: parent_master_file.id, file: fixture_file_upload(Rails.root.join('spec', 'fixtures', 'chunk_test.vtt'), 'text/vtt')) } + + it 'returns result value without time cues' do + item = subject.iiif_content_search[:items].first + expect(item[:body][:value]).to_not match(/\d{2}:\d{2}:\d{2}/) + end + + it 'returns result target with time cue' do + item = subject.iiif_content_search[:items].first + expect(item[:target]).to include '#t=00:00:22.200,00:00:26.600' + end + end end end \ No newline at end of file From 119f38259d71ff2b11e4ece6f887ac795b381a30 Mon Sep 17 00:00:00 2001 From: Dananji Withana Date: Wed, 29 May 2024 09:33:51 -0400 Subject: [PATCH 056/152] Add IIIF Manifest link to the share resource panel (#5825) * Add IIIF Manifest link to the share resource panel * Update app/views/media_objects/_share_resource.html.erb Co-authored-by: Mason Ballengee <68433277+masaball@users.noreply.github.com> * Change from code review: use rails routes to get manifest URL * Update app/views/media_objects/_share_resource.html.erb Co-authored-by: Chris Colvard --------- Co-authored-by: Mason Ballengee <68433277+masaball@users.noreply.github.com> Co-authored-by: Chris Colvard --- app/views/media_objects/_share_resource.html.erb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/views/media_objects/_share_resource.html.erb b/app/views/media_objects/_share_resource.html.erb index 1675dded1e..db6e2ef1fd 100644 --- a/app/views/media_objects/_share_resource.html.erb +++ b/app/views/media_objects/_share_resource.html.erb @@ -29,6 +29,11 @@ Unless required by applicable law or agreed to in writing, software distributed

<%= I18n.t('media_object.empty_share_section_permalink_notice') %>

<% end %>
+
+ + +
<% elsif section=='share-tabs' %> From ccff513f3582628ebb3b789ced6a8ca4f179443f Mon Sep 17 00:00:00 2001 From: Chris Colvard Date: Mon, 3 Jun 2024 14:50:08 -0400 Subject: [PATCH 057/152] Use JSON string array of ids instead of ordered_aggregation linked list (#5778) * Proof of concept * Cache MasterFiles to avoid calling fedora each time * Rename property and main read/write methods * Migrate from ordered_aggregation to JSON id array property lazily * Workaround #5834 * Rework migration callbacks and add some tests * Fix tests and convert #master_files calls to #sections * Further test fixes and reworking of saving master_files * Convert calls to master_files or master_file_ids to sections/section_ids and rename variables where appropriate as well * Remove #master_files/#master_file_ids from presenter and test #sections/#section_ids * Changes based upon code review --- app/controllers/bookmarks_controller.rb | 2 +- app/controllers/master_files_controller.rb | 7 +- app/controllers/media_objects_controller.rb | 41 ++--- app/helpers/media_objects_helper.rb | 6 +- app/helpers/security_helper.rb | 14 +- app/javascript/components/MediaObjectRamp.jsx | 10 +- app/models/avalon/rdf_vocab.rb | 1 + app/models/batch_entries.rb | 4 +- app/models/concerns/master_file_behavior.rb | 3 +- app/models/concerns/media_object_behavior.rb | 5 +- app/models/concerns/media_object_intercom.rb | 2 +- app/models/file_upload_step.rb | 17 +-- app/models/iiif_manifest_presenter.rb | 8 +- app/models/master_file.rb | 29 ++-- app/models/media_object.rb | 117 +++++++++++---- app/models/structure_step.rb | 7 +- app/presenters/speedy_af/proxy/master_file.rb | 3 +- .../speedy_af/proxy/media_object.rb | 34 ++--- .../ingest_batch_mailer/status_email.html.erb | 6 +- .../media_objects/_embed_checkout.html.erb | 2 +- app/views/media_objects/_item_view.html.erb | 6 +- app/views/media_objects/tree.html.erb | 2 +- spec/controllers/catalog_controller_spec.rb | 2 +- .../master_files_controller_spec.rb | 30 ++-- .../media_objects_controller_spec.rb | 115 +++++++------- spec/factories/media_objects.rb | 5 +- spec/features/login_redirect_spec.rb | 6 +- spec/helpers/media_objects_helper_spec.rb | 8 +- spec/jobs/media_object_indexing_job_spec.rb | 7 +- spec/lib/avalon/batch/entry_spec.rb | 2 +- spec/lib/avalon/intercom_spec.rb | 2 +- spec/models/batch_entries_spec.rb | 10 +- spec/models/file_upload_step_spec.rb | 4 +- spec/models/ingest_batch_spec.rb | 18 +-- spec/models/master_file_spec.rb | 47 +++--- spec/models/media_object_spec.rb | 140 +++++++++++++++--- spec/models/working_file_path_spec.rb | 28 ++-- .../speedy_af/proxy/media_object_spec.rb | 22 +++ 38 files changed, 469 insertions(+), 303 deletions(-) diff --git a/app/controllers/bookmarks_controller.rb b/app/controllers/bookmarks_controller.rb index 5da807b68a..f6938628dd 100644 --- a/app/controllers/bookmarks_controller.rb +++ b/app/controllers/bookmarks_controller.rb @@ -141,7 +141,7 @@ def add_to_playlist_action documents playlist = Playlist.find(params[:target_playlist_id]) Array(documents.map(&:id)).each do |id| media_object = SpeedyAF::Proxy::MediaObject.find(id) - media_object.ordered_master_files.to_a.each do |mf| + media_object.sections.each do |mf| clip = AvalonClip.create(master_file: mf) PlaylistItem.create(clip: clip, playlist: playlist) end diff --git a/app/controllers/master_files_controller.rb b/app/controllers/master_files_controller.rb index 9a3a11132d..ff23133395 100644 --- a/app/controllers/master_files_controller.rb +++ b/app/controllers/master_files_controller.rb @@ -203,13 +203,10 @@ def destroy authorize! :destroy, @master_file, message: "You do not have sufficient privileges to delete files" filename = File.basename(@master_file.file_location) if @master_file.file_location.present? filename ||= @master_file.id - media_object = MediaObject.find(@master_file.media_object_id) - media_object.ordered_master_files.delete(@master_file) - media_object.master_files.delete(@master_file) - media_object.save + media_object_id = @master_file.media_object_id @master_file.destroy flash[:notice] = "#{filename} has been deleted from the system" - redirect_to edit_media_object_path(media_object, step: "file-upload") + redirect_to edit_media_object_path(media_object_id, step: "file-upload") end def set_frame diff --git a/app/controllers/media_objects_controller.rb b/app/controllers/media_objects_controller.rb index 0de5e37fbc..1fd28f319a 100644 --- a/app/controllers/media_objects_controller.rb +++ b/app/controllers/media_objects_controller.rb @@ -119,7 +119,7 @@ def add_to_playlist end playlistitem_scope = params[:post][:playlistitem_scope] #'section', 'structure' # If a single masterfile_id wasn't in the request, then create playlist_items for all masterfiles - masterfile_ids = masterfile_id.present? ? [masterfile_id] : @media_object.ordered_master_file_ids + masterfile_ids = masterfile_id.present? ? [masterfile_id] : @media_object.section_ids masterfile_ids.each do |mf_id| mf = SpeedyAF::Proxy::MasterFile.find(mf_id) if playlistitem_scope=='structure' && mf.has_structuralMetadata? && mf.structuralMetadata.xpath('//Span').present? @@ -139,7 +139,7 @@ def add_to_playlist end else #create a single item for the entire masterfile - item_title = @media_object.master_file_ids.count>1? mf.embed_title : @media_object.title + item_title = @media_object.section_ids.count > 1 ? mf.embed_title : @media_object.title clip = AvalonClip.new(title: item_title, master_file: mf) playlist.items += [PlaylistItem.new(clip: clip, playlist: playlist)] end @@ -216,7 +216,7 @@ def update_media_object if !@media_object.save error_messages += ['Failed to create media object:']+@media_object.errors.full_messages elsif master_files_params.respond_to?('each') - old_ordered_master_files = @media_object.ordered_master_files.to_a.collect(&:id) + old_ordered_sections = @media_object.section_ids master_files_params.each_with_index do |file_spec, index| master_file = MasterFile.new(file_spec.except(:structure, :captions, :captions_type, :files, :other_identifier, :label, :date_digitized)) # master_file.media_object = @media_object @@ -230,12 +230,11 @@ def update_media_object master_file.date_digitized = DateTime.parse(file_spec[:date_digitized]).to_time.utc.iso8601 if file_spec[:date_digitized].present? master_file.identifier += Array(params[:files][index][:other_identifier]) master_file.comment += Array(params[:files][index][:comment]) - master_file._media_object = @media_object + master_file.media_object = @media_object if file_spec[:files].present? if master_file.update_derivatives(file_spec[:files], false) master_file.update_stills_from_offset! WaveformJob.perform_later(master_file.id) - @media_object.ordered_master_files += [master_file] else file_location = file_spec.dig(:file_location) || '' message = "Problem saving MasterFile for #{file_location}:" @@ -248,10 +247,14 @@ def update_media_object if error_messages.empty? if api_params[:replace_masterfiles] - old_ordered_master_files.each do |mf| + old_ordered_sections.each do |mf| p = MasterFile.find(mf) + # FIXME: Figure out why this next line is necessary + # without it the save below will fail with Ldp::Gone when attempting to set master_files in the MediaObject before_save callback + # This could be avoided by doing a reload after all of the destroys but I'm afraid that would mess up other changes already staged in-memory. @media_object.master_files.delete(p) - @media_object.ordered_master_files.delete(p) + # Need to manually remove from section_ids in memory to match changes that will persist when the master file is destroyed + @media_object.section_ids -= [p.id] p.destroy end end @@ -284,7 +287,7 @@ def update_media_object def custom_edit if ['preview', 'structure', 'file-upload'].include? @active_step - @masterFiles = load_master_files + @masterFiles = load_sections end if 'preview' == @active_step @@ -364,9 +367,9 @@ def show_progress [encode.master_file_id, mf_status] end ] - master_files_count = @media_object.master_files.size - if master_files_count > 0 - overall.each { |k,v| overall[k] = [0,[100,v.to_f/master_files_count.to_f].min].max.floor } + sections_count = @media_object.sections.size + if sections_count > 0 + overall.each { |k,v| overall[k] = [0,[100,v.to_f/sections_count.to_f].min].max.floor } else overall = {success: 0, error: 0} end @@ -452,7 +455,7 @@ def tree } format.json { result = { @media_object.id => {} } - @media_object.indexed_master_files.each do |mf| + @media_object.sections.each do |mf| result[@media_object.id][mf.id] = mf.derivatives.collect(&:id) end render :json => result @@ -525,11 +528,11 @@ def load_resource def master_file_presenters # Assume that @media_object is a SpeedyAF::Proxy::MediaObject - @media_object.ordered_master_files + @media_object.sections end - def load_master_files(mode = :rw) - @masterFiles ||= mode == :rw ? @media_object.indexed_master_files.to_a : master_file_presenters + def load_sections(mode = :rw) + @masterFiles ||= mode == :rw ? @media_object.sections : master_file_presenters end def set_player_token @@ -554,13 +557,13 @@ def load_player_context if params[:part] index = params[:part].to_i-1 - if index < 0 or index > @media_object.master_files.size-1 + if index < 0 or index > @media_object.sections.size - 1 raise ActiveFedora::ObjectNotFoundError end - params[:content] = @media_object.indexed_master_file_ids[index] + params[:content] = @media_object.section_ids[index] end - load_master_files(mode: :ro) + load_sections(mode: :ro) load_current_stream end @@ -583,7 +586,7 @@ def set_active_file end end if @currentStream.nil? - @currentStream = @media_object.indexed_master_files.first + @currentStream = @media_object.sections.first end return @currentStream end diff --git a/app/helpers/media_objects_helper.rb b/app/helpers/media_objects_helper.rb index 84f370eb1e..e7c3a093c4 100644 --- a/app/helpers/media_objects_helper.rb +++ b/app/helpers/media_objects_helper.rb @@ -195,10 +195,10 @@ def get_duration_from_fragment(start, stop) milliseconds_to_formatted_time((stop.to_i - start.to_i) * 1000, false) end - # This method mirrors the one in the MediaObject model but makes use of the master files passed in which can be SpeedyAF Objects + # This method mirrors the one in the MediaObject model but makes use of the sections passed in which can be SpeedyAF Objects # This would be good to refactor in the future but speeds things up considerably for now - def gather_all_comments(media_object, master_files) - media_object.comment.sort + master_files.collect do |mf| + def gather_all_comments(media_object, sections) + media_object.comment.sort + sections.collect do |mf| mf.comment.reject(&:blank?).collect do |c| mf.display_title.present? ? "[#{mf.display_title}] #{c}" : c end.sort diff --git a/app/helpers/security_helper.rb b/app/helpers/security_helper.rb index 850e33438b..8b6113dbb3 100644 --- a/app/helpers/security_helper.rb +++ b/app/helpers/security_helper.rb @@ -30,20 +30,20 @@ def secure_streams(stream_info, media_object_id) # media_object CDL check only happens once # session tokens retrieved in batch then passed into add_stream_url # Returns Hash[MasterFile.id, stream_info] - def secure_stream_infos(master_files, media_objects) + def secure_stream_infos(sections, media_objects) stream_info_hash = {} not_checked_out_hash = {} - mo_ids = master_files.collect(&:media_object_id) + mo_ids = sections.collect(&:media_object_id) mo_ids.each { |mo_id| not_checked_out_hash[mo_id] ||= not_checked_out?(mo_id, media_object: media_objects.find {|mo| mo.id == mo_id}) } - not_checked_out_master_files = master_files.select { |mf| not_checked_out_hash[mf.media_object_id] } - checked_out_master_files = master_files - not_checked_out_master_files + not_checked_out_sections = sections.select { |mf| not_checked_out_hash[mf.media_object_id] } + checked_out_sections = sections - not_checked_out_sections - not_checked_out_master_files.each { |mf| stream_info_hash[mf.id] = mf.stream_details } + not_checked_out_sections.each { |mf| stream_info_hash[mf.id] = mf.stream_details } - stream_tokens = StreamToken.get_session_tokens_for(session: session, targets: checked_out_master_files.map(&:id)) + stream_tokens = StreamToken.get_session_tokens_for(session: session, targets: checked_out_sections.map(&:id)) stream_token_hash = stream_tokens.pluck(:target, :token).to_h - checked_out_master_files.each { |mf| stream_info_hash[mf.id] = secure_stream_info(mf.stream_details, stream_token_hash[mf.id]) } + checked_out_sections.each { |mf| stream_info_hash[mf.id] = secure_stream_info(mf.stream_details, stream_token_hash[mf.id]) } stream_info_hash end diff --git a/app/javascript/components/MediaObjectRamp.jsx b/app/javascript/components/MediaObjectRamp.jsx index 047ae20f8b..f8476ef071 100644 --- a/app/javascript/components/MediaObjectRamp.jsx +++ b/app/javascript/components/MediaObjectRamp.jsx @@ -40,7 +40,7 @@ const ExpandCollapseArrow = () => { const Ramp = ({ urls, - master_files_count, + sections_count, has_structure, title, share, @@ -153,7 +153,7 @@ const Ramp = ({ ) : ( - {master_files_count > 0 && + {sections_count > 0 &&
@@ -217,7 +217,7 @@ const Ramp = ({ ref={expandCollapseBtnRef} > - {isClosed ? ' Expand' : ' Close'} {master_files_count > 1 ? `${master_files_count} Sections` : 'Section'} + {isClosed ? ' Expand' : ' Close'} {sections_count > 1 ? `${sections_count} Sections` : 'Section'} } @@ -244,13 +244,13 @@ const Ramp = ({ ) } - + {cdl.enabled &&
} - {(cdl.can_stream && master_files_count != 0 && has_transcripts) && + {(cdl.can_stream && sections_count != 0 && has_transcripts) && %(avr-media_object:).freeze, type: "rdfs:Class".freeze property :supplementalFiles, "rdfs:isDefinedBy" => %(avr-media_object:).freeze, type: "rdfs:Class".freeze property :avalon_uploader, "rdfs:isDefinedBy" => %(avr-media_object:).freeze, type: "rdfs:Class".freeze + property :section_list, "rdfs:isDefinedBy" => %(avr-media_object:).freeze, type: "rdfs:Class".freeze end class Collection < RDF::StrictVocabulary("http://avalonmediasystem.org/rdf/vocab/collection#") diff --git a/app/models/batch_entries.rb b/app/models/batch_entries.rb index 6c29e7fe00..8b5ea83bf3 100644 --- a/app/models/batch_entries.rb +++ b/app/models/batch_entries.rb @@ -73,13 +73,13 @@ def encoding_status return (@encoding_status = :error) unless media_object # TODO: match file_locations strings with those in MasterFiles? - if media_object.master_files.to_a.count != files.count + if media_object.sections.count != files.count return (@encoding_status = :error) end # Only return success if all MasterFiles have status 'COMPLETED' status = :success - media_object.master_files.each do |master_file| + media_object.sections.each do |master_file| next if master_file.status_code == 'COMPLETED' # TODO: explore border cases if master_file.status_code == 'FAILED' || master_file.status_code == 'CANCELLED' diff --git a/app/models/concerns/master_file_behavior.rb b/app/models/concerns/master_file_behavior.rb index e4420c7b09..d878514d6c 100644 --- a/app/models/concerns/master_file_behavior.rb +++ b/app/models/concerns/master_file_behavior.rb @@ -108,8 +108,7 @@ def display_title structuralMetadata.section_title elsif title.present? title - # FIXME: The test for media_object.master_file_ids.size is expensive and takes ~0.25 seconds - elsif file_location.present? && (media_object.master_file_ids.size > 1) + elsif file_location.present? && (media_object.section_ids.size > 1) file_location.split("/").last end mf_title.blank? ? nil : mf_title diff --git a/app/models/concerns/media_object_behavior.rb b/app/models/concerns/media_object_behavior.rb index bc7084d779..13994eec75 100644 --- a/app/models/concerns/media_object_behavior.rb +++ b/app/models/concerns/media_object_behavior.rb @@ -14,6 +14,7 @@ # This module contains methods which transform stored values for use either on the MediaObject or the SpeedyAF presenter module MediaObjectBehavior + def as_json(options={}) { id: id, @@ -58,11 +59,11 @@ def access_text end def has_captions - master_files.any? { |mf| mf.has_captions? } + sections.any? { |mf| mf.has_captions? } end def has_transcripts - master_files.any? { |mf| mf.has_transcripts? } + sections.any? { |mf| mf.has_transcripts? } end # CDL methods diff --git a/app/models/concerns/media_object_intercom.rb b/app/models/concerns/media_object_intercom.rb index 34fffff7d6..69e47fea15 100644 --- a/app/models/concerns/media_object_intercom.rb +++ b/app/models/concerns/media_object_intercom.rb @@ -15,7 +15,7 @@ module MediaObjectIntercom def to_ingest_api_hash(include_structure = true, remove_identifiers: false, publish: false) { - files: ordered_master_files.to_a.collect { |mf| mf.to_ingest_api_hash(include_structure, remove_identifiers: remove_identifiers) }, + files: sections.collect { |mf| mf.to_ingest_api_hash(include_structure, remove_identifiers: remove_identifiers) }, fields: { duration: duration, diff --git a/app/models/file_upload_step.rb b/app/models/file_upload_step.rb index a8218691d2..a9ce47269c 100644 --- a/app/models/file_upload_step.rb +++ b/app/models/file_upload_step.rb @@ -36,15 +36,11 @@ def after_step context end def execute context - deleted_master_files = update_master_files context - context[:notice] = "Several clean up jobs have been sent out. Their statuses can be viewed by your sysadmin at #{ Settings.matterhorn.cleanup_log }" unless deleted_master_files.empty? + deleted_sections = update_master_files context + context[:notice] = "Several clean up jobs have been sent out. Their statuses can be viewed by your sysadmin at #{ Settings.matterhorn.cleanup_log }" unless deleted_sections.empty? - # Reloads media_object.master_files, should use .reload when we update hydra-head media = MediaObject.find(context[:media_object].id) - unless media.master_files.empty? - media.save - context[:media_object] = media - end + context[:media_object] = media context end @@ -56,16 +52,15 @@ def execute context # label - Display label in the interface # id - Identifier for the masterFile to help with mapping def update_master_files(context) - media_object = context[:media_object] files = context[:master_files] || {} - deleted_master_files = [] + deleted_sections = [] if not files.blank? files.each_pair do |id,master_file| selected_master_file = MasterFile.find(id) if selected_master_file if master_file[:remove] - deleted_master_files << selected_master_file + deleted_sections << selected_master_file selected_master_file.destroy else selected_master_file.title = master_file[:title] unless master_file[:title].nil? @@ -80,6 +75,6 @@ def update_master_files(context) end end end - deleted_master_files + deleted_sections end end diff --git a/app/models/iiif_manifest_presenter.rb b/app/models/iiif_manifest_presenter.rb index 6933fc6903..daa78fe450 100644 --- a/app/models/iiif_manifest_presenter.rb +++ b/app/models/iiif_manifest_presenter.rb @@ -20,16 +20,16 @@ class IiifManifestPresenter IIIF_ALLOWED_TAGS = ['a', 'b', 'br', 'i', 'img', 'p', 'small', 'span', 'sub', 'sup'].freeze IIIF_ALLOWED_ATTRIBUTES = ['href', 'src', 'alt'].freeze - attr_reader :media_object, :master_files, :lending_enabled + attr_reader :media_object, :sections, :lending_enabled def initialize(media_object:, master_files:, lending_enabled: false) @media_object = media_object - @master_files = master_files + @sections = master_files @lending_enabled = lending_enabled end def file_set_presenters - master_files + sections end def work_presenters @@ -202,7 +202,7 @@ def iiif_metadata_fields end def thumbnail_url - master_file_id = media_object.ordered_master_file_ids.try :first + master_file_id = media_object.section_ids.try :first video_count = media_object.avalon_resource_type.map(&:titleize)&.count { |m| m.start_with?('moving image'.titleize) } || 0 audio_count = media_object.avalon_resource_type.map(&:titleize)&.count { |m| m.start_with?('sound recording'.titleize) } || 0 diff --git a/app/models/master_file.rb b/app/models/master_file.rb index 1a6d4e1240..de171d4a33 100644 --- a/app/models/master_file.rb +++ b/app/models/master_file.rb @@ -239,17 +239,23 @@ def set_workflow( workflow = nil ) # This requires the MasterFile having an actual id def media_object=(mo) + self.save!(validate: false) unless self.persisted? + # Removes existing association if self.media_object.present? - self.media_object.master_files = self.media_object.master_files.to_a.reject { |mf| mf.id == self.id } - self.media_object.ordered_master_files = self.media_object.ordered_master_files.to_a.reject { |mf| mf.id == self.id } - self.media_object.save + self.media_object.section_ids -= [self.id] + self.media_object.save(validate: false) end self._media_object=(mo) + self.save!(validate: false) + unless self.media_object.nil? - self.media_object.ordered_master_files += [self] - self.media_object.save + self.media_object.section_ids += [self.id] + self.media_object.save(validate: false) + # Need to reload here because somehow a cached copy of media_object is saved in memory + # which lacks the updated section_ids and master_file_ids just set and persisted above + self.media_object.reload end end @@ -546,6 +552,11 @@ def self.calculate_working_file_path(old_path) end end + def stop_processing! + # Stops all processing + ActiveEncodeJobs::CancelEncodeJob.perform_later(workflow_id, id) if workflow_id.present? && !finished_processing? + end + protected def mediainfo @@ -766,15 +777,9 @@ def find_encoder_class(klass_name) klass if klass&.ancestors&.include?(ActiveEncode::Base) end - def stop_processing! - # Stops all processing - ActiveEncodeJobs::CancelEncodeJob.perform_later(workflow_id, id) if workflow_id.present? && finished_processing? - end - def update_parent! return unless media_object.present? - media_object.master_files.delete(self) - media_object.ordered_master_files.delete(self) + media_object.section_ids -= [self.id] media_object.set_media_types! media_object.set_duration! if !media_object.save diff --git a/app/models/media_object.rb b/app/models/media_object.rb index 368bb39d1e..c42b9f4a2d 100644 --- a/app/models/media_object.rb +++ b/app/models/media_object.rb @@ -36,6 +36,25 @@ 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 + + after_find do + # Force loading of section_ids from list_source + self.section_ids if self.section_list.nil? + end + + # Persist to master_files only on save to avoid changes to master_files auto-saving and making things out of sync + # This might be able to be removed along with the ordered_aggregation to rely purely on section_list and the relationship + # on the MasterFile model + # Have to handle create specially otherwise will attempt to associate prior to having an id + around_create do |_, block| + block.call + # Saving again will force running through the before_save callback that should do the actual work + self.save!(validate: false) unless self.master_file_ids.sort == self.section_ids.sort + end + before_save do + self.master_files = self.sections unless self.new_record? || self.master_file_ids.sort == self.section_ids.sort + end + after_save :update_dependent_permalinks_job, if: Proc.new { |mo| mo.persisted? && mo.published? } after_save :remove_bookmarks after_update_index :enqueue_long_indexing @@ -117,6 +136,8 @@ def validate_date(date_field) index.as :stored_sortable end + #TODO: get rid of all ordered_* and indexed_* references, after everything is migrated then convert from `ordered_aggregation` to `has_many` + # OR possibly remove the master_files relationship entirely? 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 # and methods for master_files: first, last, [index], =, <<, +=, delete(mf) @@ -125,10 +146,40 @@ def validate_date(date_field) accepts_nested_attributes_for :master_files, :allow_destroy => true + property :section_list, predicate: Avalon::RDFVocab::MediaObject.section_list, multiple: false do |index| + index.as :symbol + end + + def section_ids= ids + self.section_list = ids.to_json + @sections = nil + @section_ids = ids + end + + def sections= mfs + self.section_ids = mfs.map(&:id) + @sections = mfs + end + + def sections + @sections ||= MasterFile.find(self.section_ids) + end + + def section_ids + return @section_ids if @section_ids + + # Do migration + self.section_ids = self.ordered_master_file_ids if self.section_list.nil? + + return [] if self.section_list.nil? + @section_ids = JSON.parse(self.section_list) + end + def destroy # attempt to stop the matterhorn processing job - self.master_files.each(&:destroy) - self.master_files.clear + self.sections.each(&:stop_processing!) + # avoid calling destroy on each section since it calls save on parent media object + self.sections.each(&:delete) Bookmark.where(document_id: self.id).destroy_all super end @@ -163,7 +214,7 @@ def publish!(user_key, validate: true) end def finished_processing? - self.master_files.all?{ |master_file| master_file.finished_processing? } + self.sections.all? { |section| section.finished_processing? } end def set_duration! @@ -179,42 +230,42 @@ def report_missing_attributes end def set_media_types! - mime_types = master_file_solr_docs.reject { |mf| mf["file_location_ssi"].blank? }.collect do |mf| - Rack::Mime.mime_type(File.extname(mf["file_location_ssi"])) + mime_types = section_solr_docs.reject { |section| section["file_location_ssi"].blank? }.collect do |section| + Rack::Mime.mime_type(File.extname(section["file_location_ssi"])) end.uniq self.format = mime_types.empty? ? nil : mime_types end def set_resource_types! - self.avalon_resource_type = master_file_solr_docs.reject { |mf| mf["file_format_ssi"].blank? }.collect do |mf| - case mf["file_format_ssi"] + self.avalon_resource_type = section_solr_docs.reject { |section| section["file_format_ssi"].blank? }.collect do |section| + case section["file_format_ssi"] when 'Moving image' 'moving image' when 'Sound' 'sound recording' else - mf.file_format.downcase + section.file_format.downcase end end.uniq end def update_dependent_properties! - @master_file_docs = nil + @section_docs = nil self.set_duration! self.set_media_types! self.set_resource_types! end def all_comments - comment.sort + ordered_master_files.to_a.compact.collect do |mf| - mf.comment.reject(&:blank?).collect do |c| - mf.display_title.present? ? "[#{mf.display_title}] #{c}" : c + comment.sort + sections.compact.collect do |section| + section.comment.reject(&:blank?).collect do |c| + section.display_title.present? ? "[#{section.display_title}] #{c}" : c end.sort end.flatten.uniq end def section_labels - all_labels = master_files.collect{|mf|mf.structural_metadata_labels << mf.title} + all_labels = sections.collect{ |section| section.structural_metadata_labels << section.title} all_labels.flatten.uniq.compact end @@ -222,8 +273,8 @@ def section_labels # @return [Array] A unique list of all physical descriptions for the media object def section_physical_descriptions all_pds = [] - self.master_files.each do |master_file| - all_pds += Array(master_file.physical_description) unless master_file.physical_description.nil? + self.sections.each do |section| + all_pds += Array(section.physical_description) unless section.physical_description.nil? end all_pds.uniq end @@ -232,10 +283,9 @@ def section_physical_descriptions # using a copy of the master file solr doc to avoid having to fetch them all from fedora # this is probably okay since this is just aggregating the values already in the master file solr docs - def fill_in_solr_fields_that_need_master_files(solr_doc) - solr_doc['section_id_ssim'] = ordered_master_file_ids - solr_doc["other_identifier_sim"] += master_files.collect {|mf| mf.identifier.to_a }.flatten - solr_doc["date_digitized_ssim"] = master_files.collect {|mf| mf.date_digitized }.compact.map {|t| Time.parse(t).strftime "%F" } + def fill_in_solr_fields_that_need_sections(solr_doc) + solr_doc["other_identifier_sim"] += sections.collect {|section| section.identifier.to_a }.flatten + solr_doc["date_digitized_ssim"] = sections.collect {|section| section.date_digitized }.compact.map {|t| Time.parse(t).strftime "%F" } solr_doc["has_captions_bsi"] = has_captions solr_doc["has_transcripts_bsi"] = has_transcripts solr_doc["section_label_tesim"] = section_labels @@ -265,8 +315,9 @@ def to_solr(include_child_fields: false) solr_doc['note_ssm'] = self.note.collect { |n| n.to_json } solr_doc['other_identifier_ssm'] = self.other_identifier.collect { |oi| oi.to_json } solr_doc['related_item_url_ssm'] = self.related_item_url.collect { |r| r.to_json } + solr_doc['section_id_ssim'] = section_ids if include_child_fields - fill_in_solr_fields_that_need_master_files(solr_doc) + fill_in_solr_fields_that_need_sections(solr_doc) elsif id.present? # avoid error in test suite # Fill in other identifier so these values aren't stripped from the solr doc while waiting for the background job mf_docs = ActiveFedora::SolrService.query("isPartOf_ssim:#{id}", rows: 1_000_000) @@ -317,10 +368,10 @@ def update_dependent_permalinks_job end def update_dependent_permalinks - self.master_files.each do |master_file| + self.sections.each do |section| begin - updated = master_file.ensure_permalink! - master_file.save( validate: false ) if updated + updated = section.ensure_permalink! + section.save( validate: false ) if updated rescue # no-op # Save is called (uncharacteristically) during a destroy. @@ -349,7 +400,7 @@ def merge!(media_objects) media_objects.dup.each do |mo| begin # TODO: mass assignment may speed things up - mo.ordered_master_files.to_a.dup.each { |mf| mf.media_object = self } + mo.sections.each { |section| section.media_object = self } mo.reload.destroy! mergeds << mo @@ -368,7 +419,9 @@ def lending_period # Override to reset memoized fields def reload - @master_file_docs = nil + @section_docs = nil + @sections = nil + @section_ids = nil super end @@ -401,12 +454,17 @@ def self.autocomplete(query, id) private - def master_file_solr_docs - @master_file_docs ||= ActiveFedora::SolrService.query("isPartOf_ssim:#{id}", rows: 1_000_000) + def section_solr_docs + # Explicitly query for each id in section_ids instead of the reverse to ensure consistency + # This may skip master file objects which claim to be a part of this media object but are not + # in the section_list + return [] unless section_ids.present? + query = "id:" + section_ids.join(" id:") + @section_docs ||= ActiveFedora::SolrService.query(query, rows: 1_000_000) end def calculate_duration - master_file_solr_docs.collect { |h| h['duration_ssi'].to_i }.compact.sum + section_solr_docs.collect { |h| h['duration_ssi'].to_i }.compact.sum end def collect_ips_for_index ip_strings @@ -418,6 +476,7 @@ def collect_ips_for_index ip_strings end def sections_with_files(tag: '*') - ordered_master_file_ids.select { |m| SpeedyAF::Proxy::MasterFile.find(m).supplemental_files(tag: tag).present? } + # TODO: Optimize this into a single solr query? + section_ids.select { |m| SpeedyAF::Proxy::MasterFile.find(m).supplemental_files(tag: tag).present? } end end diff --git a/app/models/structure_step.rb b/app/models/structure_step.rb index ee7b2507f2..aa35b0935f 100644 --- a/app/models/structure_step.rb +++ b/app/models/structure_step.rb @@ -20,12 +20,7 @@ def initialize(step = 'structure', title = "Structure", summary = "Organization def execute context media_object = context[:media_object] if ! context[:master_file_ids].nil? - # gather the parts in the right order - # in this situation we cannot use MatterFile.find([]) because - # it will not return the results in the correct order - master_files = context[:master_file_ids].map{ |master_file_id| MasterFile.find(master_file_id) } - # re-add the parts that are now in the right order - media_object.ordered_master_files = master_files + media_object.section_ids = context[:master_file_ids] media_object.save end context diff --git a/app/presenters/speedy_af/proxy/master_file.rb b/app/presenters/speedy_af/proxy/master_file.rb index 113be17358..1584b2d9e5 100644 --- a/app/presenters/speedy_af/proxy/master_file.rb +++ b/app/presenters/speedy_af/proxy/master_file.rb @@ -40,8 +40,7 @@ def display_title structuralMetadata.section_title elsif title.present? title - # FIXME: The test for media_object.master_file_ids.size is expensive and takes ~0.25 seconds - elsif file_location.present? && (media_object.master_file_ids.size > 1) + elsif file_location.present? && (media_object.section_ids.size > 1) file_location.split("/").last end mf_title.blank? ? nil : mf_title diff --git a/app/presenters/speedy_af/proxy/media_object.rb b/app/presenters/speedy_af/proxy/media_object.rb index 7dafe8855f..cf84fe2fce 100644 --- a/app/presenters/speedy_af/proxy/media_object.rb +++ b/app/presenters/speedy_af/proxy/media_object.rb @@ -27,6 +27,7 @@ def initialize(solr_document, instance_defaults = {}) end # Handle this case here until a better fix can be found for multiple solr fields which don't have a model property @attrs[:section_id] = solr_document["section_id_ssim"] + @attrs[:section_ids] = solr_document["section_id_ssim"] @attrs[:hidden?] = solr_document["hidden_bsi"] @attrs[:read_groups] = solr_document["read_access_group_ssim"] || [] @attrs[:edit_groups] = solr_document["edit_access_group_ssim"] || [] @@ -79,30 +80,13 @@ def supplemental_files(tag: '*') end end - def master_file_ids - if real? - real_object.indexed_master_file_ids - elsif section_id.nil? # No master files or not indexed yet - ActiveFedora::Base.logger.warn("Reifying MediaObject because master_files not indexed") - real_object.indexed_master_file_ids - else - section_id - end - end - alias_method :indexed_master_file_ids, :master_file_ids - alias_method :ordered_master_file_ids, :master_file_ids - - def master_files - # NOTE: Defaults are set on returned SpeedyAF::Base objects if field isn't present in the solr doc. - # This is important otherwise speedy_af will reify from fedora when trying to access this field. - # When adding a new property to the master file model that will be used in the interface, - # add it to the default below to avoid reifying for master files lacking a value for the property. - @master_files ||= SpeedyAF::Proxy::MasterFile.where("isPartOf_ssim:#{id}", - order: -> { master_file_ids }, - load_reflections: true) + def sections + return [] unless section_ids.present? + query = "id:" + section_ids.join(" id:") + @sections ||= SpeedyAF::Proxy::MasterFile.where(query, + order: -> { section_ids }, + load_reflections: true) end - alias_method :indexed_master_files, :master_files - alias_method :ordered_master_files, :master_files def collection @collection ||= SpeedyAF::Proxy::Admin::Collection.find(collection_id) @@ -114,7 +98,7 @@ def lending_period def format # TODO figure out how to memoize this - mime_types = master_files.reject { |mf| mf.file_location.blank? }.collect do |mf| + mime_types = sections.reject { |mf| mf.file_location.blank? }.collect do |mf| Rack::Mime.mime_type(File.extname(mf.file_location)) end.uniq mime_types.empty? ? nil : mime_types @@ -149,7 +133,7 @@ def language end def sections_with_files(tag: '*') - master_files.select { |master_file| master_file.supplemental_files(tag: tag).present? }.map(&:id) + sections.select { |master_file| master_file.supplemental_files(tag: tag).present? }.map(&:id) end def permalink_with_query(query_vars = {}) diff --git a/app/views/ingest_batch_mailer/status_email.html.erb b/app/views/ingest_batch_mailer/status_email.html.erb index 373d525fd3..03d8c2cf2b 100644 --- a/app/views/ingest_batch_mailer/status_email.html.erb +++ b/app/views/ingest_batch_mailer/status_email.html.erb @@ -20,8 +20,8 @@ Unless required by applicable law or agreed to in writing, software distributed

<%= media_object.title %>

- <% master_files = media_object.master_files.to_a %> - <% if master_files.count > 0 %> + <% sections = media_object.sections %> + <% if sections.count > 0 %> @@ -29,7 +29,7 @@ Unless required by applicable law or agreed to in writing, software distributed - <% master_files.each do |master_file| %> + <% sections.each do |master_file| %> <% status_code = master_file.status_code.downcase %> - + {playlist_item_ids?.length > 0 && ( diff --git a/app/javascript/components/embeds/EmbeddedRamp.jsx b/app/javascript/components/embeds/EmbeddedRamp.jsx index 7846a0d998..99918b4881 100644 --- a/app/javascript/components/embeds/EmbeddedRamp.jsx +++ b/app/javascript/components/embeds/EmbeddedRamp.jsx @@ -111,9 +111,9 @@ const Ramp = ({ customErrorMessage='This embed encountered an error. Please refresh or contact an administrator.' startCanvasId={startCanvasId} startCanvasTime={startCanvasTime}> - + ); }; -export default Ramp; \ No newline at end of file +export default Ramp; From e36fef88521aa5863b545b57a4398acad45a40d3 Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Wed, 12 Jun 2024 14:37:50 -0400 Subject: [PATCH 081/152] Fix stretched posters --- app/models/master_file.rb | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/app/models/master_file.rb b/app/models/master_file.rb index de171d4a33..3c69ee3cc4 100644 --- a/app/models/master_file.rb +++ b/app/models/master_file.rb @@ -171,6 +171,7 @@ def error after_transcoding :generate_waveform after_transcoding :update_ingest_batch # Generate and set the poster and thumbnail + after_transcoding :set_display_aspect_ratio after_transcoding :set_default_poster_offset after_transcoding :update_stills_from_offset! @@ -605,9 +606,11 @@ def extract_frame(options={}) end frame_size = (options[:size].nil? or options[:size] == 'auto') ? self.original_frame_size : options[:size] + # Handle existing files that may have inaccurate display_aspect_ratio saved. + aspect_ratio = actual_aspect_ratio != self.display_aspect_ratio.to_f ? actual_aspect_ratio : self.display_aspect_ratio (new_width,new_height) = frame_size.split(/x/).collect(&:to_f) - new_height = (new_width/self.display_aspect_ratio.to_f).round + new_height = (new_width/aspect_ratio.to_f).round frame_source = find_frame_source(offset: offset) data = get_ffmpeg_frame_data(frame_source, new_width, new_height, options[:headers]) raise RuntimeError, "Frame extraction failed. See log for details." if data.empty? @@ -747,6 +750,23 @@ def reloadTechnicalMetadata! end end + def actual_aspect_ratio + deriv = self.derivatives.where(quality_ssi: "high").first + + deriv.width.to_f / deriv.height.to_f + end + + # Input videos that are in portrait orientation can have metadata showing landscape orientation + # with a rotation value. This can cause the aspect ratio on the master file to be incorrect. + # We can get the proper aspect ratio from the transcoded files, so set the master file off the + # derivatives when the aspect ratios do not match. + def set_display_aspect_ratio + if self.display_aspect_ratio.to_f != actual_aspect_ratio + self.display_aspect_ratio = actual_aspect_ratio.to_s + self.save + end + end + # This should only be getting called by the :after_transcode hook. Ensure that # poster_offset is only set if it has not already been manually set via # BatchEntry or another method. From 7b8eec6659cecd01ef4966665f7def6e4983560f Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Thu, 13 Jun 2024 16:07:54 -0400 Subject: [PATCH 082/152] Make playlist description collapsible --- app/javascript/components/PlaylistRamp.jsx | 63 ++++++++++++++++++- .../playlists/_description_and_tags.html.erb | 28 --------- app/views/playlists/show.html.erb | 9 ++- 3 files changed, 69 insertions(+), 31 deletions(-) delete mode 100644 app/views/playlists/_description_and_tags.html.erb diff --git a/app/javascript/components/PlaylistRamp.jsx b/app/javascript/components/PlaylistRamp.jsx index 722a88d9c3..c069ad013d 100644 --- a/app/javascript/components/PlaylistRamp.jsx +++ b/app/javascript/components/PlaylistRamp.jsx @@ -44,14 +44,19 @@ const Ramp = ({ playlist_item_ids, token, share, - comment_tag + comment_label, + comment, + tags }) => { const [manifestUrl, setManifestUrl] = React.useState(''); const [activeItemTitle, setActiveItemTitle] = React.useState(); const [activeItemSummary, setActiveItemSummary] = React.useState(); const [startCanvasId, setStartCanvasId] = React.useState(); + const [expanded, setExpanded] = React.useState(false); + const [description, setDescription] = React.useState(); let interval; + let descriptionCheck; const USER_AGENT = window.navigator.userAgent; const IS_MOBILE = (/Mobi/i).test(USER_AGENT); @@ -71,9 +76,15 @@ const Ramp = ({ setManifestUrl(url); interval = setInterval(addPlayerEventListeners, 500); + /** + * The passed in description is not immediately available for some reason. + * Use an interval to wait and set initial description. + */ + descriptionCheck = setInterval(prepInitialDescription, 100); // Clear interval upon component unmounting return () => clearInterval(interval); + return () => clearInterval(descriptionCheck); }, []); /** @@ -94,6 +105,34 @@ const Ramp = ({ } }; + const expandBtn = { + paddingLeft: '2px', + cursor: 'pointer' + }; + + const wordCount = 32; + const words = comment ? comment.split(' ') : []; + + function prepInitialDescription() { + if (words !== undefined && words.length > 0) { + clearInterval(descriptionCheck); + let desc = words.length > wordCount + ? `${words.slice(0, wordCount).join(' ')}...` + : words.join(' '); + + setDescription(desc); + } else if (words.length === 0) { + clearInterval(descriptionCheck); + } + } + + const handleClick = () => { + setDescription( + expanded ? `${words.slice(0, wordCount).join(' ')}...` : words.join(' ') + ); + setExpanded(!expanded); + }; + return ( -
+
+ {comment && ( +
+

{comment_label}

+
+ + {words.length > wordCount && ( + + Show {expanded ? 'less' : 'more'} + + )} +
+
+ )} + {tags && ( +
+

Tags

+
+
+ )} +
{playlist_item_ids?.length > 0 && (

Playlist Items

diff --git a/app/views/playlists/_description_and_tags.html.erb b/app/views/playlists/_description_and_tags.html.erb deleted file mode 100644 index 80254767d5..0000000000 --- a/app/views/playlists/_description_and_tags.html.erb +++ /dev/null @@ -1,28 +0,0 @@ -<%# -Copyright 2011-2024, 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 @playlist.comment.present? %> -

<%= t("activerecord.attributes.playlist.comment") %>

-<%= simple_format @playlist.comment %> -<% end %> - -<% if @playlist.tags.present? %> -

Tags

-
- <% @playlist.tags.each do |tag| %> - <%=tag%> - <% end %> -
-<% end %> diff --git a/app/views/playlists/show.html.erb b/app/views/playlists/show.html.erb index 80dbac5fec..a07da5df5b 100644 --- a/app/views/playlists/show.html.erb +++ b/app/views/playlists/show.html.erb @@ -28,6 +28,11 @@ Unless required by applicable law or agreed to in writing, software distributed
+ <% # Replace 2 or more consecutive sets of line break characters with
tags %> + <% # This is because

tags force the show more link to a new line since it %> + <% # is outside the element(s) containing the text. Avoiding

allows the link to be inline %> + <% comment = @playlist.comment.gsub(/(\r?\n){2,}/m, "

") %> + <% tags = @playlist.tags.map { |tag| "#{tag}" }.join(' ') %> <%= react_component("PlaylistRamp", { urls: { base_url: request.protocol+request.host_with_port, fullpath_url: request.fullpath }, @@ -35,7 +40,9 @@ Unless required by applicable law or agreed to in writing, software distributed playlist_item_ids: @playlist.item_ids, token: @playlist_token, share: { canShare: (will_partial_list_render? :share), content: render('share') }, - comment_tag: { content: render('description_and_tags') } + comment_label: t("activerecord.attributes.playlist.comment"), + comment: comment, + tags: tags } ) %>

From 9d7592281c5fbd4be2780a79cc935b00533bf5e2 Mon Sep 17 00:00:00 2001 From: Mason Ballengee <68433277+masaball@users.noreply.github.com> Date: Fri, 14 Jun 2024 12:20:26 -0400 Subject: [PATCH 083/152] Use more specific function name Co-authored-by: Chris Colvard --- app/javascript/components/PlaylistRamp.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/javascript/components/PlaylistRamp.jsx b/app/javascript/components/PlaylistRamp.jsx index c069ad013d..6ad73df35c 100644 --- a/app/javascript/components/PlaylistRamp.jsx +++ b/app/javascript/components/PlaylistRamp.jsx @@ -126,7 +126,7 @@ const Ramp = ({ } } - const handleClick = () => { + const handleDescriptionMoreLessClick = () => { setDescription( expanded ? `${words.slice(0, wordCount).join(' ')}...` : words.join(' ') ); @@ -210,7 +210,7 @@ const Ramp = ({
{words.length > wordCount && ( - + Show {expanded ? 'less' : 'more'} )} From 5322a9973a65a3cdf2641fbb3933832f0a89f8da Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Fri, 14 Jun 2024 14:43:21 -0400 Subject: [PATCH 084/152] Add parent_id backfill rake migration --- lib/tasks/avalon_migrations.rake | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/lib/tasks/avalon_migrations.rake b/lib/tasks/avalon_migrations.rake index d0c050574c..b234370f1b 100644 --- a/lib/tasks/avalon_migrations.rake +++ b/lib/tasks/avalon_migrations.rake @@ -64,5 +64,34 @@ namespace :avalon do puts("#{error.length} files migrated with unsupported mime types. Refer to caption_type_errors.log for full list of SupplementalFile IDs.") end end + desc "Backfill the parent_id field on SupplementalFile records as part of improvements to the model to allow transcript indexing" + task backfill_parent_id: :environment do + error = [] + logger = Logger.new(File.join(Rails.root, 'log/parent_id_backfill_errors.log')) + + objects = ActiveFedora::SolrService.get("supplemental_files_json_ssi:*", { fl: "id,supplemental_files_json_ssi", rows: 1000000 })["response"]["docs"] + objects.each do |object| + files = JSON.parse(object["supplemental_files_json_ssi"]).collect { |file_gid| GlobalID::Locator.locate(file_gid) } + + files.each do |file| + begin + if file.parent_id.blank? + file.parent_id = object["id"] + file.save! + else + next + end + rescue + error += [file.id] + end + end + end + + puts("Backfill complete.") + if error.present? + logger.info("Failed to save: #{error}") + puts("#{error.length} files failed to save. Refer to parent_id_backfill_errors.log for full list of SupplementalFile IDs.") + end + end end end From 25de4cca7a5f9790422cbc5c5daa8513d88743b4 Mon Sep 17 00:00:00 2001 From: cjcolvar Date: Fri, 14 Jun 2024 14:15:27 -0400 Subject: [PATCH 085/152] Migrate remaining media objects to section_list array property --- lib/tasks/avalon_migrations.rake | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/lib/tasks/avalon_migrations.rake b/lib/tasks/avalon_migrations.rake index b234370f1b..e188e633da 100644 --- a/lib/tasks/avalon_migrations.rake +++ b/lib/tasks/avalon_migrations.rake @@ -93,5 +93,26 @@ namespace :avalon do puts("#{error.length} files failed to save. Refer to parent_id_backfill_errors.log for full list of SupplementalFile IDs.") end end + desc "Migrate MediaObjects from list_source linked list of sections to JSON array section_list property" + task media_object_section_list: :environment do + error_ids = [] + mo_count = MediaObject.count + ids_to_migrate = ActiveFedora::SolrService.query("has_model_ssim:MediaObject AND NOT section_list_ssim:[* TO *]", rows: mo_count).pluck("id") + puts "Migrating #{ids_to_migrate.size} out of #{mo_count} Media Objects." + ids_to_migrate.each do |id| + MediaObject.find(id).save! + rescue StandardError => error + error_ids += [id] + puts "Error migrating #{id}: #{error.message}" + end + remaining_ids_count = ActiveFedora::SolrService.query("has_model_ssim:MediaObject AND NOT section_list_ssim:[* TO *]", rows: mo_count).size + if error_ids.size > 0 + puts "Migration finished running but #{error_ids.size} Media Objects failed to migrate. Try running the migration again." + elsif remaining_ids_count > 0 + puts "Migration finished running but #{remaining_ids_count} Media Objects found still needing to be migrated." + else + puts "Migration completed successfully." + end + end end end From 5ff367012be958ee284ffa769158b05134235777 Mon Sep 17 00:00:00 2001 From: dwithana Date: Mon, 17 Jun 2024 10:46:31 -0400 Subject: [PATCH 086/152] New Ramp build with CSS changes --- yarn.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/yarn.lock b/yarn.lock index 308b057ed7..8bbc7f0bc8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1479,7 +1479,7 @@ "@samvera/ramp@https://github.com/samvera-labs/ramp.git": version "3.1.2" - resolved "https://github.com/samvera-labs/ramp.git#836f27f2225fbb4406b412603a1ca124d4563430" + resolved "https://github.com/samvera-labs/ramp.git#6ba3a206ac7bbda3682d70c6bdb53c709c36b29f" dependencies: "@rollup/plugin-json" "^6.0.1" "@silvermine/videojs-quality-selector" "^1.3.1" @@ -4629,9 +4629,9 @@ make-dir@^3.0.2, make-dir@^3.1.0: semver "^6.0.0" mammoth@^1.4.19: - version "1.7.2" - resolved "https://registry.yarnpkg.com/mammoth/-/mammoth-1.7.2.tgz#e0efd28f46e183d807230e9ce119966dc6b1215e" - integrity sha512-MqWU2hcLf1I5QMKyAbfJCvrLxnv5WztrAQyorfZ+WPq7Hk82vZFmvfR2/64ajIPpM4jlq0TXp1xZvp/FFaL1Ug== + version "1.8.0" + resolved "https://registry.yarnpkg.com/mammoth/-/mammoth-1.8.0.tgz#d8f1b0d3a0355fda129270346e9dc853f223028f" + integrity sha512-pJNfxSk9IEGVpau+tsZFz22ofjUsl2mnA5eT8PjPs2n0BP+rhVte4Nez6FdgEuxv3IGI3afiV46ImKqTGDVlbA== dependencies: "@xmldom/xmldom" "^0.8.6" argparse "~1.0.3" From 468958423d7d49c69fc1ec51dd0240090f5bd226 Mon Sep 17 00:00:00 2001 From: cjcolvar Date: Mon, 17 Jun 2024 11:54:29 -0400 Subject: [PATCH 087/152] Display Bibliographic ID distinctly from Other Identifiers and make searchable --- app/models/iiif_manifest_presenter.rb | 10 ++++++-- app/models/media_object.rb | 1 + spec/controllers/catalog_controller_spec.rb | 2 +- spec/models/iiif_manifest_presenter_spec.rb | 28 ++++++++++++++++++--- 4 files changed, 34 insertions(+), 7 deletions(-) diff --git a/app/models/iiif_manifest_presenter.rb b/app/models/iiif_manifest_presenter.rb index daa78fe450..4dab4f39b2 100644 --- a/app/models/iiif_manifest_presenter.rb +++ b/app/models/iiif_manifest_presenter.rb @@ -107,11 +107,16 @@ def combined_display_date(media_object) def display_other_identifiers(media_object) # bibliographic_id has form [:type,"value"], other_identifier has form [[:type,"value],[:type,"value"],...] - ids = media_object.bibliographic_id.present? ? [media_object.bibliographic_id] : [] - ids += Array(media_object.other_identifier) + ids = Array(media_object.other_identifier) - [media_object.bibliographic_id] + return nil unless ids.present? ids.uniq.collect { |i| "#{ModsDocument::IDENTIFIER_TYPES[i[:source]]}: #{i[:id]}" } end + def display_bibliographic_id(media_object) + return nil unless media_object.bibliographic_id.present? + "#{ModsDocument::IDENTIFIER_TYPES[media_object.bibliographic_id[:source]]}: #{media_object.bibliographic_id[:id]}" + end + def note_fields(media_object) fields = [] note_types = ModsDocument::NOTE_TYPES.clone @@ -197,6 +202,7 @@ def iiif_metadata_fields metadata_field('Lending Period', display_lending_period(media_object)) ] fields += note_fields(media_object) + fields += [metadata_field('Bibliographic ID', display_bibliographic_id(media_object))] fields += [metadata_field('Other Identifier', display_other_identifiers(media_object))] fields end diff --git a/app/models/media_object.rb b/app/models/media_object.rb index 84767ea07b..e2030cd738 100644 --- a/app/models/media_object.rb +++ b/app/models/media_object.rb @@ -353,6 +353,7 @@ def to_solr(include_child_fields: false) all_text_values << solr_doc["notes_sim"] all_text_values << solr_doc["table_of_contents_ssim"] all_text_values << solr_doc["other_identifier_sim"] + all_text_values << solr_doc["bibliographic_id_ssi"] solr_doc["all_text_timv"] = all_text_values.flatten solr_doc.each_pair { |k,v| solr_doc[k] = v.is_a?(Array) ? v.select { |e| e =~ /\S/ } : v } end diff --git a/spec/controllers/catalog_controller_spec.rb b/spec/controllers/catalog_controller_spec.rb index 1d8a748e79..296d584b24 100644 --- a/spec/controllers/catalog_controller_spec.rb +++ b/spec/controllers/catalog_controller_spec.rb @@ -172,7 +172,7 @@ describe "search fields" do let(:media_object) { FactoryBot.create(:fully_searchable_media_object) } - ["title_tesi", "creator_ssim", "contributor_ssim", "unit_ssim", "collection_ssim", "abstract_ssi", "publisher_ssim", "topical_subject_ssim", "geographic_subject_ssim", "temporal_subject_ssim", "genre_ssim", "physical_description_ssim", "language_ssim", "date_sim", "notes_sim", "table_of_contents_ssim", "other_identifier_sim", "series_ssim" ].each do |field| + ["title_tesi", "creator_ssim", "contributor_ssim", "unit_ssim", "collection_ssim", "abstract_ssi", "publisher_ssim", "topical_subject_ssim", "geographic_subject_ssim", "temporal_subject_ssim", "genre_ssim", "physical_description_ssim", "language_ssim", "date_sim", "notes_sim", "table_of_contents_ssim", "other_identifier_sim", "series_ssim", "bibliographic_id_ssi" ].each do |field| it "should find results based upon #{field}" do query = Array(media_object.to_solr[field]).first #split on ' ' and only search on the first word of a multiword field value diff --git a/spec/models/iiif_manifest_presenter_spec.rb b/spec/models/iiif_manifest_presenter_spec.rb index 4ae010dd84..686a3bb148 100644 --- a/spec/models/iiif_manifest_presenter_spec.rb +++ b/spec/models/iiif_manifest_presenter_spec.rb @@ -50,20 +50,40 @@ allow_any_instance_of(IiifManifestPresenter).to receive(:lending_enabled).and_return(false) ['Title', 'Date', 'Main contributor', 'Summary', 'Contributor', 'Publisher', 'Genre', 'Subject', 'Time period', - 'Location', 'Collection', 'Unit', 'Language', 'Rights Statement', 'Terms of Use', 'Physical Description', - 'Series', 'Related Item', 'Notes', 'Table of Contents', 'Local Note', 'Other Identifiers', 'Access Restrictions'].each do |field| + 'Location', 'Collection', 'Unit', 'Language', 'Rights Statement', 'Terms of Use', 'Physical Description', 'Series', + 'Related Item', 'Notes', 'Table of Contents', 'Local Note', 'Other Identifier', 'Access Restrictions', 'Bibliographic ID' + ].each do |field| expect(subject).to include(field) end expect(subject).to_not include('Lending Period') end + context 'with duplicate identifiers' do + let(:media_object) do + FactoryBot.build(:fully_searchable_media_object).tap do |mo| + mo.other_identifier += mo.other_identifier + mo.other_identifier += [mo.bibliographic_id] + mo + end + end + + it 'de-dupes other identifiers' do + allow_any_instance_of(IiifManifestPresenter).to receive(:lending_enabled).and_return(false) + expect(subject['Bibliographic ID'].first).to include media_object.bibliographic_id[:id] + expect(subject['Other Identifier'].size).to eq 1 + expect(subject['Other Identifier'].first).not_to include media_object.bibliographic_id[:id] + expect(subject['Other Identifier'].first).to include media_object.other_identifier.first[:id] + end + end + context 'when controlled digital lending is enabled' do it 'provides metadata' do allow_any_instance_of(IiifManifestPresenter).to receive(:lending_enabled).and_return(true) ['Title', 'Date', 'Main contributor', 'Summary', 'Contributor', 'Publisher', 'Genre', 'Subject', 'Time period', - 'Location', 'Collection', 'Unit', 'Language', 'Rights Statement', 'Terms of Use', 'Physical Description', - 'Series', 'Related Item', 'Notes', 'Table of Contents', 'Local Note', 'Other Identifiers', 'Access Restrictions', 'Lending Period'].each do |field| + 'Location', 'Collection', 'Unit', 'Language', 'Rights Statement', 'Terms of Use', 'Physical Description', 'Series', + 'Related Item', 'Notes', 'Table of Contents', 'Local Note', 'Other Identifier', 'Access Restrictions', 'Bibliographic ID', 'Lending Period' + ].each do |field| expect(subject).to include(field) end end From 920021e7a3b88052276ba5c404c33310f2f309c2 Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Mon, 17 Jun 2024 10:53:01 -0400 Subject: [PATCH 088/152] Move setting aspect ratio into update_progress_on_success! --- app/models/master_file.rb | 29 ++++++++--------------------- 1 file changed, 8 insertions(+), 21 deletions(-) diff --git a/app/models/master_file.rb b/app/models/master_file.rb index 3c69ee3cc4..b11947e982 100644 --- a/app/models/master_file.rb +++ b/app/models/master_file.rb @@ -171,7 +171,6 @@ def error after_transcoding :generate_waveform after_transcoding :update_ingest_batch # Generate and set the poster and thumbnail - after_transcoding :set_display_aspect_ratio after_transcoding :set_default_poster_offset after_transcoding :update_stills_from_offset! @@ -309,6 +308,13 @@ def update_progress_on_success!(encode) # is stored as an integer string self.duration = encode.input.duration.to_i.to_s if encode.input.duration.present? + # Input videos that are in portrait orientation can have metadata showing landscape orientation + # with a rotation value. This can cause the aspect ratio on the master file to be incorrect. + # We can get the proper aspect ratio from the transcoded files, so we set the master file off the + # encode output. + high_output = Array(encode.output).select { |out| out.label.include?("high") }.first + self.display_aspect_ratio = (high_output.width.to_f / high_output.height.to_f).to_s + outputs = Array(encode.output).collect do |output| { id: output.id, @@ -606,11 +612,9 @@ def extract_frame(options={}) end frame_size = (options[:size].nil? or options[:size] == 'auto') ? self.original_frame_size : options[:size] - # Handle existing files that may have inaccurate display_aspect_ratio saved. - aspect_ratio = actual_aspect_ratio != self.display_aspect_ratio.to_f ? actual_aspect_ratio : self.display_aspect_ratio (new_width,new_height) = frame_size.split(/x/).collect(&:to_f) - new_height = (new_width/aspect_ratio.to_f).round + new_height = (new_width/self.display_aspect_ratio.to_f).round frame_source = find_frame_source(offset: offset) data = get_ffmpeg_frame_data(frame_source, new_width, new_height, options[:headers]) raise RuntimeError, "Frame extraction failed. See log for details." if data.empty? @@ -750,23 +754,6 @@ def reloadTechnicalMetadata! end end - def actual_aspect_ratio - deriv = self.derivatives.where(quality_ssi: "high").first - - deriv.width.to_f / deriv.height.to_f - end - - # Input videos that are in portrait orientation can have metadata showing landscape orientation - # with a rotation value. This can cause the aspect ratio on the master file to be incorrect. - # We can get the proper aspect ratio from the transcoded files, so set the master file off the - # derivatives when the aspect ratios do not match. - def set_display_aspect_ratio - if self.display_aspect_ratio.to_f != actual_aspect_ratio - self.display_aspect_ratio = actual_aspect_ratio.to_s - self.save - end - end - # This should only be getting called by the :after_transcode hook. Ensure that # poster_offset is only set if it has not already been manually set via # BatchEntry or another method. From 70e0212e69d32eeb99aff66c38779ee2887280ea Mon Sep 17 00:00:00 2001 From: Mason Ballengee <68433277+masaball@users.noreply.github.com> Date: Mon, 17 Jun 2024 12:33:21 -0400 Subject: [PATCH 089/152] Add video check for setting aspect ratio Co-authored-by: Chris Colvard --- app/models/master_file.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/models/master_file.rb b/app/models/master_file.rb index b11947e982..0eb74941ca 100644 --- a/app/models/master_file.rb +++ b/app/models/master_file.rb @@ -312,8 +312,10 @@ def update_progress_on_success!(encode) # with a rotation value. This can cause the aspect ratio on the master file to be incorrect. # We can get the proper aspect ratio from the transcoded files, so we set the master file off the # encode output. - high_output = Array(encode.output).select { |out| out.label.include?("high") }.first - self.display_aspect_ratio = (high_output.width.to_f / high_output.height.to_f).to_s + if is_video? + high_output = Array(encode.output).select { |out| out.label.include?("high") }.first + self.display_aspect_ratio = (high_output.width.to_f / high_output.height.to_f).to_s + end outputs = Array(encode.output).collect do |output| { From 8a20bf427c6337547ba7258d4699c8dd58a46bdd Mon Sep 17 00:00:00 2001 From: cjcolvar Date: Fri, 14 Jun 2024 15:56:13 -0400 Subject: [PATCH 090/152] Add max_upload_size to config/settings.yml and default to disabling limit if :none or not present --- app/models/master_file.rb | 8 ++--- app/services/master_file_builder.rb | 2 +- app/views/media_objects/_file_upload.html.erb | 6 +++- config/settings.yml | 2 ++ .../master_files_controller_spec.rb | 7 ++++ spec/services/master_file_builder_spec.rb | 34 ++++++++++++++----- 6 files changed, 42 insertions(+), 17 deletions(-) diff --git a/app/models/master_file.rb b/app/models/master_file.rb index de171d4a33..96f6869390 100644 --- a/app/models/master_file.rb +++ b/app/models/master_file.rb @@ -176,12 +176,8 @@ def error after_processing :post_processing_file_management - # First and simplest test - make sure that the uploaded file does not exceed the - # limits of the system. For now this is hard coded but should probably eventually - # be set up in a configuration file somewhere - # - # 250 MB is the file limit for now - MAXIMUM_UPLOAD_SIZE = Settings.max_upload_size || 2.gigabytes + # Make sure that the uploaded file does not exceed the limits of the system + MAXIMUM_UPLOAD_SIZE = Settings.max_upload_size WORKFLOWS = ['fullaudio', 'avalon', 'pass_through', 'avalon-skip-transcoding', 'avalon-skip-transcoding-audio'].freeze AUDIO_TYPES = ["audio/vnd.wave", "audio/mpeg", "audio/mp3", "audio/mp4", "audio/wav", "audio/x-wav"] diff --git a/app/services/master_file_builder.rb b/app/services/master_file_builder.rb index b368228b76..3f891cfb6c 100644 --- a/app/services/master_file_builder.rb +++ b/app/services/master_file_builder.rb @@ -77,7 +77,7 @@ def self.create_upload_notice(format) module FileUpload def self.build(params) params[:Filedata].collect do |file| - if (file.size > MasterFile::MAXIMUM_UPLOAD_SIZE) + if (MasterFile::MAXIMUM_UPLOAD_SIZE.is_a? Numeric) && (file.size > MasterFile::MAXIMUM_UPLOAD_SIZE) raise BuildError, "The file you have uploaded is too large" end Spec.new(file, file.original_filename, file.content_type, params[:workflow]) diff --git a/app/views/media_objects/_file_upload.html.erb b/app/views/media_objects/_file_upload.html.erb index 03167c123e..bd51e61f81 100644 --- a/app/views/media_objects/_file_upload.html.erb +++ b/app/views/media_objects/_file_upload.html.erb @@ -178,7 +178,11 @@ Unless required by applicable law or agreed to in writing, software distributed
-

Upload through the web (files must not exceed <%= number_to_human_size MasterFile::MAXIMUM_UPLOAD_SIZE %>)

+ <% if MasterFile::MAXIMUM_UPLOAD_SIZE.is_a? Numeric %> +

Upload through the web (files must not exceed <%= number_to_human_size MasterFile::MAXIMUM_UPLOAD_SIZE %>)

+ <% else %> +

Upload through the web

+ <% end %>
<%= form_tag(master_files_path, :enctype=>"multipart/form-data", class: upload_form_classes, data: upload_form_data) do -%> diff --git a/config/settings.yml b/config/settings.yml index d8a81d3f4e..ec358e8a0c 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -121,3 +121,5 @@ recaptcha: derivative: # Choose whether collection managers and admins can download high quality derivatives allow_download: true +# Maximum size for uploaded files in bytes (default is disabled) +#max_upload_size: 2147483648 # Use :none or comment out to disable limit diff --git a/spec/controllers/master_files_controller_spec.rb b/spec/controllers/master_files_controller_spec.rb index cbbb9acdbc..bb17b06b53 100644 --- a/spec/controllers/master_files_controller_spec.rb +++ b/spec/controllers/master_files_controller_spec.rb @@ -57,6 +57,13 @@ end context "cannot upload a file over the defined limit" do + around do |example| + @old_maximum_upload_size = MasterFile::MAXIMUM_UPLOAD_SIZE + MasterFile::MAXIMUM_UPLOAD_SIZE = 2.gigabytes + example.run + MasterFile::MAXIMUM_UPLOAD_SIZE = @old_maximum_upload_size + end + it "provides a warning about the file size" do request.env["HTTP_REFERER"] = "/" diff --git a/spec/services/master_file_builder_spec.rb b/spec/services/master_file_builder_spec.rb index 0a3c4800e4..e029131bca 100644 --- a/spec/services/master_file_builder_spec.rb +++ b/spec/services/master_file_builder_spec.rb @@ -24,17 +24,33 @@ end describe 'FileUpload.build' do - it "should raise error if file is too big" do - file = double("file", size: MasterFile::MAXIMUM_UPLOAD_SIZE + 1) - params = { Filedata: [file] } - expect { MasterFileBuilder::FileUpload.build(params) }.to raise_error MasterFileBuilder::BuildError + context "with upload limit enabled" do + around do |example| + @old_maximum_upload_size = MasterFile::MAXIMUM_UPLOAD_SIZE + MasterFile::MAXIMUM_UPLOAD_SIZE = 2.gigabytes + example.run + MasterFile::MAXIMUM_UPLOAD_SIZE = @old_maximum_upload_size + end + + it "should raise error if file is too big" do + file = double("file", size: MasterFile::MAXIMUM_UPLOAD_SIZE + 1) + params = { Filedata: [file] } + expect { MasterFileBuilder::FileUpload.build(params) }.to raise_error MasterFileBuilder::BuildError + end + + it "should return a Spec for legit file" do + file = double("file", size: MasterFile::MAXIMUM_UPLOAD_SIZE - 1, original_filename: "aname", content_type: "mp4") + params = { Filedata: [file], workflow: double("workflow") } + s = MasterFileBuilder::FileUpload.build(params) + expect(s).to eq [MasterFileBuilder::Spec.new(file, file.original_filename, file.content_type, params[:workflow])] + end end - it "should return a Spec for legit file" do - file = double("file", size: MasterFile::MAXIMUM_UPLOAD_SIZE - 1, original_filename: "aname", content_type: "mp4") - params = { Filedata: [file], workflow: double("workflow") } - s = MasterFileBuilder::FileUpload.build(params) - expect(s).to eq [MasterFileBuilder::Spec.new(file, file.original_filename, file.content_type, params[:workflow])] + it "should not raise error when no limit" do + expect(MasterFile::MAXIMUM_UPLOAD_SIZE.is_a? Numeric).to eq false + file = double("file", size: 2.gigabytes + 1) + params = { Filedata: [file] } + expect { MasterFileBuilder::FileUpload.build(params) }.not_to raise_error MasterFileBuilder::BuildError end end From 65a8c7c2d4d28168bb2049e79fae4dec2ea3151d Mon Sep 17 00:00:00 2001 From: cjcolvar Date: Mon, 17 Jun 2024 15:52:19 -0400 Subject: [PATCH 091/152] Skip validations when migrating --- lib/tasks/avalon_migrations.rake | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/tasks/avalon_migrations.rake b/lib/tasks/avalon_migrations.rake index e188e633da..3b5bd49cb6 100644 --- a/lib/tasks/avalon_migrations.rake +++ b/lib/tasks/avalon_migrations.rake @@ -77,7 +77,7 @@ namespace :avalon do begin if file.parent_id.blank? file.parent_id = object["id"] - file.save! + file.save!(validate: false) else next end @@ -100,7 +100,7 @@ namespace :avalon do ids_to_migrate = ActiveFedora::SolrService.query("has_model_ssim:MediaObject AND NOT section_list_ssim:[* TO *]", rows: mo_count).pluck("id") puts "Migrating #{ids_to_migrate.size} out of #{mo_count} Media Objects." ids_to_migrate.each do |id| - MediaObject.find(id).save! + MediaObject.find(id).save!(validate: false) rescue StandardError => error error_ids += [id] puts "Error migrating #{id}: #{error.message}" From dd4e79b248e7682519abc95f4036fdc7de15d3e7 Mon Sep 17 00:00:00 2001 From: cjcolvar Date: Mon, 17 Jun 2024 17:22:37 -0400 Subject: [PATCH 092/152] Avalon::TranscriptSearch does phrase searching by default and provides an option to turn it off --- lib/avalon/transcript_search.rb | 18 +++-- .../master_files_controller_spec.rb | 9 +++ spec/lib/avalon/transcript_search.rb | 75 ++++++++++++------- 3 files changed, 69 insertions(+), 33 deletions(-) diff --git a/lib/avalon/transcript_search.rb b/lib/avalon/transcript_search.rb index 83f70bc163..26c788d402 100644 --- a/lib/avalon/transcript_search.rb +++ b/lib/avalon/transcript_search.rb @@ -24,10 +24,14 @@ def initialize(query:, master_file:, request_url: nil) @request_url = request_url end - def perform_search - terms = query.split - term_subquery = terms.map { |term| "transcript_tsim:#{RSolr.solr_escape(term)}" }.join(" OR ") - ActiveFedora::SolrService.get("isPartOf_ssim:#{master_file.id} AND #{term_subquery}", + def perform_search(phrase_searching: true) + subquery = if phrase_searching + "transcript_tsim:\"#{RSolr.solr_escape(query)}\"" + else + query.split.map { |term| "transcript_tsim:#{RSolr.solr_escape(term)}" }.join(" OR ") + end + + ActiveFedora::SolrService.get("isPartOf_ssim:#{master_file.id} AND #{subquery}", "fl": "id,mime_type_ssi", "hl": true, "hl.fl": "transcript_tsim", @@ -36,8 +40,8 @@ def perform_search "hl.method": "original") end - def iiif_content_search - results = perform_search + def iiif_content_search(phrase_searching: true) + results = perform_search(phrase_searching: phrase_searching) { "@context": "http://iiif.io/api/search/2/context.json", @@ -95,4 +99,4 @@ def format_item(result, canvas, target, time_cue: nil) } end end -end \ No newline at end of file +end diff --git a/spec/controllers/master_files_controller_spec.rb b/spec/controllers/master_files_controller_spec.rb index bb17b06b53..a742861d44 100644 --- a/spec/controllers/master_files_controller_spec.rb +++ b/spec/controllers/master_files_controller_spec.rb @@ -855,5 +855,14 @@ class << file result = JSON.parse(response.body) expect(result['items'].any? { |item| item["id"].include?(other_master_file.id) }).to eq false end + + it 'does phrase searches' do + get('search', params: { id: parent_master_file.id, q: 'before lunch' } ) + result = JSON.parse(response.body) + items = result["items"] + expect(items.count).to eq 1 + expect(items[0]["body"]["value"]).to eq "Just before lunch one day, a puppet show was put on at school." + expect(items[0]["target"]).to eq "#{Rails.application.routes.url_helpers.transcripts_master_file_supplemental_file_url(parent_master_file.id, transcript_1.id)}#t=00:00:22.200,00:00:26.600" + end end end diff --git a/spec/lib/avalon/transcript_search.rb b/spec/lib/avalon/transcript_search.rb index c6776df281..67294b2353 100644 --- a/spec/lib/avalon/transcript_search.rb +++ b/spec/lib/avalon/transcript_search.rb @@ -18,34 +18,50 @@ describe Avalon::TranscriptSearch do subject { described_class.new(query: 'before', master_file: parent_master_file) } - let(:transcript) { FactoryBot.create(:supplemental_file, :with_transcript_tag, parent_id: parent_master_file.id, file: fixture_file_upload(Rails.root.join('spec', 'fixtures', 'chunk_test.txt'), 'text/plain')) } + let(:transcript) { FactoryBot.create(:supplemental_file, :with_transcript_tag, parent_id: parent_master_file.id, file: fixture_file_upload(Rails.root.join('spec', 'fixtures', 'chunk_test.vtt'), transcript_mime_type)) } + let(:transcript_global_id) { transcript.to_global_id.to_s } + let(:transcript_mime_type) { 'text/vtt' } + let(:transcript2) { FactoryBot.create(:supplemental_file, :with_transcript_tag, parent_id: parent_master_file.id, file: fixture_file_upload(Rails.root.join('spec', 'fixtures', 'chunk_test.txt'), transcript2_mime_type)) } + let(:transcript2_global_id) { transcript2.to_global_id.to_s } + let(:transcript2_mime_type) { 'text/plain' } let(:parent_master_file) { FactoryBot.create(:master_file, :with_media_object) } before :each do - parent_master_file.supplemental_files += [transcript] + parent_master_file.supplemental_files += [transcript, transcript2] parent_master_file.save end describe '#perform_search' do it 'returns a solr document of transcript chunks with the matching term' do search = subject.perform_search - global_id = transcript.to_global_id.to_s - expect(search['response']['docs'].first['id']).to eq global_id - expect(search['response']['docs'].first['mime_type_ssi']).to eq transcript.file.content_type - expect(search['highlighting'][global_id]['transcript_tsim']).to be_a Array - expect(search['highlighting'][global_id]['transcript_tsim'].count).to eq 1 - expect(search['highlighting'][global_id]['transcript_tsim'].first).to include 'before' + expect(search['response']['docs'].count).to eq 2 + expect(search['response']['docs']).to include({"id"=>transcript_global_id, "mime_type_ssi"=>transcript_mime_type}) + expect(search['response']['docs']).to include({"id"=>transcript2_global_id, "mime_type_ssi"=>transcript2_mime_type}) + expect(search['highlighting'][transcript_global_id]['transcript_tsim']).to be_a Array + expect(search['highlighting'][transcript_global_id]['transcript_tsim'].count).to eq 1 + expect(search['highlighting'][transcript_global_id]['transcript_tsim'].first).to include 'before' + expect(search['highlighting'][transcript2_global_id]['transcript_tsim']).to be_a Array + expect(search['highlighting'][transcript2_global_id]['transcript_tsim'].count).to eq 1 + expect(search['highlighting'][transcript2_global_id]['transcript_tsim'].first).to include 'before' end context 'with multiple terms in the query' do subject { described_class.new(query: 'before lunch', master_file: parent_master_file) } - let(:transcript) { FactoryBot.create(:supplemental_file, :with_transcript_tag, parent_id: parent_master_file.id, file: fixture_file_upload(Rails.root.join('spec', 'fixtures', 'chunk_test.vtt'), 'text/vtt')) } - - it 'returns a solr document of transcript chunks with any of the matching terms' do + + it 'returns a solr document of transcript chunks' do search = subject.perform_search - global_id = transcript.to_global_id.to_s # solr search is case insensitive, so we convert results to lower case for simpler testing - expect(search['highlighting'][global_id]['transcript_tsim'].all? { |h| h.downcase.include?('before') || h.downcase.include?('lunch') }).to eq true + expect(search['highlighting'][transcript_global_id]['transcript_tsim'].all? { |h| h.downcase.include?('before lunch') }).to eq true + expect(search['highlighting'][transcript_global_id]['transcript_tsim'].count).to eq 1 + end + + context 'with phrase searching disabled' do + it 'returns a solr document of transcript chunks with any of the matching terms' do + search = subject.perform_search(phrase_searching: false) + # solr search is case insensitive, so we convert results to lower case for simpler testing + expect(search['highlighting'][transcript_global_id]['transcript_tsim'].all? { |h| h.downcase.include?('before') || h.downcase.include?('lunch') }).to eq true + expect(search['highlighting'][transcript_global_id]['transcript_tsim'].count).to eq 4 + end end end end @@ -60,21 +76,28 @@ expect(search[:items]).to be_present items = search[:items] expect(items).to be_a Array - item = items.first - expect(item[:id]).to eq "#{Rails.application.routes.url_helpers.media_object_url(parent_master_file.media_object_id)}/manifest/canvas/#{parent_master_file.id}/search/abc1234" - expect(item[:type]).to eq 'Annotation' - expect(item[:motivation]).to eq 'supplementing' - expect(item[:body]).to be_present - expect(item[:body][:type]).to eq 'TextualBody' - expect(item[:body][:value]).to be_a String - expect(item[:body][:value]).to include 'before' - expect(item[:body][:format]).to eq 'text/plain' - expect(item[:target]).to eq Rails.application.routes.url_helpers.transcripts_master_file_supplemental_file_url(parent_master_file.id, transcript.id) + expect(items.first[:id]).to eq "#{Rails.application.routes.url_helpers.media_object_url(parent_master_file.media_object_id)}/manifest/canvas/#{parent_master_file.id}/search/abc1234" + expect(items.first[:type]).to eq 'Annotation' + expect(items.first[:motivation]).to eq 'supplementing' + expect(items.first[:body]).to be_present + expect(items.first[:body][:type]).to eq 'TextualBody' + expect(items.first[:body][:value]).to be_a String + expect(items.first[:body][:value]).to include 'before' + expect(items.first[:body][:format]).to eq 'text/plain' + expect(items.first[:target]).to eq Rails.application.routes.url_helpers.transcripts_master_file_supplemental_file_url(parent_master_file.id, transcript.id, anchor: "t=00:00:22.200,00:00:26.600") + expect(items.second[:id]).to eq "#{Rails.application.routes.url_helpers.media_object_url(parent_master_file.media_object_id)}/manifest/canvas/#{parent_master_file.id}/search/abc1234" + expect(items.second[:type]).to eq 'Annotation' + expect(items.second[:motivation]).to eq 'supplementing' + expect(items.second[:body]).to be_present + expect(items.second[:body][:type]).to eq 'TextualBody' + expect(items.second[:body][:value]).to be_a String + expect(items.second[:body][:value]).to include 'before' + expect(items.second[:body][:format]).to eq 'text/plain' + expect(items.second[:target]).to eq Rails.application.routes.url_helpers.transcripts_master_file_supplemental_file_url(parent_master_file.id, transcript2.id) + end context 'transcript with timed text' do - let(:transcript) { FactoryBot.create(:supplemental_file, :with_transcript_tag, parent_id: parent_master_file.id, file: fixture_file_upload(Rails.root.join('spec', 'fixtures', 'chunk_test.vtt'), 'text/vtt')) } - it 'returns result value without time cues' do item = subject.iiif_content_search[:items].first expect(item[:body][:value]).to_not match(/\d{2}:\d{2}:\d{2}/) @@ -86,4 +109,4 @@ end end end -end \ No newline at end of file +end From ae5258b8a4d8d83780dcb9592f68849c7d89f89f Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Tue, 18 Jun 2024 13:36:08 -0400 Subject: [PATCH 093/152] Bump VideoJS version dependency When we upgraded Ramp to video.js@^8.10.0, we neglected to bump the dependency in Avalon's package.json --- package.json | 2 +- yarn.lock | 127 +++++++++++++++++++++++++++++---------------------- 2 files changed, 73 insertions(+), 56 deletions(-) diff --git a/package.json b/package.json index af93d90620..ce260e76ed 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "terser-webpack-plugin": "5", "url-search-params-polyfill": "^7.0.1", "util": "^0.12.5", - "video.js": "7.21.4", + "video.js": "^8.10.0", "webpack": "5", "webpack-assets-manifest": "5", "webpack-merge": "^5.9.0" diff --git a/yarn.lock b/yarn.lock index 8bbc7f0bc8..0421754ed6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1797,21 +1797,30 @@ dependencies: "@types/node" "*" -"@videojs/http-streaming@2.16.2": - version "2.16.2" - resolved "https://registry.yarnpkg.com/@videojs/http-streaming/-/http-streaming-2.16.2.tgz#a9be925b4e368a41dbd67d49c4f566715169b84b" - integrity sha512-etPTUdCFu7gUWc+1XcbiPr+lrhOcBu3rV5OL1M+3PDW89zskScAkkcdqYzP4pFodBPye/ydamQoTDScOnElw5A== +"@videojs/http-streaming@3.12.1": + version "3.12.1" + resolved "https://registry.yarnpkg.com/@videojs/http-streaming/-/http-streaming-3.12.1.tgz#0d924879bc395e66e1d112698e0f54920ffae1d7" + integrity sha512-rpB5AMt0QZ9bMXzwiWhynF2NLNnm5g2DZjPOFX6OoFqqXhbe2ngY2nqm9lLRhRVe22YeysQCmAlvBNwGuWFI8Q== dependencies: "@babel/runtime" "^7.12.5" - "@videojs/vhs-utils" "3.0.5" - aes-decrypter "3.1.3" + "@videojs/vhs-utils" "4.0.0" + aes-decrypter "4.0.1" global "^4.4.0" - m3u8-parser "4.8.0" - mpd-parser "^0.22.1" - mux.js "6.0.1" - video.js "^6 || ^7" + m3u8-parser "^7.1.0" + mpd-parser "^1.3.0" + mux.js "7.0.3" + video.js "^7 || ^8" -"@videojs/vhs-utils@3.0.5", "@videojs/vhs-utils@^3.0.4", "@videojs/vhs-utils@^3.0.5": +"@videojs/vhs-utils@4.0.0", "@videojs/vhs-utils@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@videojs/vhs-utils/-/vhs-utils-4.0.0.tgz#4d4dbf5d61a9fbd2da114b84ec747c3a483bc60d" + integrity sha512-xJp7Yd4jMLwje2vHCUmi8MOUU76nxiwII3z4Eg3Ucb+6rrkFVGosrXlMgGnaLjq724j3wzNElRZ71D/CKrTtxg== + dependencies: + "@babel/runtime" "^7.12.5" + global "^4.4.0" + url-toolkit "^2.2.1" + +"@videojs/vhs-utils@^3.0.5": version "3.0.5" resolved "https://registry.yarnpkg.com/@videojs/vhs-utils/-/vhs-utils-3.0.5.tgz#665ba70d78258ba1ab977364e2fe9f4d4799c46c" integrity sha512-PKVgdo8/GReqdx512F+ombhS+Bzogiofy1LgAj4tN8PfdBx3HSS7V5WfJotKTqtOWGwVfSWsrYN/t09/DSryrw== @@ -2013,10 +2022,10 @@ acorn@^8.7.1, acorn@^8.8.2: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.10.0.tgz#8be5b3907a67221a81ab23c7889c4c5526b62ec5" integrity sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw== -aes-decrypter@3.1.3: - version "3.1.3" - resolved "https://registry.yarnpkg.com/aes-decrypter/-/aes-decrypter-3.1.3.tgz#65ff5f2175324d80c41083b0e135d1464b12ac35" - integrity sha512-VkG9g4BbhMBy+N5/XodDeV6F02chEk9IpgRTq/0bS80y4dzy79VH2Gtms02VXomf3HmyRe3yyJYkJ990ns+d6A== +aes-decrypter@4.0.1, aes-decrypter@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/aes-decrypter/-/aes-decrypter-4.0.1.tgz#c1a81d0bde0e96fed0674488d2a31a6d7ab9b7a7" + integrity sha512-H1nh/P9VZXUf17AA5NQfJML88CFjVBDuGkp5zDHa7oEhYN9TTpNLJknRY1ie0iSKWlDf6JRnJKaZVDSQdPy6Cg== dependencies: "@babel/runtime" "^7.12.5" "@videojs/vhs-utils" "^3.0.5" @@ -3711,7 +3720,7 @@ global-dirs@^3.0.0: dependencies: ini "2.0.0" -global@^4.3.1, global@^4.4.0, global@~4.4.0: +global@4.4.0, global@^4.3.1, global@^4.4.0, global@~4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/global/-/global-4.4.0.tgz#3e7b105179006a323ed71aafca3e9c57a5cc6406" integrity sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w== @@ -4435,10 +4444,10 @@ jszip@^3.7.1: readable-stream "~2.3.6" setimmediate "^1.0.5" -keycode@^2.2.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/keycode/-/keycode-2.2.1.tgz#09c23b2be0611d26117ea2501c2c391a01f39eff" - integrity sha512-Rdgz9Hl9Iv4QKi8b0OlCRQEzp4AgVxyCtz5S/+VIHezDmrDhkp2N2TqBWOLz0/gbeREXOOiI9/4b8BY9uw2vFg== +keycode@2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/keycode/-/keycode-2.2.0.tgz#3d0af56dc7b8b8e5cba8d0a97f107204eec22b04" + integrity sha512-ps3I9jAdNtRpJrbBvQjpzyFbss/skHqzS+eu4RxKLaEAtFqkjZaB6TZMSivPbLxf4K7VI4SjR0P5mRCX5+Q25A== kind-of@^6.0.2, kind-of@^6.0.3: version "6.0.3" @@ -4612,10 +4621,10 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" -m3u8-parser@4.8.0: - version "4.8.0" - resolved "https://registry.yarnpkg.com/m3u8-parser/-/m3u8-parser-4.8.0.tgz#4a2d591fdf6f2579d12a327081198df8af83083d" - integrity sha512-UqA2a/Pw3liR6Df3gwxrqghCP17OpPlQj6RBPLYygf/ZSQ4MoSgvdvhvt35qV+3NaaA0FSZx93Ix+2brT1U7cA== +m3u8-parser@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/m3u8-parser/-/m3u8-parser-7.1.0.tgz#fa92ee22fc798150397c297152c879fe09f066c6" + integrity sha512-7N+pk79EH4oLKPEYdgRXgAsKDyA/VCo0qCHlUwacttQA0WqsjZQYmNfywMvjlY9MpEBVZEt0jKFd73Kv15EBYQ== dependencies: "@babel/runtime" "^7.12.5" "@videojs/vhs-utils" "^3.0.5" @@ -4769,13 +4778,13 @@ moment@^2.29.4: resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108" integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w== -mpd-parser@0.22.1, mpd-parser@^0.22.1: - version "0.22.1" - resolved "https://registry.yarnpkg.com/mpd-parser/-/mpd-parser-0.22.1.tgz#bc2bf7d3e56368e4b0121035b055675401871521" - integrity sha512-fwBebvpyPUU8bOzvhX0VQZgSohncbgYwUyJJoTSNpmy7ccD2ryiCvM7oRkn/xQH5cv73/xU7rJSNCLjdGFor0Q== +mpd-parser@^1.2.2, mpd-parser@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/mpd-parser/-/mpd-parser-1.3.0.tgz#38c20f4d73542b4ed554158bc1f0fa571dc61388" + integrity sha512-WgeIwxAqkmb9uTn4ClicXpEQYCEduDqRKfmUdp4X8vmghKfBNXZLYpREn9eqrDx/Tf5LhzRcJLSpi4ohfV742Q== dependencies: "@babel/runtime" "^7.12.5" - "@videojs/vhs-utils" "^3.0.5" + "@videojs/vhs-utils" "^4.0.0" "@xmldom/xmldom" "^0.8.3" global "^4.4.0" @@ -4802,10 +4811,10 @@ multicast-dns@^7.2.4: dns-packet "^5.2.2" thunky "^1.0.2" -mux.js@6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/mux.js/-/mux.js-6.0.1.tgz#65ce0f7a961d56c006829d024d772902d28c7755" - integrity sha512-22CHb59rH8pWGcPGW5Og7JngJ9s+z4XuSlYvnxhLuc58cA1WqGDQPzuG8I+sPm1/p0CdgpzVTaKW408k5DNn8w== +mux.js@7.0.3, mux.js@^7.0.1: + version "7.0.3" + resolved "https://registry.yarnpkg.com/mux.js/-/mux.js-7.0.3.tgz#18fbbc607faeeaa8c897e0410d2067be2bdc7e1e" + integrity sha512-gzlzJVEGFYPtl2vvEiJneSWAWD4nfYRHD5XgxmB2gWvXraMPOYk+sxfvexmNfjQUFpmk6hwLR5C6iSFmuwCHdQ== dependencies: "@babel/runtime" "^7.11.2" global "^4.4.0" @@ -6678,39 +6687,47 @@ verror@1.10.0: core-util-is "1.0.2" extsprintf "^1.2.0" -video.js@7.21.4, "video.js@^6 || ^7": - version "7.21.4" - resolved "https://registry.yarnpkg.com/video.js/-/video.js-7.21.4.tgz#362a2549467434b27507e0420b30eb4758feb128" - integrity sha512-R5e57M/5uqxQMQpFpybNbd8GtiRwFJPqkHjrhv0QTJ2tqnesbjETbck5kU5dhFr1FevsJRFhjBG4hAnvRGnXbw== +"video.js@^7 || ^8", video.js@^8.10.0: + version "8.12.0" + resolved "https://registry.yarnpkg.com/video.js/-/video.js-8.12.0.tgz#f85295f55afbef8b29d885e23777d3a64651f536" + integrity sha512-bLjfg3y09CAed1xZ4FujdTW7G9kgL0CJHaBnDKwBUgYuutijCutYPP5yQGCdN6VOi76uEuOpINwmTJSJia6zww== dependencies: "@babel/runtime" "^7.12.5" - "@videojs/http-streaming" "2.16.2" - "@videojs/vhs-utils" "^3.0.4" + "@videojs/http-streaming" "3.12.1" + "@videojs/vhs-utils" "^4.0.0" "@videojs/xhr" "2.6.0" - aes-decrypter "3.1.3" - global "^4.4.0" - keycode "^2.2.0" - m3u8-parser "4.8.0" - mpd-parser "0.22.1" - mux.js "6.0.1" + aes-decrypter "^4.0.1" + global "4.4.0" + keycode "2.2.0" + m3u8-parser "^7.1.0" + mpd-parser "^1.2.2" + mux.js "^7.0.1" safe-json-parse "4.0.0" - videojs-font "3.2.0" - videojs-vtt.js "^0.15.4" + videojs-contrib-quality-levels "4.1.0" + videojs-font "4.1.0" + videojs-vtt.js "0.15.5" -videojs-font@3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/videojs-font/-/videojs-font-3.2.0.tgz#212c9d3f4e4ec3fa7345167d64316add35e92232" - integrity sha512-g8vHMKK2/JGorSfqAZQUmYYNnXmfec4MLhwtEFS+mMs2IDY398GLysy6BH6K+aS1KMNu/xWZ8Sue/X/mdQPliA== +videojs-contrib-quality-levels@4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/videojs-contrib-quality-levels/-/videojs-contrib-quality-levels-4.1.0.tgz#44c2d2167114a5c8418548b10a25cb409d6cba51" + integrity sha512-TfrXJJg1Bv4t6TOCMEVMwF/CoS8iENYsWNKip8zfhB5kTcegiFYezEA0eHAJPU64ZC8NQbxQgOwAsYU8VXbOWA== + dependencies: + global "^4.4.0" + +videojs-font@4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/videojs-font/-/videojs-font-4.1.0.tgz#3ae1dbaac60b4f0f1c4e6f7ff9662a89df176015" + integrity sha512-X1LuPfLZPisPLrANIAKCknZbZu5obVM/ylfd1CN+SsCmPZQ3UMDPcvLTpPBJxcBuTpHQq2MO1QCFt7p8spnZ/w== videojs-markers-plugin@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/videojs-markers-plugin/-/videojs-markers-plugin-1.0.2.tgz#7e40da152504a0f1be0ee1cf608c7dccc4abf8b3" integrity sha512-yxUp6hT0Czx7i7MMMxcqExhOcDp48vkJUMJccnb1XhUqc52gbnVipP10g2XG0ZDkJ2WBXKQw49JI+pZXZUkwPg== -videojs-vtt.js@^0.15.4: - version "0.15.4" - resolved "https://registry.yarnpkg.com/videojs-vtt.js/-/videojs-vtt.js-0.15.4.tgz#5dc5aabcd82ba40c5595469bd855ea8230ca152c" - integrity sha512-r6IhM325fcLb1D6pgsMkTQT1PpFdUdYZa1iqk7wJEu+QlibBwATPfPc9Bg8Jiym0GE5yP1AG2rMLu+QMVWkYtA== +videojs-vtt.js@0.15.5: + version "0.15.5" + resolved "https://registry.yarnpkg.com/videojs-vtt.js/-/videojs-vtt.js-0.15.5.tgz#567776eaf2a7a928d88b148a8b401ade2406f2ca" + integrity sha512-yZbBxvA7QMYn15Lr/ZfhhLPrNpI/RmCSCqgIff57GC2gIrV5YfyzLfLyZMj0NnZSAz8syB4N0nHXpZg9MyrMOQ== dependencies: global "^4.3.1" From 47afda1a16995cfdaab208dede22391c84257f7c Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Thu, 20 Jun 2024 09:21:04 -0400 Subject: [PATCH 094/152] Set footer to negative z-index so paging works If controls such as the playlist paging ended up on the same level as the footer they were not clickable because the padding around the footer was covering them up. Setting the footer with a large negative z-index ensures that it will always be below the paging controls. --- app/assets/stylesheets/avalon/_footer.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/stylesheets/avalon/_footer.scss b/app/assets/stylesheets/avalon/_footer.scss index bd8499adc6..5313f9a3f9 100644 --- a/app/assets/stylesheets/avalon/_footer.scss +++ b/app/assets/stylesheets/avalon/_footer.scss @@ -21,6 +21,7 @@ width: 100%; min-height: 2.5rem; color: #999999; + z-index: -1000; } footer { From 34168389d563f6212008a770ee7e32ce64875856 Mon Sep 17 00:00:00 2001 From: dwithana Date: Thu, 20 Jun 2024 14:38:14 -0400 Subject: [PATCH 095/152] Use player.src() to enable action buttons instead of player.readyState() --- app/assets/javascripts/player_listeners.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/app/assets/javascripts/player_listeners.js b/app/assets/javascripts/player_listeners.js index f79266fb84..1db0483b13 100644 --- a/app/assets/javascripts/player_listeners.js +++ b/app/assets/javascripts/player_listeners.js @@ -19,6 +19,7 @@ let currentSectionLabel = undefined; let addToPlaylistListenersAdded = false; let firstLoad = true; let streamId = ''; +let isMobile = false; /** * Bind action buttons on the page with player events and re-populate details @@ -34,15 +35,13 @@ function addActionButtonListeners(player, mediaObjectId, sectionIds) { if (player && player.player != undefined) { let currentIndex = parseInt(player.dataset.canvasindex); /* - For iOS Safari, player.readyState() is 0 until media playback is + For both Android and iOS, player.readyState() is 0 until media playback is started. Therefore, use player.src() to check whether there's a playable media loaded into the player instead of player.readyState(). Keep the player.readyState() >= 2 check for desktop browsers, because without that check the add to playlist form populates NaN values for time fields when user clicks the 'Add to Playlist' button immediately on page load, which does not happen in mobile context. - Update all action buttons and share links when player.readyState() >= 2, - i.e. player has enough data for playable media source */ const USER_AGENT = window.navigator.userAgent; // Identify both iPad and iPhone devices @@ -50,9 +49,10 @@ function addActionButtonListeners(player, mediaObjectId, sectionIds) { const IS_MOBILE = (/Mobi/i).test(USER_AGENT); const IS_TOUCH_ONLY = navigator.maxTouchPoints && navigator.maxTouchPoints > 2 && !window.matchMedia("(pointer: fine").matches; const IS_SAFARI = (/Safari/i).test(USER_AGENT); + // For mobile devices use this check instead of player.readyState() >= 2 for enabling action buttons + isMobile = (IS_TOUCH_ONLY || IS_IPHONE || IS_MOBILE) && IS_SAFARI && player?.player.src() != ''; if (currentIndex != canvasIndex && !player.player.canvasIsEmpty) { - if ((((IS_TOUCH_ONLY || IS_IPHONE || IS_MOBILE) && IS_SAFARI && player?.player.src() != '') - || player?.player.readyState() >= 2)) { + if (isMobile || player?.player.readyState() >= 2) { canvasIndex = currentIndex; buildActionButtons(player, mediaObjectId, sectionIds); firstLoad = false; @@ -198,7 +198,7 @@ function setUpAddToPlaylist(player, sectionIds, mediaObjectId) { let addToPlaylistBtn = document.getElementById('addToPlaylistBtn'); if (addToPlaylistBtn && addToPlaylistBtn.disabled - && player.player?.readyState() >= 2) { + && (player.player?.readyState() >= 2 || isMobile)) { addToPlaylistBtn.disabled = false; if (!addToPlaylistListenersAdded) { // Add 'Add new playlist' option to dropdown @@ -334,7 +334,7 @@ function setUpCreateThumbnail(player, sectionIds) { // Leave 'Create Thumbnail' button disabled when item is audio if (thumbnailBtn && thumbnailBtn.disabled - && player.player?.readyState() >= 2 && !player.player.isAudio()) { + && (player.player?.readyState() >= 2 || isMobile) && !player.player.isAudio()) { thumbnailBtn.disabled = false; /* @@ -423,7 +423,7 @@ function setUpCreateTimeline(player) { let timelineBtn = document.getElementById('timelineBtn'); if (timelineBtn && timelineBtn.disabled - && player.player?.readyState() >= 2) { + && (player.player?.readyState() >= 2 || isMobile)) { timelineBtn.disabled = false; timelineBtn.addEventListener('click', () => handleCreateTimelineModalShow() From 282df316d835edd7e7bb9f318a1675d07de328b9 Mon Sep 17 00:00:00 2001 From: cjcolvar Date: Mon, 24 Jun 2024 13:10:04 -0400 Subject: [PATCH 096/152] Fix NoMethodError when saving parent object fails --- .../supplemental_files_controller.rb | 4 +-- .../supplemental_files_controller_examples.rb | 31 +++++++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/app/controllers/supplemental_files_controller.rb b/app/controllers/supplemental_files_controller.rb index c9b8f3c8ea..a37de63b38 100644 --- a/app/controllers/supplemental_files_controller.rb +++ b/app/controllers/supplemental_files_controller.rb @@ -59,7 +59,7 @@ def create raise Avalon::SaveError, @supplemental_file.errors.full_messages unless @supplemental_file.save @object.supplemental_files += [@supplemental_file] - raise Avalon::SaveError, @object.errors[:supplemental_files_json].full_messages unless @object.save + raise Avalon::SaveError, @object.errors[:supplemental_files_json] unless @object.save flash[:success] = "Supplemental file successfully added." @@ -131,7 +131,7 @@ def destroy find_supplemental_file @object.supplemental_files -= [@supplemental_file] - raise Avalon::SaveError, "An error occurred when deleting the supplemental file: #{@object.errors[:supplemental_files_json].full_messages}" unless @object.save + raise Avalon::SaveError, "An error occurred when deleting the supplemental file: #{@object.errors[:supplemental_files_json]}" unless @object.save # FIXME: also wrap this in a transaction raise Avalon::SaveError, "An error occurred when deleting the supplemental file: #{@supplemental_file.errors.full_messages}" unless @supplemental_file.destroy diff --git a/spec/support/supplemental_files_controller_examples.rb b/spec/support/supplemental_files_controller_examples.rb index eadf174aa5..a890957323 100644 --- a/spec/support/supplemental_files_controller_examples.rb +++ b/spec/support/supplemental_files_controller_examples.rb @@ -361,6 +361,23 @@ expect(JSON.parse(response.body)["errors"]).to be_present end end + + context 'throws error while saving' do + let(:supplemental_file) { FactoryBot.build(:supplemental_file) } + before do + allow(SupplementalFile).to receive(:new).and_return(supplemental_file) + allow(master_file).to receive(:save).and_return(false) + allow(controller).to receive(:fetch_object).and_return(master_file) + end + + it 'does not add a SupplementalFile to parent object' do + expect { + post :create, params: { class_id => object.id, supplemental_file: valid_create_attributes, format: :json }, session: valid_session + }.not_to change { master_file.reload.supplemental_files.size } + expect(response).to have_http_status(500) + expect(JSON.parse(response.body)["errors"]).to be_present + end + end end describe "PUT #update" do @@ -574,5 +591,19 @@ end end end + + context 'throws error while saving' do + let(:supplemental_file) { FactoryBot.create(:supplemental_file) } + before do + allow(master_file).to receive(:save).and_return(false) + allow(controller).to receive(:fetch_object).and_return(master_file) + end + + it 'returns a 500' do + delete :destroy, params: { class_id => object.id, id: supplemental_file.id, format: :json }, session: valid_session + expect(response).to have_http_status(500) + expect(JSON.parse(response.body)["errors"]).to be_present + end + end end end From eec53efeff8e64c0b30edf2a7cf3aa7c9a4c54de Mon Sep 17 00:00:00 2001 From: cjcolvar Date: Mon, 24 Jun 2024 15:51:47 -0400 Subject: [PATCH 097/152] Limit the number of stream tokens in a user session to avoid SessionOverflow errors --- app/models/stream_token.rb | 4 +++- spec/models/stream_token_spec.rb | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/app/models/stream_token.rb b/app/models/stream_token.rb index 3d15bbc5f2..f446941ec9 100644 --- a/app/models/stream_token.rb +++ b/app/models/stream_token.rb @@ -17,6 +17,8 @@ class StreamToken < ActiveRecord::Base class Unauthorized < Exception; end # attr_accessible :token, :target, :expires + class_attribute :max_tokens_per_user + self.max_tokens_per_user = 1000 def self.media_token(session) session[:hash_tokens] ||= [] @@ -75,7 +77,7 @@ def self.logout!(session) def self.purge_expired!(session) purged = expired.delete_all - session[:hash_tokens] = StreamToken.where(token: Array(session[:hash_tokens])).pluck(:token) + session[:hash_tokens] = StreamToken.where(token: Array(session[:hash_tokens])).order(expires: :desc).limit(max_tokens_per_user).pluck(:token) purged end diff --git a/spec/models/stream_token_spec.rb b/spec/models/stream_token_spec.rb index 163165bce4..ebf6c6e10b 100644 --- a/spec/models/stream_token_spec.rb +++ b/spec/models/stream_token_spec.rb @@ -131,5 +131,23 @@ expect(session[:hash_tokens]).not_to include(token) end end + + context 'with custom max_tokens_per_user' do + around do |example| + @previous_max_tokens_per_user = StreamToken.max_tokens_per_user + StreamToken.max_tokens_per_user = 10 + example.run + StreamToken.max_tokens_per_user = @previous_max_tokens_per_user + end + + it 'limits the number of tokens in the session' do + (1..10).each { |i| StreamToken.find_or_create_session_token(session, i.to_s) } + expect(session[:hash_tokens].size).to eq 11 + expect(session[:hash_tokens]).to include(token) + StreamToken.purge_expired!(session) + expect(session[:hash_tokens].size).to eq 10 + expect(session[:hash_tokens]).not_to include(token) + end + end end end From 51b57b2a8858087a20039d92907b636b0d33a05c Mon Sep 17 00:00:00 2001 From: cjcolvar Date: Mon, 24 Jun 2024 17:00:21 -0400 Subject: [PATCH 098/152] Bump session data to mediumtext and token limit to 2000 --- app/models/stream_token.rb | 2 +- .../20240624204921_change_sessions_data_to_medium_text.rb | 5 +++++ db/schema.rb | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 db/migrate/20240624204921_change_sessions_data_to_medium_text.rb diff --git a/app/models/stream_token.rb b/app/models/stream_token.rb index f446941ec9..1837725f6d 100644 --- a/app/models/stream_token.rb +++ b/app/models/stream_token.rb @@ -18,7 +18,7 @@ class Unauthorized < Exception; end # attr_accessible :token, :target, :expires class_attribute :max_tokens_per_user - self.max_tokens_per_user = 1000 + self.max_tokens_per_user = 2000 def self.media_token(session) session[:hash_tokens] ||= [] diff --git a/db/migrate/20240624204921_change_sessions_data_to_medium_text.rb b/db/migrate/20240624204921_change_sessions_data_to_medium_text.rb new file mode 100644 index 0000000000..27337a53b7 --- /dev/null +++ b/db/migrate/20240624204921_change_sessions_data_to_medium_text.rb @@ -0,0 +1,5 @@ +class ChangeSessionsDataToMediumText < ActiveRecord::Migration[7.0] + def change + change_column :sessions, :data, :text, limit: 16777215 + end +end diff --git a/db/schema.rb b/db/schema.rb index 96885f150b..fba55dcffe 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[7.0].define(version: 2024_05_31_201828) do +ActiveRecord::Schema[7.0].define(version: 2024_06_24_204921) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" From 0809dbdeb233e21cac20c672cdaea0be7c938ca4 Mon Sep 17 00:00:00 2001 From: cjcolvar Date: Tue, 25 Jun 2024 14:10:56 -0400 Subject: [PATCH 099/152] Feedback from review --- spec/models/stream_token_spec.rb | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/spec/models/stream_token_spec.rb b/spec/models/stream_token_spec.rb index ebf6c6e10b..a4335806bd 100644 --- a/spec/models/stream_token_spec.rb +++ b/spec/models/stream_token_spec.rb @@ -133,11 +133,8 @@ end context 'with custom max_tokens_per_user' do - around do |example| - @previous_max_tokens_per_user = StreamToken.max_tokens_per_user - StreamToken.max_tokens_per_user = 10 - example.run - StreamToken.max_tokens_per_user = @previous_max_tokens_per_user + before do + allow(StreamToken).to receive(:max_tokens_per_user).and_return(10) end it 'limits the number of tokens in the session' do From 8fcae0f0b581786fc85cd88809e5e4b720c74211 Mon Sep 17 00:00:00 2001 From: Chris Colvard Date: Wed, 26 Jun 2024 17:04:04 -0400 Subject: [PATCH 100/152] Return all sections and all transcripts when generating hit count fields --- app/models/search_builder.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/models/search_builder.rb b/app/models/search_builder.rb index bf823cb468..fbefdb8707 100644 --- a/app/models/search_builder.rb +++ b/app/models/search_builder.rb @@ -70,6 +70,7 @@ def term_frequency_counts(solr_parameters) fl << "sections:[subquery]" solr_parameters["sections.q"] = "{!terms f=isPartOf_ssim v=$row.id}" solr_parameters["sections.defType"] = "lucene" + solr_parameters["sections.rows"] = 1_000_000 sections_fl = ['id'] transcripts_fl = ['id'] if transcripts_present @@ -89,6 +90,7 @@ def term_frequency_counts(solr_parameters) solr_parameters["sections.fl"] = sections_fl.join(',') solr_parameters["sections.transcripts.fl"] = transcripts_fl.join(',') solr_parameters["sections.transcripts.defType"] = "lucene" + solr_parameters["sections.transcripts.rows"] = 1_000_000 solr_parameters["sections.transcripts.q"] = "{!terms f=isPartOf_ssim v=$row.id}{!join to=id from=isPartOf_ssim}" end end From a45b12effdd9c59fce51b5468080e314a7203dc1 Mon Sep 17 00:00:00 2001 From: dwithana Date: Thu, 27 Jun 2024 12:07:36 -0400 Subject: [PATCH 101/152] New Ramp build --- package.json | 2 +- yarn.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index ce260e76ed..32ececdad1 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "@babel/plugin-proposal-object-rest-spread": "^7.20.7", "@babel/preset-react": "^7.0.0", "@babel/runtime": "7", - "@samvera/ramp": "https://github.com/samvera-labs/ramp.git", + "@samvera/ramp": "https://github.com/samvera-labs/ramp.git#07c050224c11070a008c0db804c9fa169caa47ca", "babel-plugin-macros": "^3.1.0", "babel-plugin-transform-react-remove-prop-types": "^0.4.24", "buffer": "^6.0.3", diff --git a/yarn.lock b/yarn.lock index 0421754ed6..111a788aeb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1477,9 +1477,9 @@ estree-walker "^2.0.2" picomatch "^2.3.1" -"@samvera/ramp@https://github.com/samvera-labs/ramp.git": +"@samvera/ramp@https://github.com/samvera-labs/ramp.git#07c050224c11070a008c0db804c9fa169caa47ca": version "3.1.2" - resolved "https://github.com/samvera-labs/ramp.git#6ba3a206ac7bbda3682d70c6bdb53c709c36b29f" + resolved "https://github.com/samvera-labs/ramp.git#07c050224c11070a008c0db804c9fa169caa47ca" dependencies: "@rollup/plugin-json" "^6.0.1" "@silvermine/videojs-quality-selector" "^1.3.1" From e5730d067e2d9283268f2bebf734cdc246ab9691 Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Thu, 27 Jun 2024 12:55:32 -0400 Subject: [PATCH 102/152] Make download file path construction configurable --- app/controllers/master_files_controller.rb | 21 +------ app/models/derivative.rb | 4 ++ app/presenters/speedy_af/proxy/derivative.rb | 4 ++ app/services/file_locator.rb | 17 +----- lib/avalon/configuration.rb | 59 ++++++++++++++++++ .../master_files_controller_spec.rb | 16 ++--- spec/models/derivative_spec.rb | 60 +++++++++++++++++++ 7 files changed, 140 insertions(+), 41 deletions(-) diff --git a/app/controllers/master_files_controller.rb b/app/controllers/master_files_controller.rb index a2e1d16869..1589b5d0ca 100644 --- a/app/controllers/master_files_controller.rb +++ b/app/controllers/master_files_controller.rb @@ -328,7 +328,8 @@ def download_derivative authorize! :download, @master_file begin - path = derivative_path + high_deriv = @master_file.derivatives.find { |deriv| deriv.quality == 'high' } + path = high_deriv.download_path unless FileLocator.new(path).exist? flash[:error] = "Unable to find or access derivative file." @@ -423,24 +424,6 @@ def samples_per_frame Settings.waveform.sample_rate * Settings.waveform.finest_zoom / Settings.waveform.player_width end - def derivative_path - derivative = @master_file.derivatives.find { |d| d.quality == "high" } - # There is no guarantee that the full path contained in the absolute_location attribute is accurate. - # However, the sub-path in location_url should be consistent between server moves. The location_url - # does not include the extension, so we retrieve the extension from absolute_location. - extension = File.extname(derivative.absolute_location) - location = derivative.location_url + extension - # Derivative files that have been moved from their original location/server should have been moved into - # the new root path for derivatives/encodings. Combining the subpath and extension from the existing - # record with the derivative path defined in our environment variables, we should be able to retrieve the - # derivative files, regardless of how many times they have been rehomed. - if Settings.encoding.derivative_bucket - File.join('s3://', Settings.encoding.derivative_bucket, location) - else - File.join(ENV["ENCODE_WORK_DIR"], location).to_s - end - end - private def search_response_json diff --git a/app/models/derivative.rb b/app/models/derivative.rb index a39d22b5b7..d6d7f845fd 100644 --- a/app/models/derivative.rb +++ b/app/models/derivative.rb @@ -130,6 +130,10 @@ def bitrate audio_bitrate.to_i + video_bitrate.to_i end + def download_path + Avalon::Configuration.construct_download_path.call(self) + end + private def delete_file! diff --git a/app/presenters/speedy_af/proxy/derivative.rb b/app/presenters/speedy_af/proxy/derivative.rb index e51eda0628..7c847f889b 100644 --- a/app/presenters/speedy_af/proxy/derivative.rb +++ b/app/presenters/speedy_af/proxy/derivative.rb @@ -18,4 +18,8 @@ class SpeedyAF::Proxy::Derivative < SpeedyAF::Base def bitrate audio_bitrate.to_i + video_bitrate.to_i end + + def download_path + Avalon::Configuration.construct_download_path.call(self) + end end diff --git a/app/services/file_locator.rb b/app/services/file_locator.rb index 2b7db4077f..6b6740c079 100644 --- a/app/services/file_locator.rb +++ b/app/services/file_locator.rb @@ -40,21 +40,8 @@ def local_file end def download_url - # Minio: - # Because the request to the generated URL will be coming from an external context, we need - # to generate the presigned URL with a publicly accessible endpoint. To accomplish this, we - # create a new client definition using the public_host address and use that for access to the - # object. - if Settings.minio.present? && Settings.minio.public_host.present? - client = Aws::S3::Client.new(endpoint: Settings.minio.public_host, - access_key_id: Settings.minio.access, - secret_access_key: Settings.minio.secret, - region: ENV["AWS_REGION"]) - download_object = Aws::S3::Object.new(bucket_name: bucket, key: key, client: client) - end - # Other AWS implementations should be fine with the default client so we can use the default object. - download_object ||= object - # Presigned URL is set to expire in 1 hour. Is that too long? + download_object = Avalon::Configuration.construct_s3_download_object.call(bucket, key, object) + # Presigned URL is set to expire in 1 hour. download_object.presigned_url(:get, expires_in: 3600, response_content_disposition: "attachment; filename=#{File.basename(key)}") end end diff --git a/lib/avalon/configuration.rb b/lib/avalon/configuration.rb index 2897301774..d83a7f2945 100644 --- a/lib/avalon/configuration.rb +++ b/lib/avalon/configuration.rb @@ -35,6 +35,65 @@ def sanitize_filename end end + attr_writer :construct_download_path + def construct_download_path + @construct_download_path ||= lambda do |derivative| + url = derivative.hls_url + http_base = Settings.streaming.http_base + + # `Settings.streaming.server` returns symbols in testing environment + # but strings in dev/prod. Explicitly call to_sym for consistency. + location = case Settings.streaming.server.to_sym + # HLS Url templates can be found in config/url_handlers.yml + when :generic, :adobe + url.gsub(/(?:#{Regexp.escape(http_base)}\/)(?:audio-only\/)?(.*)(?:\.m3u8)/, '\1') + when :nginx + url.gsub(/(?:#{Regexp.escape(Settings.streaming.http_base)}\/)(.*)(?:\/index\.m3u8)/, '\1') + when :wowza + # Wowza HLS urls include the extension between the base and relative path. + # "http_base/extension:path/filename.extension/playlist.m3u8" + # (?:.*?:) is a non-capturing group that will non-greedily match + # any character until the first colon. This removes the extension from + # the middle of the path. + url.gsub(/(?:#{Regexp.escape(Settings.streaming.http_base)}\/)(?:.*?:)(.*)(?:\/playlist.m3u8)/, '\1') + end + + # Derivative files that have been moved from their original location/server should have been moved into + # the new root path for derivatives/encodings. Combining the subpath from the existing record with the + # derivative path defined in our environment variables, we should be able to retrieve the derivative files, + # regardless of how many times they have been rehomed. + if Settings.encoding.derivative_bucket + File.join('s3://', Settings.encoding.derivative_bucket, location) + else + File.join(ENV["ENCODE_WORK_DIR"], location).to_s + end + end + end + + attr_writer :construct_s3_download_object + def construct_s3_download_object + # Certain S3 implementations will require special handling to generate a usable download object. + @construct_s3_download_object ||= lambda do |bucket, key, object| + # Minio: + # Because the request to the generated URL will be coming from an external context, we need + # to generate the presigned URL with a publicly accessible endpoint. To accomplish this, we + # create a new client definition using the public_host address and use that for access to the + # object. + if Settings.minio.present? && Settings.minio.public_host.present? + client = Aws::S3::Client.new(endpoint: Settings.minio.public_host, + access_key_id: Settings.minio.access, + secret_access_key: Settings.minio.secret, + region: ENV["AWS_REGION"]) + download_object = Aws::S3::Object.new(bucket_name: bucket, key: key, client: client) + end + # Most S3 implementations should be fine with the default client so we can simply + # return the original object + download_object ||= object + + download_object + end + end + # To be called as Avalon::Configuration.controlled_digital_lending_enabled? def controlled_digital_lending_enabled? !!Settings.controlled_digital_lending&.enable diff --git a/spec/controllers/master_files_controller_spec.rb b/spec/controllers/master_files_controller_spec.rb index a742861d44..cebbcec7c7 100644 --- a/spec/controllers/master_files_controller_spec.rb +++ b/spec/controllers/master_files_controller_spec.rb @@ -759,18 +759,17 @@ class << file let(:collection) { FactoryBot.create(:collection) } let(:media_object) { FactoryBot.create(:media_object, collection: collection)} let(:master_file) { FactoryBot.create(:master_file, media_object: media_object, derivatives: [high_derivative, med_derivative]) } - let(:high_derivative) { FactoryBot.create(:derivative, absolute_location: Rails.root.join('spec', 'fixtures', 'meow.wav').to_s) } - let(:med_derivative) { FactoryBot.create(:derivative, quality: 'medium') } + let(:high_derivative) { FactoryBot.create(:derivative, absolute_location: "/fixtures/meow.wav") } + let(:med_derivative) { FactoryBot.create(:derivative, quality: 'medium', absolute_location: "/fixtures/meow-medium.wav") } before do allow(Settings.derivative).to receive(:allow_download).and_return(true) + allow(Settings.streaming).to receive(:content_path).and_return('/') login_user collection.managers.first end it 'should download the high quality derivative' do allow(ENV).to receive(:[]).and_call_original allow(ENV).to receive(:[]).with("ENCODE_WORK_DIR").and_return(Rails.root.join('spec').to_s) - allow(high_derivative).to receive(:location_url).and_return('/fixtures/meow') - allow(med_derivative).to receive(:location_url).and_return('/fixtures/meow-medium') get :download_derivative, params: { id: master_file.id } expect(response.header['Content-Disposition']).to include 'attachment; filename="meow.wav"' expect(response.header['Content-Disposition']).to_not include 'filename="meow-medium.wav"' @@ -779,7 +778,7 @@ class << file it 'should display a flash message if file is not found' do allow(ENV).to receive(:[]).and_call_original allow(ENV).to receive(:[]).with("ENCODE_WORK_DIR").and_return(Rails.root.join('spec').to_s) - allow(high_derivative).to receive(:location_url).and_return('missing') + allow(high_derivative).to receive(:hls_url).and_return('missing') get :download_derivative, params: { id: master_file.id } expect(response).to redirect_to(edit_media_object_path(media_object)) expect(flash[:error]).to be_present @@ -795,6 +794,11 @@ class << file end context 's3 file' do + # Settings.encoding does not have a `derivative_bucket` method in + # the default configuration so we have to use a full double rather + # than just stubbing the method in these tests. RSpec also throws + # an error when using a double within an `around` block so we + # explicitly use a `before...after` pattern here. before do @encoding_backup = Settings.encoding Settings.encoding = double("encoding", engine_adapter: 'test', derivative_bucket: 'mybucket') @@ -803,7 +807,6 @@ class << file it 'should redirect to a presigned url for AWS' do allow(ENV).to receive(:[]).and_call_original allow(ENV).to receive(:[]).with('AWS_REGION').and_return('us-east-2') - allow(high_derivative).to receive(:location_url).and_return('/fixtures/meow') get :download_derivative, params: { id: master_file.id } expect(response.status).to eq 302 expect(response.location).to include('s3.us-stubbed-1.amazonaws.com', 'X-Amz-Algorithm', 'X-Amz-Credential', 'X-Amz-Expires', 'X-Amz-SignedHeaders', 'X-Amz-Signature') @@ -818,7 +821,6 @@ class << file it 'should redirect to restricted page' do allow(ENV).to receive(:[]).and_call_original allow(ENV).to receive(:[]).with("ENCODE_WORK_DIR").and_return(Rails.root.join('spec').to_s) - allow(high_derivative).to receive(:location_url).and_return('/fixtures/meow') login_as :user expect(get :download_derivative, params: { id: master_file.id }).to render_template 'errors/restricted_pid' end diff --git a/spec/models/derivative_spec.rb b/spec/models/derivative_spec.rb index 02b23fb4ec..dc33f9d0cd 100644 --- a/spec/models/derivative_spec.rb +++ b/spec/models/derivative_spec.rb @@ -114,4 +114,64 @@ end end end + + describe "#download_path" do + subject { described_class.new } + before :each do + allow(ENV).to receive(:[]).and_call_original + allow(ENV).to receive(:[]).with("ENCODE_WORK_DIR").and_return("file://") + allow(subject).to receive(:hls_url).and_return(hls_url) + end + + describe "generic" do + let(:hls_url) { "http://localhost:3000/streams/6f69c008-06a4-4bad-bb60-26297f0b4c06/35bddaa0-fbb4-404f-ab76-58f22921529c/warning.mp4.m3u8" } + it "provides a file path" do + allow(Settings.streaming).to receive(:server).and_return(:generic) + expect(subject.download_path).to eq "file://6f69c008-06a4-4bad-bb60-26297f0b4c06/35bddaa0-fbb4-404f-ab76-58f22921529c/warning.mp4" + end + end + + describe "adobe" do + let(:hls_url) { "http://localhost:3000/streams/audio-only/6f69c008-06a4-4bad-bb60-26297f0b4c06/35bddaa0-fbb4-404f-ab76-58f22921529c/warning.mp4.m3u8" } + it "provides a file path" do + allow(Settings.streaming).to receive(:server).and_return(:adobe) + expect(subject.download_path).to eq "file://6f69c008-06a4-4bad-bb60-26297f0b4c06/35bddaa0-fbb4-404f-ab76-58f22921529c/warning.mp4" + end + end + + describe "wowza" do + let(:hls_url) { "http://localhost:3000/streams/mp4:6f69c008-06a4-4bad-bb60-26297f0b4c06/35bddaa0-fbb4-404f-ab76-58f22921529c/warning.mp4/playlist.m3u8" } + it "provides a file path" do + allow(Settings.streaming).to receive(:server).and_return(:wowza) + expect(subject.download_path).to eq "file://6f69c008-06a4-4bad-bb60-26297f0b4c06/35bddaa0-fbb4-404f-ab76-58f22921529c/warning.mp4" + end + end + + describe "nginx" do + let(:hls_url) { "http://localhost:3000/streams/6f69c008-06a4-4bad-bb60-26297f0b4c06/35bddaa0-fbb4-404f-ab76-58f22921529c/warning.mp4/index.m3u8" } + it "provides a file path" do + allow(Settings.streaming).to receive(:server).and_return(:nginx) + expect(subject.download_path).to eq "file://6f69c008-06a4-4bad-bb60-26297f0b4c06/35bddaa0-fbb4-404f-ab76-58f22921529c/warning.mp4" + end + end + + describe "s3" do + let(:hls_url) { "http://localhost:3000/streams/6f69c008-06a4-4bad-bb60-26297f0b4c06/35bddaa0-fbb4-404f-ab76-58f22921529c/warning.mp4.m3u8" } + before do + @encoding_backup = Settings.encoding + Settings.encoding = double("encoding", engine_adapter: 'test', derivative_bucket: 'mybucket') + + # We are using the generic format for hls url in this test, so we should explicitly define the generic streaming server + allow(Settings.streaming).to receive(:server).and_return(:generic) + end + + it "provides a s3 path" do + expect(subject.download_path).to eq "s3://mybucket/6f69c008-06a4-4bad-bb60-26297f0b4c06/35bddaa0-fbb4-404f-ab76-58f22921529c/warning.mp4" + end + + after do + Settings.encoding = @encoding_backup + end + end + end end From 4ab508c62ab07c0d7d7cf7cd3c8921b9df1f5816 Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Thu, 27 Jun 2024 15:09:02 -0400 Subject: [PATCH 103/152] Display default language for legacy files missing language field --- app/helpers/media_objects_helper.rb | 5 +++++ app/views/media_objects/_supplemental_files_list.html.erb | 8 ++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/app/helpers/media_objects_helper.rb b/app/helpers/media_objects_helper.rb index e7c3a093c4..bf0c6c255e 100644 --- a/app/helpers/media_objects_helper.rb +++ b/app/helpers/media_objects_helper.rb @@ -204,4 +204,9 @@ def gather_all_comments(media_object, sections) end.sort end.flatten.uniq end + + def display_supplemental_file_language(language) + return LanguageTerm.find(language).text if language.present? + Settings.caption_default.name + end end diff --git a/app/views/media_objects/_supplemental_files_list.html.erb b/app/views/media_objects/_supplemental_files_list.html.erb index 0e30ded934..39d8b3ce0f 100644 --- a/app/views/media_objects/_supplemental_files_list.html.erb +++ b/app/views/media_objects/_supplemental_files_list.html.erb @@ -41,7 +41,9 @@ Unless required by applicable law or agreed to in writing, software distributed
<% if tag == 'transcript' %>
- <%= form.text_field :language, id: "supplemental_file_language_#{section.id}_#{file.id}", value: LanguageTerm.find(file.language).text, + <%= form.text_field :language, + id: "supplemental_file_language_#{section.id}_#{file.id}", + value: display_supplemental_file_language(file.language), class: "typeahead from-model form-control", data: { model: 'languageTerm', validate: false } %>
@@ -56,7 +58,9 @@ Unless required by applicable law or agreed to in writing, software distributed <% end %> <% if tag == 'caption' %>
- <%= form.text_field :language, id: "supplemental_file_language_#{section.id}_#{file.id}", value: LanguageTerm.find(file.language).text, + <%= form.text_field :language, + id: "supplemental_file_language_#{section.id}_#{file.id}", + value: display_supplemental_file_language(file.language), class: "typeahead from-model form-control", data: { model: 'languageTerm', validate: false } %>
From aa8164d4598ea9ad29b024e3c70964ece915966e Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Thu, 27 Jun 2024 15:12:17 -0400 Subject: [PATCH 104/152] Fix formatting in MediaObjectHelper --- app/helpers/media_objects_helper.rb | 392 ++++++++++++++-------------- 1 file changed, 196 insertions(+), 196 deletions(-) diff --git a/app/helpers/media_objects_helper.rb b/app/helpers/media_objects_helper.rb index bf0c6c255e..04b0d60968 100644 --- a/app/helpers/media_objects_helper.rb +++ b/app/helpers/media_objects_helper.rb @@ -13,200 +13,200 @@ # --- END LICENSE_HEADER BLOCK --- module MediaObjectsHelper - # Quick and dirty solution to the problem of displaying the right template. - # Quick and dirty also gets it done faster. - def current_step_for(status=nil) - if status.nil? - status = HYDRANT_STEPS.first - end - - HYDRANT_STEPS.template(status) - end - - # Based on the current context it will choose which class should be - # applied to the display. If you are not using Twitter Bootstrap or - # want different defaults then change them here. - # - # The context here is the media_object you are working with. - def class_for_step(context, step) - css_class = case - # when context.workflow.current?(step) - # 'nav-info' - when context.workflow.completed?(step) - 'nav-success' - else 'nav-disabled' - end - - css_class - end - - def form_id_for_step(step) - "#{step.gsub('-','_')}_form" - end - - def dropbox_url collection - ic = Iconv.new('UTF-8//IGNORE', 'UTF-8') - path = Addressable::URI.escape_component(collection.dropbox_directory_name || "", %r{[/\\%& #]}) - url = File.join(Settings.dropbox.upload_uri, path) - ic.iconv(url) - end - - def combined_display_date media_object - (issued,created) = case media_object - when MediaObject, SpeedyAF::Proxy::MediaObject - [media_object.date_issued, media_object.date_created] - when Hash - [media_object[:document]['date_issued_ssi'], media_object[:document]['date_created_ssi']] - end - result = issued - result += " (Creation date: #{created})" if created.present? - result - end - - def display_other_identifiers media_object - # bibliographic_id has form [:type,"value"], other_identifier has form [[:type,"value],[:type,"value"],...] - ids = media_object.bibliographic_id.present? ? [media_object.bibliographic_id] : [] - ids += Array(media_object.other_identifier) - ids.uniq.collect{|i| "#{ ModsDocument::IDENTIFIER_TYPES[i[:source]] }: #{ i[:id] }" } - end - - def display_notes media_object - note_string = "" - note_types = ModsDocument::NOTE_TYPES.clone - note_types['table of contents']='Contents' - sorted_note_types = note_types.keys.sort - sorted_note_types.prepend(sorted_note_types.delete 'general') - sorted_note_types.each do |note_type| - notes = note_type == 'table of contents'? media_object.table_of_contents : gather_notes_of_type(media_object, note_type) - notes.each_with_index do |note, i| - note_string += "

#{note_types[note_type]}

" if i==0 and note_type!='general' - note_string += "
#{note}
" - end - end - note_string - end - - def gather_notes_of_type media_object, type - media_object.note.present? ? media_object.note.select{|n| n[:type]==type}.collect{|n|n[:note]} : [] - end - - def display_collection(media_object) - link_to(media_object.collection.name, collection_path(media_object.collection.id)) - end - - def display_unit(media_object) - link_to(media_object.collection.unit, collections_path(filter: media_object.collection.unit)) - end - - def display_language media_object - media_object.language.collect{|l|l[:text]}.uniq - end - - def display_related_item media_object - media_object.related_item_url.collect{ |r| link_to( r[:label], r[:url]) } - end - - def display_series media_object - media_object.series.collect { |s| link_to(s, blacklight_path({ "f[collection_ssim][]" => media_object.collection.name, "f[series_ssim][]" => s }))} - end - - def display_rights_statement media_object - return nil unless media_object.rights_statement.present? - label = ModsDocument::RIGHTS_STATEMENTS[media_object.rights_statement] - return nil unless label.present? - link = link_to label, media_object.rights_statement, target: '_blank' - content_tag(:dt, 'Rights Statement') + content_tag(:dd) { link } - 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]} - available_qualities.uniq! - quality ||= session[:quality] if session['quality'].present? && available_qualities.include?(session[:quality]) - quality ||= Settings.streaming.default_quality if available_qualities.include?(Settings.streaming.default_quality) - quality ||= available_qualities.first - quality - end - - def is_current_section? section - @currentStream && ( section.id == @currentStream.id ) - end - - def show_progress?(sections) - encode_gids = sections.collect { |mf| "gid://ActiveEncode/#{mf.encoder_class}/#{mf.workflow_id}" } - ActiveEncode::EncodeRecord.where(global_id: encode_gids).any? { |encode| encode.state.to_s.upcase != 'COMPLETED' } - end - - def any_failed?(sections) - encode_gids = sections.collect { |mf| "gid://ActiveEncode/#{mf.encoder_class}/#{mf.workflow_id}" } - ActiveEncode::EncodeRecord.where(global_id: encode_gids).any? { |encode| encode.state.to_s.upcase == 'FAILED' } - end - - def parse_section section, node, index - sectionnode = section.structuralMetadata.xpath('//Item') - if sectionnode.children.present? - tracknumber = 0 - contents = '' - sectionnode.children.each do |node| - next if node.blank? - st, tracknumber = parse_node section, node, tracknumber - contents+=st - end - else - contents, tracknumber = parse_node section, sectionnode.first, index - end - return contents, tracknumber - end - - def parse_node section, node, tracknumber - if node.name.upcase=="DIV" - contents = '' - node.children.each do |n| - next if n.blank? - nodecontent, tracknumber = parse_node section, n, tracknumber - contents+=nodecontent - end - return "
  • #{node.attribute('label')}
    • #{contents}
  • ", tracknumber - elsif ['SPAN','ITEM'].include? node.name.upcase - tracknumber += 1 - start, stop = get_xml_media_fragment node, section - label = "#{tracknumber}. #{node.attribute('label').value} (#{get_duration_from_fragment(start, stop)})" - native_url = "#{id_section_media_object_path(@media_object, section.id)}?t=#{start},#{stop}" - url = "#{share_link_for( section )}?t=#{start},#{stop}" - segment_id = "#{section.id}-#{tracknumber}" - data = {segment: section.id, is_video: section.file_format != 'Sound', native_url: native_url, fragmentbegin: start, fragmentend: stop} - link = link_to label.html_safe, url, id: segment_id, data: data, class: 'playable structure wrap' - return "
  • #{link}
  • ", tracknumber - end - end - - def get_xml_media_fragment node, section - start = node.attribute('begin').present? ? node.attribute('begin').value : 0 - stop = node.attribute('end').present? ? node.attribute('end').value : section.duration.blank? ? 0 : milliseconds_to_formatted_time(section.duration.to_i) - parse_media_fragment "#{start},#{stop}" - end - - def get_duration node, section - start,stop = get_xml_media_fragment node, section - milliseconds_to_formatted_time((stop.to_i - start.to_i) * 1000, false) - end - - def get_duration_from_fragment(start, stop) - milliseconds_to_formatted_time((stop.to_i - start.to_i) * 1000, false) - end - - # This method mirrors the one in the MediaObject model but makes use of the sections passed in which can be SpeedyAF Objects - # This would be good to refactor in the future but speeds things up considerably for now - def gather_all_comments(media_object, sections) - media_object.comment.sort + sections.collect do |mf| - mf.comment.reject(&:blank?).collect do |c| - mf.display_title.present? ? "[#{mf.display_title}] #{c}" : c - end.sort - end.flatten.uniq - end - - def display_supplemental_file_language(language) - return LanguageTerm.find(language).text if language.present? - Settings.caption_default.name - end + # Quick and dirty solution to the problem of displaying the right template. + # Quick and dirty also gets it done faster. + def current_step_for(status=nil) + if status.nil? + status = HYDRANT_STEPS.first + end + + HYDRANT_STEPS.template(status) + end + + # Based on the current context it will choose which class should be + # applied to the display. If you are not using Twitter Bootstrap or + # want different defaults then change them here. + # + # The context here is the media_object you are working with. + def class_for_step(context, step) + css_class = case + # when context.workflow.current?(step) + # 'nav-info' + when context.workflow.completed?(step) + 'nav-success' + else 'nav-disabled' + end + + css_class + end + + def form_id_for_step(step) + "#{step.gsub('-','_')}_form" + end + + def dropbox_url collection + ic = Iconv.new('UTF-8//IGNORE', 'UTF-8') + path = Addressable::URI.escape_component(collection.dropbox_directory_name || "", %r{[/\\%& #]}) + url = File.join(Settings.dropbox.upload_uri, path) + ic.iconv(url) + end + + def combined_display_date media_object + (issued,created) = case media_object + when MediaObject, SpeedyAF::Proxy::MediaObject + [media_object.date_issued, media_object.date_created] + when Hash + [media_object[:document]['date_issued_ssi'], media_object[:document]['date_created_ssi']] + end + result = issued + result += " (Creation date: #{created})" if created.present? + result + end + + def display_other_identifiers media_object + # bibliographic_id has form [:type,"value"], other_identifier has form [[:type,"value],[:type,"value"],...] + ids = media_object.bibliographic_id.present? ? [media_object.bibliographic_id] : [] + ids += Array(media_object.other_identifier) + ids.uniq.collect{|i| "#{ ModsDocument::IDENTIFIER_TYPES[i[:source]] }: #{ i[:id] }" } + end + + def display_notes media_object + note_string = "" + note_types = ModsDocument::NOTE_TYPES.clone + note_types['table of contents']='Contents' + sorted_note_types = note_types.keys.sort + sorted_note_types.prepend(sorted_note_types.delete 'general') + sorted_note_types.each do |note_type| + notes = note_type == 'table of contents'? media_object.table_of_contents : gather_notes_of_type(media_object, note_type) + notes.each_with_index do |note, i| + note_string += "

    #{note_types[note_type]}

    " if i==0 and note_type!='general' + note_string += "
    #{note}
    " + end + end + note_string + end + + def gather_notes_of_type media_object, type + media_object.note.present? ? media_object.note.select{|n| n[:type]==type}.collect{|n|n[:note]} : [] + end + + def display_collection(media_object) + link_to(media_object.collection.name, collection_path(media_object.collection.id)) + end + + def display_unit(media_object) + link_to(media_object.collection.unit, collections_path(filter: media_object.collection.unit)) + end + + def display_language media_object + media_object.language.collect{|l|l[:text]}.uniq + end + + def display_related_item media_object + media_object.related_item_url.collect{ |r| link_to( r[:label], r[:url]) } + end + + def display_series media_object + media_object.series.collect { |s| link_to(s, blacklight_path({ "f[collection_ssim][]" => media_object.collection.name, "f[series_ssim][]" => s }))} + end + + def display_rights_statement media_object + return nil unless media_object.rights_statement.present? + label = ModsDocument::RIGHTS_STATEMENTS[media_object.rights_statement] + return nil unless label.present? + link = link_to label, media_object.rights_statement, target: '_blank' + content_tag(:dt, 'Rights Statement') + content_tag(:dd) { link } + 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]} + available_qualities.uniq! + quality ||= session[:quality] if session['quality'].present? && available_qualities.include?(session[:quality]) + quality ||= Settings.streaming.default_quality if available_qualities.include?(Settings.streaming.default_quality) + quality ||= available_qualities.first + quality + end + + def is_current_section? section + @currentStream && ( section.id == @currentStream.id ) + end + + def show_progress?(sections) + encode_gids = sections.collect { |mf| "gid://ActiveEncode/#{mf.encoder_class}/#{mf.workflow_id}" } + ActiveEncode::EncodeRecord.where(global_id: encode_gids).any? { |encode| encode.state.to_s.upcase != 'COMPLETED' } + end + + def any_failed?(sections) + encode_gids = sections.collect { |mf| "gid://ActiveEncode/#{mf.encoder_class}/#{mf.workflow_id}" } + ActiveEncode::EncodeRecord.where(global_id: encode_gids).any? { |encode| encode.state.to_s.upcase == 'FAILED' } + end + + def parse_section section, node, index + sectionnode = section.structuralMetadata.xpath('//Item') + if sectionnode.children.present? + tracknumber = 0 + contents = '' + sectionnode.children.each do |node| + next if node.blank? + st, tracknumber = parse_node section, node, tracknumber + contents+=st + end + else + contents, tracknumber = parse_node section, sectionnode.first, index + end + return contents, tracknumber + end + + def parse_node section, node, tracknumber + if node.name.upcase=="DIV" + contents = '' + node.children.each do |n| + next if n.blank? + nodecontent, tracknumber = parse_node section, n, tracknumber + contents+=nodecontent + end + return "
  • #{node.attribute('label')}
    • #{contents}
  • ", tracknumber + elsif ['SPAN','ITEM'].include? node.name.upcase + tracknumber += 1 + start, stop = get_xml_media_fragment node, section + label = "#{tracknumber}. #{node.attribute('label').value} (#{get_duration_from_fragment(start, stop)})" + native_url = "#{id_section_media_object_path(@media_object, section.id)}?t=#{start},#{stop}" + url = "#{share_link_for( section )}?t=#{start},#{stop}" + segment_id = "#{section.id}-#{tracknumber}" + data = {segment: section.id, is_video: section.file_format != 'Sound', native_url: native_url, fragmentbegin: start, fragmentend: stop} + link = link_to label.html_safe, url, id: segment_id, data: data, class: 'playable structure wrap' + return "
  • #{link}
  • ", tracknumber + end + end + + def get_xml_media_fragment node, section + start = node.attribute('begin').present? ? node.attribute('begin').value : 0 + stop = node.attribute('end').present? ? node.attribute('end').value : section.duration.blank? ? 0 : milliseconds_to_formatted_time(section.duration.to_i) + parse_media_fragment "#{start},#{stop}" + end + + def get_duration node, section + start,stop = get_xml_media_fragment node, section + milliseconds_to_formatted_time((stop.to_i - start.to_i) * 1000, false) + end + + def get_duration_from_fragment(start, stop) + milliseconds_to_formatted_time((stop.to_i - start.to_i) * 1000, false) + end + + # This method mirrors the one in the MediaObject model but makes use of the sections passed in which can be SpeedyAF Objects + # This would be good to refactor in the future but speeds things up considerably for now + def gather_all_comments(media_object, sections) + media_object.comment.sort + sections.collect do |mf| + mf.comment.reject(&:blank?).collect do |c| + mf.display_title.present? ? "[#{mf.display_title}] #{c}" : c + end.sort + end.flatten.uniq + end + + def display_supplemental_file_language(language) + return LanguageTerm.find(language).text if language.present? + Settings.caption_default.name + end end From eee3b1c85353c6c6a5afabfbfa78b34393238cb5 Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Fri, 28 Jun 2024 11:53:11 -0400 Subject: [PATCH 105/152] Improve handling of timecues in transcript parser Malformed time cues would cause the transcript search to bomb because the regex to separate timed text cues would fail to match. We are loosening the regex to match timecues of the form `h:mm:ss.ttt` and also providing a failure path so that if the timed text cannot be separated properly, the search will still return matches. It will just be the entire cue instead of just the text with a time fragment anchored target. --- lib/avalon/transcript_parser.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/avalon/transcript_parser.rb b/lib/avalon/transcript_parser.rb index 1ed09e90db..18b7bdeb54 100644 --- a/lib/avalon/transcript_parser.rb +++ b/lib/avalon/transcript_parser.rb @@ -48,7 +48,8 @@ def normalized_text # Separate time cue and text content from a single line of timed text to facilitate result formatting of transcript searches. def self.extract_single_time_cue(timed_text) - split_text = timed_text.match(/(\d{2,}*:?\d{2}:\d{2}\.?,?\d{3} --> \d{2,}*:?\d{2}:\d{2}\.?,?\d{3})(.*)/) + split_text = timed_text.match(/(\d{0,}:?\d{2}:\d{2}\.?,?\d{3} --> \d{0,}:?\d{2}:\d{2}\.?,?\d{3})(.*)/) + return [nil, nil] if split_text.blank? time_cue = split_text[1].sub(',', '.').gsub(/\s-->\s/, ',') text = split_text[2].strip [time_cue, text] From 23bc08e87029d9123fc90e60586de450f59d8cc0 Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Mon, 1 Jul 2024 10:48:44 -0400 Subject: [PATCH 106/152] Fix playlist description display issues This commit limits the height of the description section so that it does not overwhelm the playlist items section and does not extend past the bottom of the page. With limiting the height we also enable scrolling of the description when it overflows and mimic the structured nav "scroll to see more" message. --- app/javascript/components/PlaylistRamp.jsx | 45 +++++++++++++++++----- app/javascript/components/Ramp.scss | 31 +++++++++++++++ 2 files changed, 66 insertions(+), 10 deletions(-) diff --git a/app/javascript/components/PlaylistRamp.jsx b/app/javascript/components/PlaylistRamp.jsx index 6ad73df35c..92d43d6d36 100644 --- a/app/javascript/components/PlaylistRamp.jsx +++ b/app/javascript/components/PlaylistRamp.jsx @@ -133,6 +133,26 @@ const Ramp = ({ setExpanded(!expanded); }; + // Update scrolling indicators when end of scrolling has been reached + const handleScrollableDescription = (e) => { + let elem = e.target; + const scrollMsg = elem.nextSibling; + const structureEnd = Math.abs(elem.scrollHeight - (elem.scrollTop + elem.clientHeight)) <= 1; + + if (scrollMsg && structureEnd && scrollMsg.classList.contains('scrollable')) { + scrollMsg.classList.remove('scrollable'); + } else if (scrollMsg && !structureEnd && !scrollMsg.classList.contains('scrollable')) { + scrollMsg.classList.add('scrollable'); + } + }; + + // Update scrolling indicators when page is resized + const resizeObserver = new ResizeObserver(entries => { + for (let entry of entries) { + handleScrollableDescription(entry); + } + }); + return ( -
    + {comment && ( -
    +

    {comment_label}

    -
    +
    - {words.length > wordCount && ( - - Show {expanded ? 'less' : 'more'} - - )}
    + {expanded && ( +
    + Scroll to see more +
    + )} + {words.length > wordCount && ( + + Show {expanded ? 'less' : 'more'} + + )}
    )} {tags && ( -
    +

    Tags

    )} -
    + {playlist_item_ids?.length > 0 && (

    Playlist Items

    diff --git a/app/javascript/components/Ramp.scss b/app/javascript/components/Ramp.scss index fb47b8101e..8e299ea400 100644 --- a/app/javascript/components/Ramp.scss +++ b/app/javascript/components/Ramp.scss @@ -360,6 +360,37 @@ flex-basis: auto; height: 70vh; + .ramp--playlist-description { + overflow: auto; + max-height: 25vh; + } + + // Scroll to see more message + .ramp--playlist-description-scroll { + display: none; + } + + .ramp--playlist-description-scroll.scrollable { + background: #bbbbbb; + text-align: center; + display: block; + position: absolute; + color: black; + font-size: 13px; + width: fit-content; + bottom: 0; + left: 35.5%; + border: 1px solid #ddd; + border-radius: 0.25rem 0.25rem 0 0; + border-bottom: none; + padding: 0.25em; + transform: translateY(-1.5rem); + + @media (min-width: 585px) and (max-width: 768px) { + left: 25%; + } + } + .ramp--structured-nav__border { margin-top: 0; overflow: auto; From dc2d2d5dea2259aa76c914a07d2085a29b0b20c0 Mon Sep 17 00:00:00 2001 From: dwithana Date: Mon, 1 Jul 2024 16:34:43 -0400 Subject: [PATCH 107/152] Add iiif logo to the link display in share --- app/assets/images/iiif-logo.svg | 34 +++++++++++++++++++ app/javascript/components/Ramp.scss | 4 +++ .../media_objects/_share_resource.html.erb | 2 +- 3 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 app/assets/images/iiif-logo.svg diff --git a/app/assets/images/iiif-logo.svg b/app/assets/images/iiif-logo.svg new file mode 100644 index 0000000000..529ba51ee9 --- /dev/null +++ b/app/assets/images/iiif-logo.svg @@ -0,0 +1,34 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + diff --git a/app/javascript/components/Ramp.scss b/app/javascript/components/Ramp.scss index fb47b8101e..a6c2119fb7 100644 --- a/app/javascript/components/Ramp.scss +++ b/app/javascript/components/Ramp.scss @@ -198,6 +198,10 @@ } } + .iiif-manifest-logo { + width: 1.5em; + } + @media (max-width: 585px) { .ramp--tabs-panel { padding-top: 1rem; diff --git a/app/views/media_objects/_share_resource.html.erb b/app/views/media_objects/_share_resource.html.erb index db6e2ef1fd..814dc791ec 100644 --- a/app/views/media_objects/_share_resource.html.erb +++ b/app/views/media_objects/_share_resource.html.erb @@ -30,7 +30,7 @@ Unless required by applicable law or agreed to in writing, software distributed <% end %>
    - +
    From 6e0e12242aaeb0bae8ef1bcc9708ecd8bb4f8fe5 Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Wed, 3 Jul 2024 14:11:05 -0400 Subject: [PATCH 108/152] Remove transcripts from indexes --- .../supplemental_files_controller.rb | 1 + .../concerns/supplemental_file_behavior.rb | 15 ++++++- app/models/supplemental_file.rb | 5 +++ spec/models/supplemental_file_spec.rb | 32 +++++++++++++ .../supplemental_files_controller_examples.rb | 45 +++++++++++++++++++ 5 files changed, 97 insertions(+), 1 deletion(-) diff --git a/app/controllers/supplemental_files_controller.rb b/app/controllers/supplemental_files_controller.rb index a37de63b38..b1e263c499 100644 --- a/app/controllers/supplemental_files_controller.rb +++ b/app/controllers/supplemental_files_controller.rb @@ -110,6 +110,7 @@ def update @supplemental_file.attach_file(attachment) if attachment raise Avalon::SaveError, @supplemental_file.errors.full_messages unless @supplemental_file.save + raise Avalon::SaveError, @object.errors[:supplemental_files_json] unless @object.save flash[:success] = "Supplemental file successfully updated." respond_to do |format| diff --git a/app/models/concerns/supplemental_file_behavior.rb b/app/models/concerns/supplemental_file_behavior.rb index d911c693c8..e204a274ea 100644 --- a/app/models/concerns/supplemental_file_behavior.rb +++ b/app/models/concerns/supplemental_file_behavior.rb @@ -29,7 +29,20 @@ module SupplementalFileBehavior # @return [SupplementalFile] def supplemental_files(tag: '*') return [] if supplemental_files_json.blank? - files = JSON.parse(supplemental_files_json).collect { |file_gid| GlobalID::Locator.locate(file_gid) } + # If the supplemental_files_json becomes out of sync with the + # database after a delete, this check could fail. Have not + # encountered in a live environment but came up in automated + # testing. Adding a rescue on fail to locate allows us to skip + # these out of sync files. + files = JSON.parse(supplemental_files_json).collect do |file_gid| + begin + GlobalID::Locator.locate(file_gid) + rescue ActiveRecord::RecordNotFound + nil + end + end.compact + return [] if files.blank? + case tag when '*' files diff --git a/app/models/supplemental_file.rb b/app/models/supplemental_file.rb index 135f3f00b1..e000c73eb8 100644 --- a/app/models/supplemental_file.rb +++ b/app/models/supplemental_file.rb @@ -31,6 +31,7 @@ class SupplementalFile < ApplicationRecord # See https://github.com/rails/rails/issues/37304 after_create_commit :index_file, prepend: true after_update_commit :update_index, prepend: true + after_destroy_commit :remove_from_index def attach_file(new_file) file.attach(new_file) @@ -103,6 +104,10 @@ def update_index end alias index_file update_index + def remove_from_index + ActiveFedora::SolrService.delete(to_global_id) + end + # Creates a solr document hash for the {#object} # @return [Hash] the solr document def to_solr diff --git a/spec/models/supplemental_file_spec.rb b/spec/models/supplemental_file_spec.rb index fd50cc22d2..65a3e65fac 100644 --- a/spec/models/supplemental_file_spec.rb +++ b/spec/models/supplemental_file_spec.rb @@ -95,6 +95,38 @@ after_doc = ActiveFedora::SolrService.query("id:#{RSolr.solr_escape(transcript.to_global_id.to_s)}").first expect(after_doc["transcript_tsim"].first).to eq("00:00:01.200 --> 00:00:21.000 [music]") end + + context 'caption as transcript' do + let(:transcript) { FactoryBot.create(:supplemental_file, :with_caption_file, tags: ['caption', 'transcript']) } + + it 'removes the transcript_tsim content when transcript tag is removed' do + before_doc = ActiveFedora::SolrService.query("id:#{RSolr.solr_escape(transcript.to_global_id.to_s)}").first + expect(before_doc["transcript_tsim"].first).to eq "00:00:03.500 --> 00:00:05.000 Example captions" + transcript.tags = ['caption'] + transcript.save + after_doc = ActiveFedora::SolrService.query("id:#{RSolr.solr_escape(transcript.to_global_id.to_s)}").first + expect(after_doc["transcript_tsim"]).to be_nil + end + end + end + end + + describe "#remove_from_index" do + let(:transcript) { FactoryBot.create(:supplemental_file, :with_transcript_file, :with_transcript_tag) } + + context 'on delete' do + it 'triggers callback' do + expect(transcript).to receive(:remove_from_index) + transcript.destroy + end + + it 'removes the transcript from the index' do + before_doc = ActiveFedora::SolrService.query("id:#{RSolr.solr_escape(transcript.to_global_id.to_s)}").first + expect(before_doc['transcript_tsim']).to eq ["00:00:03.500 --> 00:00:05.000 Example captions"] + transcript.destroy + after_doc = ActiveFedora::SolrService.query("id:#{RSolr.solr_escape(transcript.to_global_id.to_s)}").first + expect(after_doc).to be_nil + end end end diff --git a/spec/support/supplemental_files_controller_examples.rb b/spec/support/supplemental_files_controller_examples.rb index a890957323..84b87795ea 100644 --- a/spec/support/supplemental_files_controller_examples.rb +++ b/spec/support/supplemental_files_controller_examples.rb @@ -485,11 +485,32 @@ end context "removing transcript designation" do let(:supplemental_file) { FactoryBot.create(:supplemental_file, :with_transcript_file, tags: ['caption', 'transcript'], label: 'label (machine generated)') } + + before do + if object.is_a?(MasterFile) + supplemental_file.parent_id = object.id + supplemental_file.save + mo = object.media_object + mo.sections = [object] + mo.save + end + end + it "removes transcript note from tags" do expect { put :update, params: { class_id => object.id, id: supplemental_file.id, supplemental_file: valid_update_attributes, format: :html }, session: valid_session }.to change { master_file.reload.supplemental_files.first.tags }.from(['caption', 'transcript']).to(['caption']) end + + it 'removes transcript from parent media object\'s index' do + # Media object only looks at section files for has_transcript check, + # so we only test against the MasterFile case. + if object.is_a?(MasterFile) + expect{ + put :update, params: { class_id => object.id, id: supplemental_file.id, supplemental_file:valid_update_attributes, format: :html}, session: valid_session + }.to change { object.media_object.to_solr(include_child_fields: true)['has_transcripts_bsi'] }.from(true).to(false) + end + end end end @@ -605,5 +626,29 @@ expect(JSON.parse(response.body)["errors"]).to be_present end end + + context 'supplemental file marked as transcript' do + let(:supplemental_file) { FactoryBot.create(:supplemental_file, :with_transcript_file, tags: ['caption', 'transcript']) } + + before do + if object.is_a?(MasterFile) + supplemental_file.parent_id = object.id + supplemental_file.save + mo = object.media_object + mo.sections = [object] + mo.save + end + end + + it 'removes transcript from parent media object\'s index' do + # Media object only looks at section files for has_transcript check, + # so we only test against the MasterFile case. + if object.is_a?(MasterFile) + expect{ + delete :destroy, params: { class_id => object.id, id: supplemental_file.id, format: :html}, session: valid_session + }.to change { object.media_object.to_solr(include_child_fields: true)['has_transcripts_bsi'] }.from(true).to(false) + end + end + end end end From 233582bda668aa665a8ae243116c3f2c3c67ef0d Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Wed, 3 Jul 2024 15:04:21 -0400 Subject: [PATCH 109/152] Move download_path method into DerivativeBehavior --- app/models/concerns/derivative_behavior.rb | 4 ++++ app/models/derivative.rb | 4 ---- app/presenters/speedy_af/proxy/derivative.rb | 4 ---- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/app/models/concerns/derivative_behavior.rb b/app/models/concerns/derivative_behavior.rb index 5c77fca3d7..7eab5ae8b6 100644 --- a/app/models/concerns/derivative_behavior.rb +++ b/app/models/concerns/derivative_behavior.rb @@ -21,6 +21,10 @@ def streaming_url(is_mobile = false) is_mobile ? hls_url : location_url end + def download_path + Avalon::Configuration.construct_download_path.call(self) + end + def format if video_codec.present? 'video' diff --git a/app/models/derivative.rb b/app/models/derivative.rb index d6d7f845fd..a39d22b5b7 100644 --- a/app/models/derivative.rb +++ b/app/models/derivative.rb @@ -130,10 +130,6 @@ def bitrate audio_bitrate.to_i + video_bitrate.to_i end - def download_path - Avalon::Configuration.construct_download_path.call(self) - end - private def delete_file! diff --git a/app/presenters/speedy_af/proxy/derivative.rb b/app/presenters/speedy_af/proxy/derivative.rb index 7c847f889b..e51eda0628 100644 --- a/app/presenters/speedy_af/proxy/derivative.rb +++ b/app/presenters/speedy_af/proxy/derivative.rb @@ -18,8 +18,4 @@ class SpeedyAF::Proxy::Derivative < SpeedyAF::Base def bitrate audio_bitrate.to_i + video_bitrate.to_i end - - def download_path - Avalon::Configuration.construct_download_path.call(self) - end end From f87d3e529ca9e6b4c54670ae1bba1a9c4a296904 Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Wed, 3 Jul 2024 15:27:59 -0400 Subject: [PATCH 110/152] Fix parsing of SRT time cues --- lib/avalon/transcript_parser.rb | 2 +- spec/lib/avalon/transcript_search.rb | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/avalon/transcript_parser.rb b/lib/avalon/transcript_parser.rb index 18b7bdeb54..43147d4231 100644 --- a/lib/avalon/transcript_parser.rb +++ b/lib/avalon/transcript_parser.rb @@ -50,7 +50,7 @@ def normalized_text def self.extract_single_time_cue(timed_text) split_text = timed_text.match(/(\d{0,}:?\d{2}:\d{2}\.?,?\d{3} --> \d{0,}:?\d{2}:\d{2}\.?,?\d{3})(.*)/) return [nil, nil] if split_text.blank? - time_cue = split_text[1].sub(',', '.').gsub(/\s-->\s/, ',') + time_cue = split_text[1].gsub(',', '.').gsub(/\s-->\s/, ',') text = split_text[2].strip [time_cue, text] end diff --git a/spec/lib/avalon/transcript_search.rb b/spec/lib/avalon/transcript_search.rb index 67294b2353..24d3e4a00b 100644 --- a/spec/lib/avalon/transcript_search.rb +++ b/spec/lib/avalon/transcript_search.rb @@ -107,6 +107,15 @@ item = subject.iiif_content_search[:items].first expect(item[:target]).to include '#t=00:00:22.200,00:00:26.600' end + + context 'srt file' do + let(:transcript) { FactoryBot.create(:supplemental_file, :with_transcript_tag, parent_id: parent_master_file.id, file: fixture_file_upload(Rails.root.join('spec', 'fixtures', 'chunk_test.srt'), transcript_mime_type)) } + let(:transcript_mime_type) { 'text/srt' } + it 'returns result with properly formatted time cue' do + item = subject.iiif_content_search[:items].first + expect(item[:target]).to include '#t=00:00:22.200,00:00:26.600' + end + end end end end From 86f05efad5239fef002c9abe108be83b8df0ec1a Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Mon, 8 Jul 2024 10:54:09 -0400 Subject: [PATCH 111/152] Fix embedded player after Ramp rework Either the Video.js upgrade or other reworking of Ramp broke certain features of the embedded player. This work: 1. Fixes the "View in Repository" button so that the icon renders and button instances are not continuously added. 2. Fixes behavior so the quality selector is not present on audio items. 3. Hides tooltips/menues that render above the media player. These get cut off by the iframe and are unusable. 4. Removes the playback rate option from embedded audio player --- .../components/embeds/EmbeddedRamp.jsx | 23 +++++++++++-------- app/javascript/components/embeds/Ramp.scss | 8 +++++++ app/views/master_files/_player.html.erb | 3 ++- 3 files changed, 23 insertions(+), 11 deletions(-) diff --git a/app/javascript/components/embeds/EmbeddedRamp.jsx b/app/javascript/components/embeds/EmbeddedRamp.jsx index 99918b4881..cc4af2add3 100644 --- a/app/javascript/components/embeds/EmbeddedRamp.jsx +++ b/app/javascript/components/embeds/EmbeddedRamp.jsx @@ -25,7 +25,8 @@ import './Ramp.scss'; const Ramp = ({ urls, - media_object_id + media_object_id, + is_video }) => { const [manifestUrl, setManifestUrl] = React.useState(''); const [startCanvasId, setStartCanvasId] = React.useState(); @@ -79,16 +80,18 @@ const Ramp = ({ else if (command=='get_offset') event.source.postMessage({'command': 'currentTime','currentTime': embeddedPlayer.currentTime()}, event.origin); }); - /* - Quality selector extends outside iframe for audio items, so we need to disable that control - and rely on the quality automatically selected by the user's system. - */ - if (embeddedPlayer.isAudio()) { embeddedPlayer.controlBar.removeChild('qualitySelector'); } + if (embeddedPlayer.audioOnlyMode()) { + /* + Quality selector extends outside iframe for audio items, so we need to disable that control + and rely on the quality automatically selected by the user's system. + */ + embeddedPlayer.controlBar.qualitySelector.dispose(); + } // Create button component for "View in Repository" and add to control bar let repositoryUrl = Object.values(urls).join('/').replace('/embed', ''); - let position = embeddedPlayer.isAudio() ? embeddedPlayer.controlBar.children_.length : embeddedPlayer.controlBar.children_.length - 1; - var viewInRepoButton = embeddedPlayer.getChild('ControlBar').addChild('button', { + let position = embeddedPlayer.audioOnlyMode() ? embeddedPlayer.controlBar.children_.length : embeddedPlayer.controlBar.children_.length - 1; + var viewInRepoButton = embeddedPlayer.controlBar.addChild('button', { clickHandler: function(event) { window.open(repositoryUrl, '_blank').focus(); } @@ -99,7 +102,7 @@ const Ramp = ({ viewInRepoButton.controlText('View in Repository'); // Add button icon - document.querySelector('.vjs-custom-external-link .vjs-icon-placeholder').innerHTML = '' + document.querySelector('.vjs-custom-external-link').innerHTML = '' // This function only needs to run once, so we clear the interval here clearInterval(interval); @@ -111,7 +114,7 @@ const Ramp = ({ customErrorMessage='This embed encountered an error. Please refresh or contact an administrator.' startCanvasId={startCanvasId} startCanvasTime={startCanvasTime}> - + ); }; diff --git a/app/javascript/components/embeds/Ramp.scss b/app/javascript/components/embeds/Ramp.scss index 6dc816a220..e4cbd438d8 100644 --- a/app/javascript/components/embeds/Ramp.scss +++ b/app/javascript/components/embeds/Ramp.scss @@ -22,6 +22,14 @@ .video-js .vjs-big-play-button { left: 55% !important; } + .video-js.vjs-audio-only-mode { + // Disable tooltips for volume and progress in embedded + // audio. Viewport is too small to display them. + .vjs-volume-panel:hover .vjs-mouse-display, + .vjs-custom-progress-bar .tooltiptext { + display: none !important; + } + } @media (max-width: 585px) { .video-js .vjs-big-play-button { scale: 1.5; diff --git a/app/views/master_files/_player.html.erb b/app/views/master_files/_player.html.erb index d4ab0100e3..d891eb103a 100644 --- a/app/views/master_files/_player.html.erb +++ b/app/views/master_files/_player.html.erb @@ -17,6 +17,7 @@ Unless required by applicable law or agreed to in writing, software distributed <%= react_component("EmbeddedRamp", { urls: { base_url: request.protocol+request.host_with_port, fullpath_url: request.fullpath }, - media_object_id: @master_file.media_object_id + media_object_id: @master_file.media_object_id, + is_video: @master_file.is_video? } ) %> From 0c0be4de1c0bbd9e8edab747b975a19bf0056c4b Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Mon, 8 Jul 2024 11:52:52 -0400 Subject: [PATCH 112/152] Use update_index instead of save when updating We need to trigger an update of the parent object's index when updating Supplemental Files. This can be done more cleanly by using `update_index` method instead of saving the parent object. --- app/controllers/supplemental_files_controller.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/controllers/supplemental_files_controller.rb b/app/controllers/supplemental_files_controller.rb index b1e263c499..27b4bebcdd 100644 --- a/app/controllers/supplemental_files_controller.rb +++ b/app/controllers/supplemental_files_controller.rb @@ -110,7 +110,8 @@ def update @supplemental_file.attach_file(attachment) if attachment raise Avalon::SaveError, @supplemental_file.errors.full_messages unless @supplemental_file.save - raise Avalon::SaveError, @object.errors[:supplemental_files_json] unless @object.save + # Updates parent object's solr document + @object.update_index flash[:success] = "Supplemental file successfully updated." respond_to do |format| From 782a99cea3d2979d0ac4d8730d8b9295646fea32 Mon Sep 17 00:00:00 2001 From: dwithana Date: Tue, 9 Jul 2024 11:57:23 -0400 Subject: [PATCH 113/152] New Ramp build --- package.json | 2 +- yarn.lock | 20 +++++++++++++++++--- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 32ececdad1..6b557b69f5 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "@babel/plugin-proposal-object-rest-spread": "^7.20.7", "@babel/preset-react": "^7.0.0", "@babel/runtime": "7", - "@samvera/ramp": "https://github.com/samvera-labs/ramp.git#07c050224c11070a008c0db804c9fa169caa47ca", + "@samvera/ramp": "https://github.com/samvera-labs/ramp.git#b628106c0e83d211a7f175fac8fce98e525118de", "babel-plugin-macros": "^3.1.0", "babel-plugin-transform-react-remove-prop-types": "^0.4.24", "buffer": "^6.0.3", diff --git a/yarn.lock b/yarn.lock index 111a788aeb..5d677766c7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1477,9 +1477,9 @@ estree-walker "^2.0.2" picomatch "^2.3.1" -"@samvera/ramp@https://github.com/samvera-labs/ramp.git#07c050224c11070a008c0db804c9fa169caa47ca": +"@samvera/ramp@https://github.com/samvera-labs/ramp.git#b628106c0e83d211a7f175fac8fce98e525118de": version "3.1.2" - resolved "https://github.com/samvera-labs/ramp.git#07c050224c11070a008c0db804c9fa169caa47ca" + resolved "https://github.com/samvera-labs/ramp.git#b628106c0e83d211a7f175fac8fce98e525118de" dependencies: "@rollup/plugin-json" "^6.0.1" "@silvermine/videojs-quality-selector" "^1.3.1" @@ -5097,6 +5097,11 @@ picocolors@^1.0.0: resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== +picocolors@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.1.tgz#a8ad579b571952f0e5d25892de5445bcfe25aaa1" + integrity sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew== + picomatch@^2.0.4, picomatch@^2.2.1: version "2.3.0" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972" @@ -5381,7 +5386,16 @@ postcss-value-parser@^4.2.0: resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== -postcss@^8.3.11, postcss@^8.4.21, postcss@^8.4.24: +postcss@^8.3.11: + version "8.4.39" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.39.tgz#aa3c94998b61d3a9c259efa51db4b392e1bde0e3" + integrity sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw== + dependencies: + nanoid "^3.3.7" + picocolors "^1.0.1" + source-map-js "^1.2.0" + +postcss@^8.4.21, postcss@^8.4.24: version "8.4.38" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.38.tgz#b387d533baf2054288e337066d81c6bee9db9e0e" integrity sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A== From 09107fa55182141f693bc2d8e8323a17a7458818 Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Tue, 9 Jul 2024 15:50:11 -0400 Subject: [PATCH 114/152] Remove zero counts from "Found In" results --- app/helpers/catalog_helper.rb | 11 +++++++++++ app/views/catalog/_index_media_object.html.erb | 4 +--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/app/helpers/catalog_helper.rb b/app/helpers/catalog_helper.rb index f0c573d881..8c83c6b424 100644 --- a/app/helpers/catalog_helper.rb +++ b/app/helpers/catalog_helper.rb @@ -21,4 +21,15 @@ def current_sort_field blacklight_config.sort_fields[actualSort] || default_sort_field end + def display_found_in(document) + metadata_count = document.to_h.sum {|k,v| k =~ /metadata_tf_/ ? v : 0 } + transcript_count = document["sections"]["docs"].sum { |d| d["transcripts"]["docs"].sum {|s| s.sum {|k,v| k =~ /transcript_tf_/ ? v : 0 }}} + section_count = document.to_h.sum {|k,v| k =~ /structure_tf_/ ? v : 0 } + + metadata = "metadata (#{metadata_count})" if metadata_count > 0 + transcript = "transcript (#{transcript_count})" if transcript_count > 0 + sections = "sections (#{section_count})" if section_count > 0 + + [metadata, transcript, sections].compact.join(', ') + end end diff --git a/app/views/catalog/_index_media_object.html.erb b/app/views/catalog/_index_media_object.html.erb index 51563a7b8a..32c7bdf417 100644 --- a/app/views/catalog/_index_media_object.html.erb +++ b/app/views/catalog/_index_media_object.html.erb @@ -25,8 +25,6 @@ Unless required by applicable law or agreed to in writing, software distributed <% end %> <% if params[:q].present? %>
    Found in:
    -
    metadata (<%= doc_presenter.document.to_h.sum {|k,v| k =~ /metadata_tf_/ ? v : 0 } %>) - , transcript (<%= doc_presenter.document["sections"]["docs"].sum { |d| d["transcripts"]["docs"].sum {|s| s.sum {|k,v| k =~ /transcript_tf_/ ? v : 0 }}} %>) - , sections (<%= doc_presenter.document.to_h.sum {|k,v| k =~ /structure_tf_/ ? v : 0 } %>)
    +
    <%= display_found_in(doc_presenter.document) %>
    <% end %> From 1cdb1de35faa92d4dc83ab247babf1ee059f9290 Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Wed, 10 Jul 2024 09:26:47 -0400 Subject: [PATCH 115/152] Remove margin and padding from embed body element --- app/views/layouts/embed.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/layouts/embed.html.erb b/app/views/layouts/embed.html.erb index d2e7777297..7e4a9dff4a 100644 --- a/app/views/layouts/embed.html.erb +++ b/app/views/layouts/embed.html.erb @@ -30,7 +30,7 @@ Unless required by applicable law or agreed to in writing, software distributed <%= render "modules/google_analytics" %> - + <%= yield %> <%= yield :page_scripts %> From 50acfad5e4b3e60cdc865901721557c4a985b2b2 Mon Sep 17 00:00:00 2001 From: cjcolvar Date: Wed, 10 Jul 2024 10:56:19 -0400 Subject: [PATCH 116/152] Upgrade rails and specific dependencies --- Gemfile.lock | 140 ++++++++++++++++++++++++++------------------------- yarn.lock | 85 ++++++++++++++++--------------- 2 files changed, 116 insertions(+), 109 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 68c82f7d8c..4edc29c7ef 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -76,47 +76,47 @@ GIT GEM remote: https://rubygems.org/ specs: - actioncable (7.0.8.1) - actionpack (= 7.0.8.1) - activesupport (= 7.0.8.1) + actioncable (7.0.8.4) + actionpack (= 7.0.8.4) + activesupport (= 7.0.8.4) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (7.0.8.1) - actionpack (= 7.0.8.1) - activejob (= 7.0.8.1) - activerecord (= 7.0.8.1) - activestorage (= 7.0.8.1) - activesupport (= 7.0.8.1) + actionmailbox (7.0.8.4) + actionpack (= 7.0.8.4) + activejob (= 7.0.8.4) + activerecord (= 7.0.8.4) + activestorage (= 7.0.8.4) + activesupport (= 7.0.8.4) mail (>= 2.7.1) net-imap net-pop net-smtp - actionmailer (7.0.8.1) - actionpack (= 7.0.8.1) - actionview (= 7.0.8.1) - activejob (= 7.0.8.1) - activesupport (= 7.0.8.1) + actionmailer (7.0.8.4) + actionpack (= 7.0.8.4) + actionview (= 7.0.8.4) + activejob (= 7.0.8.4) + activesupport (= 7.0.8.4) mail (~> 2.5, >= 2.5.4) net-imap net-pop net-smtp rails-dom-testing (~> 2.0) - actionpack (7.0.8.1) - actionview (= 7.0.8.1) - activesupport (= 7.0.8.1) + actionpack (7.0.8.4) + actionview (= 7.0.8.4) + activesupport (= 7.0.8.4) rack (~> 2.0, >= 2.2.4) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (7.0.8.1) - actionpack (= 7.0.8.1) - activerecord (= 7.0.8.1) - activestorage (= 7.0.8.1) - activesupport (= 7.0.8.1) + actiontext (7.0.8.4) + actionpack (= 7.0.8.4) + activerecord (= 7.0.8.4) + activestorage (= 7.0.8.4) + activesupport (= 7.0.8.4) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (7.0.8.1) - activesupport (= 7.0.8.1) + actionview (7.0.8.4) + activesupport (= 7.0.8.4) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) @@ -153,8 +153,8 @@ GEM om (~> 3.1) rdf (~> 3.2) rdf-rdfxml (~> 3.2) - activejob (7.0.8.1) - activesupport (= 7.0.8.1) + activejob (7.0.8.4) + activesupport (= 7.0.8.4) globalid (>= 0.3.6) activejob-traffic_control (0.1.3) activejob (>= 4.2) @@ -163,25 +163,25 @@ GEM activejob-uniqueness (0.2.5) activejob (>= 4.2, < 7.1) redlock (>= 1.2, < 2) - activemodel (7.0.8.1) - activesupport (= 7.0.8.1) - activerecord (7.0.8.1) - activemodel (= 7.0.8.1) - activesupport (= 7.0.8.1) + activemodel (7.0.8.4) + activesupport (= 7.0.8.4) + activerecord (7.0.8.4) + activemodel (= 7.0.8.4) + activesupport (= 7.0.8.4) activerecord-session_store (2.0.0) actionpack (>= 5.2.4.1) activerecord (>= 5.2.4.1) multi_json (~> 1.11, >= 1.11.2) rack (>= 2.0.8, < 3) railties (>= 5.2.4.1) - activestorage (7.0.8.1) - actionpack (= 7.0.8.1) - activejob (= 7.0.8.1) - activerecord (= 7.0.8.1) - activesupport (= 7.0.8.1) + activestorage (7.0.8.4) + actionpack (= 7.0.8.4) + activejob (= 7.0.8.4) + activerecord (= 7.0.8.4) + activesupport (= 7.0.8.4) marcel (~> 1.0) mini_mime (>= 1.1.0) - activesupport (7.0.8.1) + activesupport (7.0.8.4) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) @@ -282,7 +282,7 @@ GEM bootstrap_form (5.2.3) actionpack (>= 6.0) activemodel (>= 6.0) - builder (3.2.4) + builder (3.3.0) byebug (11.1.3) cancancan (3.4.0) capistrano (3.17.3) @@ -327,7 +327,7 @@ GEM coffee-script-source execjs coffee-script-source (1.12.2) - concurrent-ruby (1.2.3) + concurrent-ruby (1.3.3) config (4.2.1) deep_merge (~> 1.2, >= 1.2.1) dry-validation (~> 1.0, >= 1.0.0) @@ -414,7 +414,7 @@ GEM mail (~> 2.7) equivalent-xml (0.6.0) nokogiri (>= 1.4.3) - erubi (1.12.0) + erubi (1.13.0) et-orbi (1.2.7) tzinfo ethon (0.16.0) @@ -499,7 +499,7 @@ GEM hydra-access-controls (= 12.1.0) hydra-core (= 12.1.0) rails (>= 5.2, < 7.1) - i18n (1.14.4) + i18n (1.14.5) concurrent-ruby (~> 1.0) iconv (1.0.8) ims-lti (1.1.13) @@ -514,7 +514,7 @@ GEM rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) thor (>= 0.14, < 2.0) - jquery-ui-rails (6.0.1) + jquery-ui-rails (7.0.0) railties (>= 3.2.16) json (2.6.3) json-canonicalization (0.3.1) @@ -578,20 +578,20 @@ GEM marcel (1.0.4) matrix (0.4.2) memoist (0.16.2) - method_source (1.0.0) + method_source (1.1.0) mime-types (3.4.1) mime-types-data (~> 3.2015) mime-types-data (3.2022.0105) mini_mime (1.1.5) - mini_portile2 (2.8.5) + mini_portile2 (2.8.7) minitar (0.9) - minitest (5.22.3) + minitest (5.24.1) msgpack (1.6.0) multi_json (1.15.0) multi_xml (0.6.0) multipart-post (2.3.0) mysql2 (0.5.5) - net-imap (0.4.10) + net-imap (0.4.14) date net-protocol net-ldap (0.18.0) @@ -601,16 +601,16 @@ GEM timeout net-scp (4.0.0) net-ssh (>= 2.6.5, < 8.0.0) - net-smtp (0.4.0.1) + net-smtp (0.5.0) net-protocol net-ssh (7.0.1) netrc (0.11.0) - nio4r (2.7.1) + nio4r (2.7.3) noid (0.9.0) noid-rails (3.1.0) actionpack (>= 5.0.0, < 7.1) noid (~> 0.9) - nokogiri (1.16.3) + nokogiri (1.16.6) mini_portile2 (~> 2.8.2) racc (~> 1.4) nom-xml (1.2.0) @@ -659,7 +659,7 @@ GEM puma (6.4.2) nio4r (~> 2.0) raabro (1.4.0) - racc (1.7.3) + racc (1.8.0) rack (2.2.9) rack-cors (2.0.2) rack (>= 2.0.0) @@ -669,20 +669,20 @@ GEM rack rack-test (2.1.0) rack (>= 1.3) - rails (7.0.8.1) - actioncable (= 7.0.8.1) - actionmailbox (= 7.0.8.1) - actionmailer (= 7.0.8.1) - actionpack (= 7.0.8.1) - actiontext (= 7.0.8.1) - actionview (= 7.0.8.1) - activejob (= 7.0.8.1) - activemodel (= 7.0.8.1) - activerecord (= 7.0.8.1) - activestorage (= 7.0.8.1) - activesupport (= 7.0.8.1) + rails (7.0.8.4) + actioncable (= 7.0.8.4) + actionmailbox (= 7.0.8.4) + actionmailer (= 7.0.8.4) + actionpack (= 7.0.8.4) + actiontext (= 7.0.8.4) + actionview (= 7.0.8.4) + activejob (= 7.0.8.4) + activemodel (= 7.0.8.4) + activerecord (= 7.0.8.4) + activestorage (= 7.0.8.4) + activesupport (= 7.0.8.4) bundler (>= 1.15.0) - railties (= 7.0.8.1) + railties (= 7.0.8.4) rails-controller-testing (1.0.5) actionpack (>= 5.0.1.rc1) actionview (>= 5.0.1.rc1) @@ -697,15 +697,15 @@ GEM rails_same_site_cookie (0.1.9) rack (>= 1.5) user_agent_parser (~> 2.6) - railties (7.0.8.1) - actionpack (= 7.0.8.1) - activesupport (= 7.0.8.1) + railties (7.0.8.4) + actionpack (= 7.0.8.4) + activesupport (= 7.0.8.4) method_source rake (>= 12.2) thor (~> 1.0) zeitwerk (~> 2.5) rainbow (3.1.1) - rake (13.1.0) + rake (13.2.1) rb-fsevent (0.11.2) rb-inotify (0.10.1) ffi (~> 1.0) @@ -772,7 +772,8 @@ GEM mime-types (>= 1.16, < 4.0) netrc (~> 0.8) retriable (3.1.2) - rexml (3.2.5) + rexml (3.3.1) + strscan roo (2.10.0) nokogiri (~> 1) rubyzip (>= 1.3.0, < 3.0.0) @@ -915,6 +916,7 @@ GEM net-scp (>= 1.1.2) net-ssh (>= 2.8.0) stomp (1.4.10) + strscan (3.1.0) suo (0.4.0) dalli msgpack @@ -972,7 +974,7 @@ GEM rexml xpath (3.2.0) nokogiri (~> 1.8) - zeitwerk (2.6.13) + zeitwerk (2.6.16) zk (1.10.0) zookeeper (~> 1.5.0) zookeeper (1.5.5) diff --git a/yarn.lock b/yarn.lock index 5d677766c7..6ba38985a0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2321,21 +2321,21 @@ bluebird@~3.4.0: resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.4.7.tgz#f72d760be09b7f76d08ed8fae98b289a8d05fab3" integrity sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA== -body-parser@1.20.0: - version "1.20.0" - resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.0.tgz#3de69bd89011c11573d7bfee6a64f11b6bd27cc5" - integrity sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg== +body-parser@1.20.2: + version "1.20.2" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.2.tgz#6feb0e21c4724d06de7ff38da36dad4f57a747fd" + integrity sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA== dependencies: bytes "3.1.2" - content-type "~1.0.4" + content-type "~1.0.5" debug "2.6.9" depd "2.0.0" destroy "1.2.0" http-errors "2.0.0" iconv-lite "0.4.24" on-finished "2.4.1" - qs "6.10.3" - raw-body "2.5.1" + qs "6.11.0" + raw-body "2.5.2" type-is "~1.6.18" unpipe "1.0.0" @@ -2363,11 +2363,11 @@ brace-expansion@^1.1.7: concat-map "0.0.1" braces@^3.0.2, braces@~3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" - integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== dependencies: - fill-range "^7.0.1" + fill-range "^7.1.1" browserslist@^4.0.0: version "4.17.2" @@ -2725,6 +2725,11 @@ content-type@~1.0.4: resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== +content-type@~1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" + integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== + convert-source-map@^1.7.0: version "1.8.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.8.0.tgz#f3373c32d21b4d780dd8004514684fb791ca4369" @@ -2737,10 +2742,10 @@ cookie-signature@1.0.6: resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= -cookie@0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" - integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== +cookie@0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051" + integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw== core-js-compat@^3.31.0: version "3.32.0" @@ -3410,16 +3415,16 @@ executable@^4.1.1: pify "^2.2.0" express@^4.17.3: - version "4.18.1" - resolved "https://registry.yarnpkg.com/express/-/express-4.18.1.tgz#7797de8b9c72c857b9cd0e14a5eea80666267caf" - integrity sha512-zZBcOX9TfehHQhtupq57OF8lFZ3UZi08Y97dwFCkD8p9d/d2Y3M+ykKcwaMDEL+4qyUolgBDX6AblpR3fL212Q== + version "4.19.2" + resolved "https://registry.yarnpkg.com/express/-/express-4.19.2.tgz#e25437827a3aa7f2a827bc8171bbbb664a356465" + integrity sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q== dependencies: accepts "~1.3.8" array-flatten "1.1.1" - body-parser "1.20.0" + body-parser "1.20.2" content-disposition "0.5.4" content-type "~1.0.4" - cookie "0.5.0" + cookie "0.6.0" cookie-signature "1.0.6" debug "2.6.9" depd "2.0.0" @@ -3435,7 +3440,7 @@ express@^4.17.3: parseurl "~1.3.3" path-to-regexp "0.1.7" proxy-addr "~2.0.7" - qs "6.10.3" + qs "6.11.0" range-parser "~1.2.1" safe-buffer "5.2.1" send "0.18.0" @@ -3508,10 +3513,10 @@ figures@^3.2.0: dependencies: escape-string-regexp "^1.0.5" -fill-range@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" - integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== dependencies: to-regex-range "^5.0.1" @@ -5494,13 +5499,6 @@ punycode@^2.1.1: resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== -qs@6.10.3: - version "6.10.3" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.3.tgz#d6cde1b2ffca87b5aa57889816c5f81535e22e8e" - integrity sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ== - dependencies: - side-channel "^1.0.4" - qs@6.10.4: version "6.10.4" resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.4.tgz#6a3003755add91c0ec9eacdc5f878b034e73f9e7" @@ -5508,6 +5506,13 @@ qs@6.10.4: dependencies: side-channel "^1.0.4" +qs@6.11.0: + version "6.11.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" + integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== + dependencies: + side-channel "^1.0.4" + querystringify@^2.1.1: version "2.2.0" resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6" @@ -5525,10 +5530,10 @@ range-parser@^1.2.1, range-parser@~1.2.1: resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== -raw-body@2.5.1: - version "2.5.1" - resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857" - integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig== +raw-body@2.5.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a" + integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA== dependencies: bytes "3.1.2" http-errors "2.0.0" @@ -6966,10 +6971,10 @@ wrappy@1: resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= -ws@^8.13.0: - version "8.13.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0" - integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA== +ws@^8.13.0, ws@^8.18.0: + version "8.18.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc" + integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw== xmlbuilder@^10.0.0: version "10.1.1" From 31d762da625dc88022d687472c1ae8b47fe1aed7 Mon Sep 17 00:00:00 2001 From: cjcolvar Date: Wed, 10 Jul 2024 11:26:59 -0400 Subject: [PATCH 117/152] Context search solr query should return results across entire indexed value when extremely long --- lib/avalon/transcript_search.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/avalon/transcript_search.rb b/lib/avalon/transcript_search.rb index 26c788d402..86b449cff6 100644 --- a/lib/avalon/transcript_search.rb +++ b/lib/avalon/transcript_search.rb @@ -37,7 +37,8 @@ def perform_search(phrase_searching: true) "hl.fl": "transcript_tsim", "hl.snippets": 1000000, "hl.fragsize": 0, - "hl.method": "original") + "hl.method": "original", + "hl.maxAnalyzedChars": "-1") end def iiif_content_search(phrase_searching: true) From aaf3d4fd0fee66cfa505ad11d3d71aea6ea1201e Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Wed, 10 Jul 2024 15:00:33 -0400 Subject: [PATCH 118/152] Fail SupplementalFile Binary update if no Accept header Users updating supplemental files via CLI should use `-H "Accept:application/json"` in their requests to ensure a JSON response from an endpoint. For updating the binary file specifically, the request is through the HTML format endpoint and so having Accept headers needs to be required to force a JSON response in the CLI environment. --- app/controllers/supplemental_files_controller.rb | 1 + spec/support/supplemental_files_controller_examples.rb | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/app/controllers/supplemental_files_controller.rb b/app/controllers/supplemental_files_controller.rb index 27b4bebcdd..8b1adf6430 100644 --- a/app/controllers/supplemental_files_controller.rb +++ b/app/controllers/supplemental_files_controller.rb @@ -100,6 +100,7 @@ def update raise Avalon::BadRequest, "Missing required Content-type headers" unless request.headers["Content-Type"] == 'application/json' elsif request.headers['Avalon-Api-Key'].present? raise Avalon::BadRequest, "Incorrect request format. Use JSON if updating metadata." unless attachment + raise Avalon::BadRequest, "Missing required Accept headers" unless request.headers["Accept"] == 'application/json' end raise Avalon::BadRequest, "Missing required parameters" unless validate_params diff --git a/spec/support/supplemental_files_controller_examples.rb b/spec/support/supplemental_files_controller_examples.rb index 84b87795ea..e2c040a2c6 100644 --- a/spec/support/supplemental_files_controller_examples.rb +++ b/spec/support/supplemental_files_controller_examples.rb @@ -527,6 +527,14 @@ expect(response).to have_http_status(:ok) expect(response.body).to eq({ "id": supplemental_file.id }.to_json) end + + context "without Accept header" do + it "returns a 400" do + request.headers['Avalon-Api-Key'] = 'secret_token' + put :update, params: { class_id => object.id, id: supplemental_file.id, file: file_update, format: :html }, session: valid_session + expect(response).to have_http_status(400) + end + end end context "API without file" do From 01ec2155bc066f2a92633d39f724fb1215a31418 Mon Sep 17 00:00:00 2001 From: cjcolvar Date: Wed, 10 Jul 2024 17:17:51 -0400 Subject: [PATCH 119/152] Force vertical alignment of horizontal volume slider thumb to top --- app/javascript/components/Ramp.scss | 3 +++ app/javascript/components/embeds/Ramp.scss | 3 +++ 2 files changed, 6 insertions(+) diff --git a/app/javascript/components/Ramp.scss b/app/javascript/components/Ramp.scss index ce09356506..18b4982164 100644 --- a/app/javascript/components/Ramp.scss +++ b/app/javascript/components/Ramp.scss @@ -38,6 +38,9 @@ min-height: 2.9em; } } + .vjs-volume-horizontal .vjs-volume-level .vjs-svg-icon>svg { + vertical-align: top; + } } .ramp--structured-nav { diff --git a/app/javascript/components/embeds/Ramp.scss b/app/javascript/components/embeds/Ramp.scss index e4cbd438d8..44293fe52f 100644 --- a/app/javascript/components/embeds/Ramp.scss +++ b/app/javascript/components/embeds/Ramp.scss @@ -45,4 +45,7 @@ min-height: 2.9em; } } + .vjs-volume-horizontal .vjs-volume-level .vjs-svg-icon>svg { + vertical-align: top; + } } From 54a1fb3771b727625a2fc090935e68645ed06e3e Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Thu, 11 Jul 2024 11:18:38 -0400 Subject: [PATCH 120/152] Add new fields to caption/transcript batch ingest Since implementing support for uploading supplemental files via batch ingest we have added additional editable fields to the various types of supplemental file. This commit adds support for setting these fields in batch ingest: 1. Transcript language 2. Marking captions to be treated as transcripts 3. Ensured that captions could be marked machine generated --- lib/avalon/batch/entry.rb | 9 +++++---- lib/avalon/batch/manifest.rb | 4 ++-- spec/lib/avalon/batch/entry_spec.rb | 31 +++++++++++++++++------------ 3 files changed, 25 insertions(+), 19 deletions(-) diff --git a/lib/avalon/batch/entry.rb b/lib/avalon/batch/entry.rb index 161d9693b3..3f07b5f925 100644 --- a/lib/avalon/batch/entry.rb +++ b/lib/avalon/batch/entry.rb @@ -276,14 +276,14 @@ def self.derivativePath(filename, quality) filename.dup.insert(filename.rindex('.'), ".#{quality}") end - def self.caption_language(language) + def self.content_language(language) begin LanguageTerm.find(language.capitalize).code rescue LanguageTerm::LookupError Settings.caption_default.language end end - private_class_method :caption_language + private_class_method :content_language def self.process_datastream(datastream, type, parent_id) file_key, label_key, language_key = ["_file", "_label", "_language"].map { |item| item.prepend(type).to_sym } @@ -292,10 +292,11 @@ def self.process_datastream(datastream, type, parent_id) # Build out file metadata filename = datastream[file_key].split('/').last label = datastream[label_key].presence || filename - language = datastream[language_key].present? ? caption_language(datastream[language_key]) : Settings.caption_default.language + language = datastream[language_key].present? ? content_language(datastream[language_key]) : Settings.caption_default.language + treat_as_transcript = datastream[:treat_as_transcript].present? ? 'transcript' : nil machine_generated = datastream[:machine_generated].present? ? 'machine_generated' : nil # Create SupplementalFile - supplemental_file = SupplementalFile.new(label: label, tags: [type, machine_generated].compact, language: language, parent_id: parent_id) + supplemental_file = SupplementalFile.new(label: label, tags: [type, treat_as_transcript, machine_generated].uniq.compact, language: language, parent_id: parent_id) supplemental_file.file.attach(io: FileLocator.new(datastream[file_key]).reader, filename: filename) supplemental_file.save ? supplemental_file : nil end diff --git a/lib/avalon/batch/manifest.rb b/lib/avalon/batch/manifest.rb index b7ced32ce3..7e9d56b454 100644 --- a/lib/avalon/batch/manifest.rb +++ b/lib/avalon/batch/manifest.rb @@ -22,7 +22,7 @@ class Manifest EXTENSIONS = ['csv','xls','xlsx','ods'] FILE_FIELDS = [:file,:label,:offset,:skip_transcoding,:absolute_location,:date_digitized, :caption_file, :caption_label, :caption_language, - :transcript_file, :transcript_label, :machine_generated] + :treat_as_transcript, :transcript_file, :transcript_label, :transcript_language, :machine_generated] SKIP_FIELDS = [:collection] def_delegators :@entries, :each @@ -142,7 +142,7 @@ def create_entries! unless f.blank? || SKIP_FIELDS.include?(f) || values[i].blank? if FILE_FIELDS.include?(f) content << {} if f == :file - if ['caption', 'transcript', 'machine_generated'].any? { |type| f.to_s.include?(type) } + if ['caption', 'transcript', 'treat_as_transcript', 'machine_generated'].any? { |type| f.to_s.include?(type) } supplementing_files(f, content, values, i) next end diff --git a/spec/lib/avalon/batch/entry_spec.rb b/spec/lib/avalon/batch/entry_spec.rb index ee67569b65..72fbe5ad6f 100644 --- a/spec/lib/avalon/batch/entry_spec.rb +++ b/spec/lib/avalon/batch/entry_spec.rb @@ -100,19 +100,19 @@ describe '#gatherFiles' do it 'should return a hash of files keyed with their quality' do - expect(Avalon::Batch::Entry.gatherFiles(filename)).to hash_match derivative_hash + expect(Avalon::Batch::Entry.gatherFiles(filename)).to hash_match derivative_hash end end describe '#derivativePaths' do it 'should return the paths to all derivative files that exist' do - expect(Avalon::Batch::Entry.derivativePaths(filename)).to eq derivative_paths + expect(Avalon::Batch::Entry.derivativePaths(filename)).to eq derivative_paths end end describe '#derivativePath' do it 'should insert supplied quality into filename' do - expect(Avalon::Batch::Entry.derivativePath(filename, 'low')).to eq filename_low + expect(Avalon::Batch::Entry.derivativePath(filename, 'low')).to eq filename_low end end end @@ -179,8 +179,8 @@ context 'with caption and transcript files' do let(:caption_file) { File.join(Rails.root, 'spec/fixtures/dropbox/example_batch_ingest/assets/sheephead_mountain.mov.vtt')} - let(:caption) {{ :caption_file => caption_file, :caption_label => 'Sheephead Captions', :caption_language => 'English' }} - let(:transcript) {{ :transcript_file => caption_file, :transcript_label => 'Sheephead Transcript', :machine_generated => 'yes' }} + let(:caption) {{ :caption_file => caption_file, :caption_label => 'Sheephead Captions', :caption_language => 'English', :treat_as_transcript => 'yes', :machine_generated => 'yes' }} + let(:transcript) {{ :transcript_file => caption_file, :transcript_label => 'Sheephead Transcript', :transcript_language => 'French', :machine_generated => 'yes' }} let(:entry_files) { [{ file: File.join(testdir, filename), offset: '00:00:00.500', label: 'Quis quo', date_digitized: '2015-10-30', skip_transcoding: false, caption_1: caption, transcript_1: transcript }] } it 'adds captions and transcripts to masterfile' do @@ -224,15 +224,15 @@ end context 'with multiple captions and transcripts' do - let(:caption) { [{ :caption_file => caption_file, :caption_label => 'Sheephead Captions', :caption_language => 'english' }, - { :caption_file => caption_file, :caption_label => 'Second Caption', :caption_language => 'fre' }] } + let(:caption) { [{ :caption_file => caption_file, :caption_label => 'Sheephead Captions', :caption_language => 'english', :treat_as_transcript => 'yes' }, + { :caption_file => caption_file, :caption_label => 'Second Caption', :caption_language => 'fre', :machine_generated => 'yes' }] } let(:transcript) { [{ :transcript_file => caption_file, :transcript_label => 'Sheephead Transcript' }, - { :transcript_file => caption_file, :machine_generated => 'yes' }] } + { :transcript_file => caption_file, :transcript_language => 'french', :machine_generated => 'yes' }] } it 'should attach all captions and transcripts to master file' do expect(master_file.has_captions?).to eq true expect(master_file.supplemental_file_captions.count).to eq 2 expect(master_file.has_transcripts?).to eq true - expect(master_file.supplemental_file_transcripts.count).to eq 2 + expect(master_file.supplemental_file_transcripts.count).to eq 3 end it 'assigns metadata properly' do @@ -240,10 +240,15 @@ expect(master_file.supplemental_file_captions[1].label).to eq 'Second Caption' expect(master_file.supplemental_file_captions[0].language).to eq 'eng' expect(master_file.supplemental_file_captions[1].language).to eq 'fre' - expect(master_file.supplemental_file_transcripts[0].label).to eq 'Sheephead Transcript' - expect(master_file.supplemental_file_transcripts[1].label).to eq 'sheephead_mountain.mov.vtt' - expect(master_file.supplemental_file_transcripts[0].tags).to_not include 'machine_generated' - expect(master_file.supplemental_file_transcripts[1].tags).to include 'machine_generated' + expect(master_file.supplemental_file_captions[0].tags).to_not include 'machine_generated' + expect(master_file.supplemental_file_captions[1].tags).to include 'machine_generated' + expect(master_file.supplemental_file_transcripts[0]).to eq master_file.supplemental_file_captions[0] + expect(master_file.supplemental_file_transcripts[1].label).to eq 'Sheephead Transcript' + expect(master_file.supplemental_file_transcripts[2].label).to eq 'sheephead_mountain.mov.vtt' + expect(master_file.supplemental_file_transcripts[1].language).to eq 'eng' + expect(master_file.supplemental_file_transcripts[2].language).to eq 'fre' + expect(master_file.supplemental_file_transcripts[1].tags).to_not include 'machine_generated' + expect(master_file.supplemental_file_transcripts[2].tags).to include 'machine_generated' expect(master_file.supplemental_files.all? { |sf| sf.parent_id == master_file.id }).to be true end end From c9733aa7e4127a38357d66765b8a8aae2dc8709a Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Thu, 11 Jul 2024 15:39:53 -0400 Subject: [PATCH 121/152] Skip transcript search in bookmark context The "Selected Items" page was returning a 400 error because the search service that populates the page runs through our SearchBuilder model and generates a `q` solr_parameter. This triggered the transcript search logic. Something about the `q` generated by the search service broke the query building for `transcript_tsim`, possibly that the search service generates `q="{!lucene}(#object_ids)"`. Checking if we are in the bookmark controller context and skipping the transcript searching allows us to bypass the error. --- app/models/search_builder.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/search_builder.rb b/app/models/search_builder.rb index fbefdb8707..014ba8bf84 100644 --- a/app/models/search_builder.rb +++ b/app/models/search_builder.rb @@ -48,7 +48,7 @@ def add_access_controls_to_solr_params(solr_parameters) end def search_section_transcripts(solr_parameters) - return unless solr_parameters[:q].present? && SupplementalFile.with_tag('transcript').any? + return unless solr_parameters[:q].present? && SupplementalFile.with_tag('transcript').any? && !(blacklight_params[:controller] == 'bookmarks') terms = solr_parameters[:q].split term_subquery = terms.map { |term| "transcript_tsim:#{RSolr.solr_escape(term)}" }.join(" OR ") @@ -57,7 +57,7 @@ def search_section_transcripts(solr_parameters) end def term_frequency_counts(solr_parameters) - return unless solr_parameters[:q].present? + return unless solr_parameters[:q].present? && !(blacklight_params[:controller] == 'bookmarks') # Any search or filtering using a `q` parameter when transcripts are not present fails because # the transcript_tsim field does not get created. We need to only add the transcript searching # when transcripts are present. From feb54f85999f934f1dfabb022b6a8d2601d20074 Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Fri, 12 Jul 2024 11:02:18 -0400 Subject: [PATCH 122/152] Set timelines without structure from full length of item --- app/assets/javascripts/ramp_utils.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/assets/javascripts/ramp_utils.js b/app/assets/javascripts/ramp_utils.js index f7a0c3ab92..30fbdefcc8 100644 --- a/app/assets/javascripts/ramp_utils.js +++ b/app/assets/javascripts/ramp_utils.js @@ -108,6 +108,11 @@ function getTimelineScopes() { } let parent = currentStructureItem.closest('ul').closest('li'); + if (parent.length === 0) { + let begin = 0; + let end = activeItem.times.end; + scopes[0].times = { begin: 0, end: end } + } while (parent.length > 0) { let next = parent.closest('ul').closest('li'); let begin = 0; From ac6b63cab6b4fc873a120fdcce3d4f873f33f39a Mon Sep 17 00:00:00 2001 From: cjcolvar Date: Fri, 12 Jul 2024 13:29:07 -0400 Subject: [PATCH 123/152] Don't show Found in if no found in hits exist --- app/views/catalog/_index_media_object.html.erb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/views/catalog/_index_media_object.html.erb b/app/views/catalog/_index_media_object.html.erb index 32c7bdf417..400f1360ec 100644 --- a/app/views/catalog/_index_media_object.html.erb +++ b/app/views/catalog/_index_media_object.html.erb @@ -23,8 +23,9 @@ Unless required by applicable law or agreed to in writing, software distributed
    <%= field_presenter.values.join(', ') %>
    <% end %> <% end %> - <% if params[:q].present? %> + <% found_in_hits = display_found_in(doc_presenter.document) %> + <% if params[:q].present? && found_in_hits.present? %>
    Found in:
    -
    <%= display_found_in(doc_presenter.document) %>
    +
    <%= found_in_hits %>
    <% end %> From 4edb3c167a6a58e604d5912ac6337cbb195e8213 Mon Sep 17 00:00:00 2001 From: dwithana Date: Fri, 12 Jul 2024 13:58:09 -0400 Subject: [PATCH 124/152] New Ramp build --- package.json | 2 +- yarn.lock | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 6b557b69f5..f4f07cb527 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "@babel/plugin-proposal-object-rest-spread": "^7.20.7", "@babel/preset-react": "^7.0.0", "@babel/runtime": "7", - "@samvera/ramp": "https://github.com/samvera-labs/ramp.git#b628106c0e83d211a7f175fac8fce98e525118de", + "@samvera/ramp": "https://github.com/samvera-labs/ramp.git#9c2798014c5afc6edaa36dd93c9b5bc404265642", "babel-plugin-macros": "^3.1.0", "babel-plugin-transform-react-remove-prop-types": "^0.4.24", "buffer": "^6.0.3", diff --git a/yarn.lock b/yarn.lock index 6ba38985a0..162ed5aa81 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1477,9 +1477,9 @@ estree-walker "^2.0.2" picomatch "^2.3.1" -"@samvera/ramp@https://github.com/samvera-labs/ramp.git#b628106c0e83d211a7f175fac8fce98e525118de": +"@samvera/ramp@https://github.com/samvera-labs/ramp.git#9c2798014c5afc6edaa36dd93c9b5bc404265642": version "3.1.2" - resolved "https://github.com/samvera-labs/ramp.git#b628106c0e83d211a7f175fac8fce98e525118de" + resolved "https://github.com/samvera-labs/ramp.git#9c2798014c5afc6edaa36dd93c9b5bc404265642" dependencies: "@rollup/plugin-json" "^6.0.1" "@silvermine/videojs-quality-selector" "^1.3.1" @@ -6971,7 +6971,7 @@ wrappy@1: resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= -ws@^8.13.0, ws@^8.18.0: +ws@^8.13.0: version "8.18.0" resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc" integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw== From 99d0f0553701a413add35eafd5f465217a8e5c36 Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Fri, 12 Jul 2024 14:07:34 -0400 Subject: [PATCH 125/152] Cast treat as transcript and machine generated field to boolean --- lib/avalon/batch.rb | 4 ++++ lib/avalon/batch/entry.rb | 4 ++-- lib/avalon/batch/manifest.rb | 7 ++----- spec/lib/avalon/batch/entry_spec.rb | 4 ++-- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/lib/avalon/batch.rb b/lib/avalon/batch.rb index 6772e1603f..93a37baabb 100644 --- a/lib/avalon/batch.rb +++ b/lib/avalon/batch.rb @@ -69,5 +69,9 @@ def self.extract_files_from_lsof_output(output) end found_files end + + def self.true_field?(value) + not (value.to_s =~ /^(y(es)?|t(rue)?)$/i).nil? + end end end diff --git a/lib/avalon/batch/entry.rb b/lib/avalon/batch/entry.rb index 3f07b5f925..873a7caa81 100644 --- a/lib/avalon/batch/entry.rb +++ b/lib/avalon/batch/entry.rb @@ -293,8 +293,8 @@ def self.process_datastream(datastream, type, parent_id) filename = datastream[file_key].split('/').last label = datastream[label_key].presence || filename language = datastream[language_key].present? ? content_language(datastream[language_key]) : Settings.caption_default.language - treat_as_transcript = datastream[:treat_as_transcript].present? ? 'transcript' : nil - machine_generated = datastream[:machine_generated].present? ? 'machine_generated' : nil + treat_as_transcript = Avalon::Batch.true_field?(datastream[:treat_as_transcript]) ? 'transcript' : nil + machine_generated = Avalon::Batch.true_field?(datastream[:machine_generated]) ? 'machine_generated' : nil # Create SupplementalFile supplemental_file = SupplementalFile.new(label: label, tags: [type, treat_as_transcript, machine_generated].uniq.compact, language: language, parent_id: parent_id) supplemental_file.file.attach(io: FileLocator.new(datastream[file_key]).reader, filename: filename) diff --git a/lib/avalon/batch/manifest.rb b/lib/avalon/batch/manifest.rb index 7e9d56b454..638bf633fd 100644 --- a/lib/avalon/batch/manifest.rb +++ b/lib/avalon/batch/manifest.rb @@ -99,9 +99,6 @@ def delete end private - def true?(value) - not (value.to_s =~ /^(y(es)?|t(rue)?)$/i).nil? - end def supplementing_files(field, content, values, i) if field.to_s.include?('file') @@ -146,7 +143,7 @@ def create_entries! supplementing_files(f, content, values, i) next end - content.last[f] = f == :skip_transcoding ? true?(values[i]) : values[i] + content.last[f] = f == :skip_transcoding ? Avalon::Batch.true_field?(values[i]) : values[i] else fields[f] << values[i] end @@ -156,7 +153,7 @@ def create_entries! opts.keys.each { |opt| val = Array(fields.delete(opt)).first.to_s if opts[opt].is_a?(TrueClass) or opts[opt].is_a?(FalseClass) - opts[opt] = true?(val) + opts[opt] = Avalon::Batch.true_field?(val) else opts[opt] = val end diff --git a/spec/lib/avalon/batch/entry_spec.rb b/spec/lib/avalon/batch/entry_spec.rb index 72fbe5ad6f..f599877058 100644 --- a/spec/lib/avalon/batch/entry_spec.rb +++ b/spec/lib/avalon/batch/entry_spec.rb @@ -225,8 +225,8 @@ context 'with multiple captions and transcripts' do let(:caption) { [{ :caption_file => caption_file, :caption_label => 'Sheephead Captions', :caption_language => 'english', :treat_as_transcript => 'yes' }, - { :caption_file => caption_file, :caption_label => 'Second Caption', :caption_language => 'fre', :machine_generated => 'yes' }] } - let(:transcript) { [{ :transcript_file => caption_file, :transcript_label => 'Sheephead Transcript' }, + { :caption_file => caption_file, :caption_label => 'Second Caption', :caption_language => 'fre', :machine_generated => 'yes', :treat_as_transcript => 'no' }] } + let(:transcript) { [{ :transcript_file => caption_file, :transcript_label => 'Sheephead Transcript', :machine_generated => 'no' }, { :transcript_file => caption_file, :transcript_language => 'french', :machine_generated => 'yes' }] } it 'should attach all captions and transcripts to master file' do expect(master_file.has_captions?).to eq true From 1906db68e5b47239b516755e437ac6d1fb5b5ebf Mon Sep 17 00:00:00 2001 From: cjcolvar Date: Fri, 12 Jul 2024 14:22:05 -0400 Subject: [PATCH 126/152] Only call display_found_in if q present; Fix case where no transcripts present in document --- app/helpers/catalog_helper.rb | 2 +- app/views/catalog/_index_media_object.html.erb | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/app/helpers/catalog_helper.rb b/app/helpers/catalog_helper.rb index 8c83c6b424..ec884f12ca 100644 --- a/app/helpers/catalog_helper.rb +++ b/app/helpers/catalog_helper.rb @@ -23,7 +23,7 @@ def current_sort_field def display_found_in(document) metadata_count = document.to_h.sum {|k,v| k =~ /metadata_tf_/ ? v : 0 } - transcript_count = document["sections"]["docs"].sum { |d| d["transcripts"]["docs"].sum {|s| s.sum {|k,v| k =~ /transcript_tf_/ ? v : 0 }}} + transcript_count = document["sections"]["docs"].sum { |d| Array(d.dig("transcripts", "docs")).sum {|s| s.sum {|k,v| k =~ /transcript_tf_/ ? v : 0 }}} section_count = document.to_h.sum {|k,v| k =~ /structure_tf_/ ? v : 0 } metadata = "metadata (#{metadata_count})" if metadata_count > 0 diff --git a/app/views/catalog/_index_media_object.html.erb b/app/views/catalog/_index_media_object.html.erb index 400f1360ec..b76f1f3bbf 100644 --- a/app/views/catalog/_index_media_object.html.erb +++ b/app/views/catalog/_index_media_object.html.erb @@ -23,8 +23,7 @@ Unless required by applicable law or agreed to in writing, software distributed
    <%= field_presenter.values.join(', ') %>
    <% end %> <% end %> - <% found_in_hits = display_found_in(doc_presenter.document) %> - <% if params[:q].present? && found_in_hits.present? %> + <% if params[:q].present? && (found_in_hits = display_found_in(doc_presenter.document)).present? %>
    Found in:
    <%= found_in_hits %>
    <% end %> From 99e61b7e4d69111a18d2ce9ed98aa342d7101b39 Mon Sep 17 00:00:00 2001 From: cjcolvar Date: Fri, 12 Jul 2024 14:28:49 -0400 Subject: [PATCH 127/152] Bump version number in preparation of release candidate --- config/application.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/application.rb b/config/application.rb index ad448a4452..1ecb941c0e 100644 --- a/config/application.rb +++ b/config/application.rb @@ -8,7 +8,7 @@ Bundler.require(*Rails.groups) module Avalon - VERSION = '7.7.2' + VERSION = '7.8.0' class Application < Rails::Application require 'avalon/configuration' From 9f7657e48091be799f9521879a9c8bdd3db7d73e Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Fri, 12 Jul 2024 14:53:14 -0400 Subject: [PATCH 128/152] Enable title link for video embeds --- app/javascript/components/embeds/EmbeddedRamp.jsx | 2 +- app/javascript/components/embeds/Ramp.scss | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/app/javascript/components/embeds/EmbeddedRamp.jsx b/app/javascript/components/embeds/EmbeddedRamp.jsx index cc4af2add3..867bc07a96 100644 --- a/app/javascript/components/embeds/EmbeddedRamp.jsx +++ b/app/javascript/components/embeds/EmbeddedRamp.jsx @@ -114,7 +114,7 @@ const Ramp = ({ customErrorMessage='This embed encountered an error. Please refresh or contact an administrator.' startCanvasId={startCanvasId} startCanvasTime={startCanvasTime}> - + ); }; diff --git a/app/javascript/components/embeds/Ramp.scss b/app/javascript/components/embeds/Ramp.scss index 44293fe52f..e563d939ec 100644 --- a/app/javascript/components/embeds/Ramp.scss +++ b/app/javascript/components/embeds/Ramp.scss @@ -48,4 +48,7 @@ .vjs-volume-horizontal .vjs-volume-level .vjs-svg-icon>svg { vertical-align: top; } + .video-js .vjs-title-bar .vjs-title-link { + font-size: 125%; + } } From e7679cc8b899f6dcc1c85aa975270ce9d588495e Mon Sep 17 00:00:00 2001 From: cjcolvar Date: Mon, 15 Jul 2024 16:21:32 -0400 Subject: [PATCH 129/152] Don't add asset_host to assets in packs manifest We set asset_host in config/initializers/default_host.rb for correctly generating links to assets, but when building the production docker image asset_host is set to localhost because the host configuration is unknown at this point. The asset_host doesn't need to be written into the packs manifest and before setting asset_host in the initializers in this version of avalon previous versions had relative asset paths in the packs manifest. See https://github.com/shakacode/shakapacker/blob/main/docs/troubleshooting.md#wrong-cdn-src-from-javascript_pack_tag --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 45119f25e1..0942a04acc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -133,7 +133,7 @@ COPY --from=node-modules --chown=app:app /node_modules ./node_modules USER app ENV RAILS_ENV=production -RUN SECRET_KEY_BASE=$(ruby -r 'securerandom' -e 'puts SecureRandom.hex(64)') bundle exec rake assets:precompile +RUN SECRET_KEY_BASE=$(ruby -r 'securerandom' -e 'puts SecureRandom.hex(64)') SHAKAPACKER_ASSET_HOST='' bundle exec rake assets:precompile RUN cp config/controlled_vocabulary.yml.example config/controlled_vocabulary.yml From e235f8b72b031e85deca59605a23f9c687a5d2a8 Mon Sep 17 00:00:00 2001 From: cjcolvar Date: Tue, 16 Jul 2024 16:09:17 -0400 Subject: [PATCH 130/152] Make sure that ActiveStorage service is set --- config/initializers/active_storage.rb | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 config/initializers/active_storage.rb diff --git a/config/initializers/active_storage.rb b/config/initializers/active_storage.rb new file mode 100644 index 0000000000..71fd303e6c --- /dev/null +++ b/config/initializers/active_storage.rb @@ -0,0 +1,2 @@ +# This is set in config/application.rb but it seems like the settings aren't loaded at that point so doing it again here +Rails.application.config.active_storage.service = (Settings&.active_storage&.service.presence || "local").to_sym From c7c7539396bac4bd7b819499268f4e8c67c221e2 Mon Sep 17 00:00:00 2001 From: cjcolvar Date: Wed, 17 Jul 2024 10:33:09 -0400 Subject: [PATCH 131/152] New timeliner build with time display fixes --- app/javascript/iiif-timeliner-styles.css | 18 +- app/javascript/packs/iiif-timeliner.js | 25042 ++++++++++----------- 2 files changed, 11859 insertions(+), 13201 deletions(-) diff --git a/app/javascript/iiif-timeliner-styles.css b/app/javascript/iiif-timeliner-styles.css index e73d5a9a73..d17e6edbc1 100644 --- a/app/javascript/iiif-timeliner-styles.css +++ b/app/javascript/iiif-timeliner-styles.css @@ -15,23 +15,23 @@ */ @import url(https://fonts.googleapis.com/css?family=Roboto:300,400,500); -*,:after,:before{box-sizing:border-box}body,html{font-family:Roboto,sans-serif;margin:0;padding:0;overflow:hidden;background:#eee;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.homepage{margin:20px}.homepage h1{font-weight:500}.container{max-width:500px;margin:100px auto}.container h1{color:#005cc5;text-align:center}.container code{border:1px solid #ddd;background:#eee}.container .panel{line-height:25px;text-align:justify;background:#dad0e4;padding:1px 15px;margin-bottom:15px}.container .button{border:1px solid #3f50b5;background-color:#3f50b5;color:#fff;-webkit-text-decoration:solid;text-decoration:solid;border-radius:3px;margin:35%;padding:5px}::-webkit-scrollbar{width:2px;height:2px}::-webkit-scrollbar-track{border-radius:0;background:rgba(0,0,0,.1)}::-webkit-scrollbar-thumb{border-radius:0;background:rgba(0,0,0,.2)}::-webkit-scrollbar-thumb:hover{background:rgba(0,0,0,.4)}::-webkit-scrollbar-thumb:window-inactive{background:rgba(0,0,0,.05)}#app{width:100%;height:100%}.documentation{position:fixed;top:0;right:0;background:#000;color:#fff;border-radius:0 0 0 3px;text-decoration:none;padding:10px} +*,:after,:before{box-sizing:border-box}body,html{font-family:Roboto,sans-serif;margin:0;padding:0;background:#eee;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.homepage{margin:20px}.homepage h1{font-weight:500}.container{max-width:500px;margin:100px auto}.container h1{color:#005cc5;text-align:center}.container code{border:1px solid #ddd;background:#eee}.container .panel{line-height:25px;text-align:justify;background:#dad0e4;padding:1px 15px;margin-bottom:15px}.container .button{border:1px solid #3f50b5;background-color:#3f50b5;color:#fff;-webkit-text-decoration:solid;text-decoration:solid;border-radius:3px;margin:35%;padding:5px}::-webkit-scrollbar{width:2px;height:2px}::-webkit-scrollbar-track{border-radius:0;background:rgba(0,0,0,.1)}::-webkit-scrollbar-thumb{border-radius:0;background:rgba(0,0,0,.2)}::-webkit-scrollbar-thumb:hover{background:rgba(0,0,0,.4)}::-webkit-scrollbar-thumb:window-inactive{background:rgba(0,0,0,.05)}#app{width:100%;height:100%}.documentation{position:fixed;top:0;right:0;background:#000;color:#fff;border-radius:0 0 0 3px;text-decoration:none;padding:10px} .variations-app-bar{background:#3f50b5;color:#fff}.variations-app-bar-no-header{background:#fff} -.current-time-indicator{color:#777;margin-right:30px}.current-time-indicator--error{color:darkred}.current-time-indicator__current-time{color:#000}.current-time-indicator__runtime{font-size:.85em} +.current-time-indicator{margin-right:30px}.current-time-indicator--error{color:#8b0000} .zoom-controls{position:relative;top:0;right:0} .audio-transport-bar{padding:8px 30px;background:#fff}.audio-transport-bar__actions{display:flex;align-items:center;flex-direction:row;justify-content:flex-start}.audio-transport-bar__buttons{display:flex;flex-direction:row;align-items:center;justify-content:center}.audio-transport-bar__volume{display:flex;align-items:end;flex-direction:row;justify-content:flex-end}div .audio-transport-bar__tooltip{font-size:12px}.audio-transport-bar .audio-transport-bar__button-text{min-width:40px} .volume-slider-compact{width:160px;position:relative;font-size:0;display:flex;flex-direction:row;justify-content:space-between;align-items:center;margin-left:20px}.volume-slider-compact--flipped{flex-direction:row-reverse;margin-right:20px;margin-left:10px}.volume-slider-compact__muter{margin:0 10px} .colour-swatch-picker__option{width:64px;height:40px} .time-picker{display:flex;flex-direction:row-reverse;align-items:flex-end;justify-content:space-evenly;width:80px;margin-right:8px;margin-top:13px;border:2px solid #ccc;padding:8px}.time-picker__unit{width:21px;border:0;outline:0;-webkit-appearance:none;-moz-appearance:none;border-radius:0} .metadata-editor__button-bar{display:flex;flex-direction:row;justify-content:flex-end;align-items:center} -.metadata{display:flex;flex-direction:row;align-items:stretch;flex-grow:1;flex-shrink:1;flex-basis:0px;min-height:0}.metadata__annotations{flex:2;padding-right:8px;display:flex;flex-direction:column}.metadata__annotations-content{background:#fff;padding:8px 16px;flex:1 1 0px;display:flex;flex-direction:column;overflow-y:auto;overflow-x:hidden}.metadata__video-playback{flex:1;padding-right:8px;display:flex;flex-direction:column}.metadata__video-playback-content{background:#fff;padding:8px 16px;flex:1 1 0px;display:flex;flex-direction:column;overflow-y:auto;overflow-x:hidden}.metadata__project{align-items:stretch;justify-content:stretch}.metadata__project,.metadata__project-content{flex:1 1 0px;display:flex;flex-direction:column}.metadata__project-content{width:100%;background:#fff;padding:8px 16px;overflow-y:auto;overflow-x:hidden}.metadata__content{padding:0 8px} +.metadata{display:flex;flex-direction:row;align-items:stretch;flex-grow:1;flex-shrink:1;flex-basis:0px;min-height:calc(50vh - 48px)}.metadata__annotations{flex:2;padding-right:8px;display:flex;flex-direction:column}.metadata__annotations-content{background:#fff;padding:8px 16px;flex:1 1 0px;display:flex;flex-direction:column;overflow-y:auto;overflow-x:hidden}.metadata__video-playback{flex:1;padding-right:8px;display:flex;flex-direction:column}.metadata__video-playback-content{background:#fff;padding:8px 16px;flex:1 1 0px;display:flex;flex-direction:column;overflow-y:auto;overflow-x:hidden}.metadata__project{align-items:stretch;justify-content:stretch}.metadata__project,.metadata__project-content{flex:1 1 0px;display:flex;flex-direction:column}.metadata__project-content{width:100%;background:#fff;padding:8px 16px;overflow-y:auto;overflow-x:hidden}.metadata__content{padding:0 8px} .file-upload{padding:14px;border:1px solid #ccc;border-radius:2px;margin-top:16px;margin-bottom:20px}.file-upload__label{line-height:34px;margin:0 15px} .footer{margin-top:24px;height:24px} -.content-overlay{width:100%;height:100%;background:hsla(0,0%,100%,.9);position:absolute;z-index:1;display:flex;justify-content:center;align-items:center} -.bubble-display{display:block;vertical-align:top}.bubble-display *{transition:all .3s}.bubble-display--mouseDown *,.bubble-display--mouseDown+.timeline-scrubber{transition:none}.bubble-display__rect{fill:transparent;stroke-width:0;stroke:transparent;cursor:-webkit-grab;cursor:grab}.bubble-display__rect:active{cursor:-webkit-grabbing;cursor:grabbing} -.single-bubble{cursor:pointer;paint-order:stroke}.single-bubble:active{cursor:-webkit-grabbing;cursor:grabbing} -.timeline-marker{position:absolute;width:4px;height:11px;display:block;margin-left:-2px;margin-top:-5px;cursor:-webkit-grab;cursor:grab;z-index:1;background:#000;transition:-webkit-transform .2s;transition:transform .2s;transition:transform .2s,-webkit-transform .2s}.timeline-marker:after{content:"";position:absolute;background:transparent;height:11px;margin-left:-5px;width:11px}.timeline-marker--marker:hover{-webkit-transform:scaleY(2);transform:scaleY(2)}.timeline-marker--marker:active{cursor:-webkit-grabbing;cursor:grabbing;-webkit-transform:scaleY(3);transform:scaleY(3);background:#000;box-shadow:0 5px 10px 0 rgba(0,0,0,.1)}.timeline-marker--bookmark{background:transparent;background:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCI+PHBhdGggZmlsbD0ibm9uZSIgZD0iTTAgMGgyNHYyNEgwVjB6Ii8+PHBhdGggZD0iTTcgMTRsNS01IDUgNUg3eiIvPjwvc3ZnPg==) no-repeat 0 0;background-size:contain;width:29px;height:29px;margin-left:-14px}.timeline-marker--bookmark:hover{-webkit-transform:scale(1.2);transform:scale(1.2)}.timeline-marker--bookmark:active{box-shadow:none;-webkit-transform:scale(1.5) translateY(10px);transform:scale(1.5) translateY(10px)}.timeline-marker--bookmark:active:after{content:"";position:absolute;background:rgba(0,0,0,.5);width:1px;height:16px;top:-5px;left:19px}.timeline-marker__tooltip{position:absolute;top:15px;left:0;margin-left:5px;color:#777;border-radius:2px;font-size:11px;-webkit-transform:translate(-50%);transform:translate(-50%);text-shadow:-1px -1px 0 #fff,1px -1px 0 #fff,-1px 1px 0 #fff,1px 1px 0 #fff} -.playhead{position:absolute;background:#ff4081;left:0;top:0;bottom:0;margin-top:-2px;margin-bottom:-2px;pointer-events:none;overflow:visible;z-index:-1;transition:width .2s linear}.playhead:after{transition:all .4s;content:"";height:100%;border-bottom:0 solid #424242;border-left:0 solid transparent;border-right:0 solid transparent;position:absolute;right:0;top:5px;cursor:ew-resize}.playhead:before{position:absolute;content:"";background:transparent;right:-6px;top:-3px;border-radius:50%;height:12px;width:12px;-webkit-transform:scale(0);transform:scale(0);transition:all .2s}.timeline-scrubber:active .playhead,.timeline-scrubber:hover .playhead{transition:none}.timeline-scrubber:active .playhead:before,.timeline-scrubber:hover .playhead:before{background:#b32d5a;-webkit-transform:scale(1);transform:scale(1)} -.timeline-scrubber{position:relative;height:6px;z-index:2;box-sizing:border-box;border-bottom:2px solid transparent;border-top:2px solid transparent;background:#eee;cursor:pointer;transition:width .3s,margin-left .3s}.timeline-scrubber:focus{outline:none}.timeline-scrubber:before{z-index:1;background:transparent;content:"";width:100%;position:absolute;left:0;right:0;top:-10px;height:20px}.timeline-scrubber__tooltip{position:absolute;background:rgba(0,0,0,.5);transition:opacity .4s;color:#fff;width:80px;height:25px;line-height:25px;text-align:center;top:-35;border-radius:3px;left:0;pointer-events:none} +.content-overlay{width:100%;height:100%;background:hsla(0,0%,100%,.9);position:absolute;z-index:1000;display:flex;justify-content:center;align-items:center} +.bubble-display{display:block;vertical-align:top}.bubble-display *{transition:all .3s}.bubble-display--mouseDown *,.bubble-display--mouseDown+.timeline-scrubber{transition:none}.bubble-display__rect{fill:transparent;stroke-width:0;stroke:transparent;cursor:grab}.bubble-display__rect:active{cursor:grabbing} +.single-bubble{cursor:pointer;paint-order:stroke}.single-bubble:active{cursor:grabbing} +.timeline-marker{position:absolute;width:4px;height:11px;display:block;margin-left:-2px;margin-top:-5px;cursor:grab;z-index:10;background:#000;transition:transform .2s}.timeline-marker:after{content:"";position:absolute;background:transparent;height:11px;margin-left:-5px;width:11px}.timeline-marker--marker:hover{transform:scaleY(2)}.timeline-marker--marker:active{cursor:grabbing;transform:scaleY(3);background:#000;box-shadow:0 5px 10px 0 rgba(0,0,0,.1)}.timeline-marker--bookmark{background:transparent;background:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCI+PHBhdGggZmlsbD0ibm9uZSIgZD0iTTAgMGgyNHYyNEgwVjB6Ii8+PHBhdGggZD0iTTcgMTRsNS01IDUgNUg3eiIvPjwvc3ZnPg==) no-repeat 0 0;background-size:contain;width:29px;height:29px;margin-left:-14px}.timeline-marker--bookmark:hover{transform:scale(1.2)}.timeline-marker--bookmark:active{box-shadow:none;transform:scale(1.5) translateY(10px)}.timeline-marker--bookmark:active:after{content:"";position:absolute;background:rgba(0,0,0,.5);width:1px;height:16px;top:-5px;left:19px}.timeline-marker__tooltip{position:absolute;top:15px;left:0;margin-left:5px;color:#777;border-radius:2px;font-size:11px;transform:translate(-50%);text-shadow:-1px -1px 0 #fff,1px -1px 0 #fff,-1px 1px 0 #fff,1px 1px 0 #fff} +.playhead{position:absolute;background:#ff4081;left:0;top:0;bottom:0;margin-top:-2px;margin-bottom:-2px;pointer-events:none;overflow:visible;z-index:-1;transition:width .2s linear}.playhead:after{transition:all .4s;content:"";height:100%;border-bottom:0 solid #424242;border-left:0 solid transparent;border-right:0 solid transparent;position:absolute;right:0;top:5px;cursor:ew-resize}.playhead:before{position:absolute;content:"";background:transparent;right:-6px;top:-3px;border-radius:50%;height:12px;width:12px;transform:scale(0);transition:all .2s}.timeline-scrubber:active .playhead,.timeline-scrubber:hover .playhead{transition:none}.timeline-scrubber:active .playhead:before,.timeline-scrubber:hover .playhead:before{background:#b32d5a;transform:scale(1)} +.timeline-scrubber{position:relative;height:6px;z-index:10;box-sizing:border-box;border-bottom:2px solid transparent;border-top:2px solid transparent;background:#eee;cursor:pointer;transition:width .3s,margin-left .3s}.timeline-scrubber:focus{outline:none}.timeline-scrubber:before{z-index:1;background:transparent;content:"";width:100%;position:absolute;left:0;right:0;top:-10px;height:20px}.timeline-scrubber__tooltip{position:absolute;background:rgba(0,0,0,.5);transition:opacity .4s;color:#fff;width:80px;height:25px;line-height:25px;text-align:center;top:-35px;border-radius:3px;left:0;pointer-events:none} .variations-app,.variations-app__content{width:100%;height:100%;display:flex;flex-direction:column}.variations-app__metadata-editor{padding:8px;background:#eee;flex:1 1 0px;display:flex;flex-direction:column}.variations-app__metadata-grid{margin-top:24px;height:24px} diff --git a/app/javascript/packs/iiif-timeliner.js b/app/javascript/packs/iiif-timeliner.js index 1658b2c30e..b08d6086e0 100644 --- a/app/javascript/packs/iiif-timeliner.js +++ b/app/javascript/packs/iiif-timeliner.js @@ -111,13195 +111,11853 @@ import "../iiif-timeliner-styles.css" /*! no static exports found */ /***/ (function(module, exports) { - eval("function _arrayLikeToArray(arr, len) {\n if (len == null || len > arr.length) len = arr.length;\n for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i];\n return arr2;\n}\nmodule.exports = _arrayLikeToArray, module.exports.__esModule = true, module.exports[\"default\"] = module.exports;\n\n//# sourceURL=webpack:///./node_modules/@babel/runtime/helpers/arrayLikeToArray.js?"); - - /***/ }), - - /***/ "./node_modules/@babel/runtime/helpers/arrayWithoutHoles.js": - /*!******************************************************************!*\ - !*** ./node_modules/@babel/runtime/helpers/arrayWithoutHoles.js ***! - \******************************************************************/ - /*! no static exports found */ - /***/ (function(module, exports, __webpack_require__) { - - eval("var arrayLikeToArray = __webpack_require__(/*! ./arrayLikeToArray.js */ \"./node_modules/@babel/runtime/helpers/arrayLikeToArray.js\");\nfunction _arrayWithoutHoles(arr) {\n if (Array.isArray(arr)) return arrayLikeToArray(arr);\n}\nmodule.exports = _arrayWithoutHoles, module.exports.__esModule = true, module.exports[\"default\"] = module.exports;\n\n//# sourceURL=webpack:///./node_modules/@babel/runtime/helpers/arrayWithoutHoles.js?"); - - /***/ }), - - /***/ "./node_modules/@babel/runtime/helpers/assertThisInitialized.js": - /*!**********************************************************************!*\ - !*** ./node_modules/@babel/runtime/helpers/assertThisInitialized.js ***! - \**********************************************************************/ - /*! no static exports found */ - /***/ (function(module, exports) { - - eval("function _assertThisInitialized(self) {\n if (self === void 0) {\n throw new ReferenceError(\"this hasn't been initialised - super() hasn't been called\");\n }\n return self;\n}\nmodule.exports = _assertThisInitialized, module.exports.__esModule = true, module.exports[\"default\"] = module.exports;\n\n//# sourceURL=webpack:///./node_modules/@babel/runtime/helpers/assertThisInitialized.js?"); - - /***/ }), - - /***/ "./node_modules/@babel/runtime/helpers/classCallCheck.js": - /*!***************************************************************!*\ - !*** ./node_modules/@babel/runtime/helpers/classCallCheck.js ***! - \***************************************************************/ - /*! no static exports found */ - /***/ (function(module, exports) { - - eval("function _classCallCheck(instance, Constructor) {\n if (!(instance instanceof Constructor)) {\n throw new TypeError(\"Cannot call a class as a function\");\n }\n}\nmodule.exports = _classCallCheck, module.exports.__esModule = true, module.exports[\"default\"] = module.exports;\n\n//# sourceURL=webpack:///./node_modules/@babel/runtime/helpers/classCallCheck.js?"); - - /***/ }), - - /***/ "./node_modules/@babel/runtime/helpers/createClass.js": - /*!************************************************************!*\ - !*** ./node_modules/@babel/runtime/helpers/createClass.js ***! - \************************************************************/ - /*! no static exports found */ - /***/ (function(module, exports, __webpack_require__) { - - eval("var toPropertyKey = __webpack_require__(/*! ./toPropertyKey.js */ \"./node_modules/@babel/runtime/helpers/toPropertyKey.js\");\nfunction _defineProperties(target, props) {\n for (var i = 0; i < props.length; i++) {\n var descriptor = props[i];\n descriptor.enumerable = descriptor.enumerable || false;\n descriptor.configurable = true;\n if (\"value\" in descriptor) descriptor.writable = true;\n Object.defineProperty(target, toPropertyKey(descriptor.key), descriptor);\n }\n}\nfunction _createClass(Constructor, protoProps, staticProps) {\n if (protoProps) _defineProperties(Constructor.prototype, protoProps);\n if (staticProps) _defineProperties(Constructor, staticProps);\n Object.defineProperty(Constructor, \"prototype\", {\n writable: false\n });\n return Constructor;\n}\nmodule.exports = _createClass, module.exports.__esModule = true, module.exports[\"default\"] = module.exports;\n\n//# sourceURL=webpack:///./node_modules/@babel/runtime/helpers/createClass.js?"); - - /***/ }), - - /***/ "./node_modules/@babel/runtime/helpers/defineProperty.js": - /*!***************************************************************!*\ - !*** ./node_modules/@babel/runtime/helpers/defineProperty.js ***! - \***************************************************************/ - /*! no static exports found */ - /***/ (function(module, exports, __webpack_require__) { - - eval("var toPropertyKey = __webpack_require__(/*! ./toPropertyKey.js */ \"./node_modules/@babel/runtime/helpers/toPropertyKey.js\");\nfunction _defineProperty(obj, key, value) {\n key = toPropertyKey(key);\n if (key in obj) {\n Object.defineProperty(obj, key, {\n value: value,\n enumerable: true,\n configurable: true,\n writable: true\n });\n } else {\n obj[key] = value;\n }\n return obj;\n}\nmodule.exports = _defineProperty, module.exports.__esModule = true, module.exports[\"default\"] = module.exports;\n\n//# sourceURL=webpack:///./node_modules/@babel/runtime/helpers/defineProperty.js?"); - - /***/ }), - - /***/ "./node_modules/@babel/runtime/helpers/esm/assertThisInitialized.js": - /*!**************************************************************************!*\ - !*** ./node_modules/@babel/runtime/helpers/esm/assertThisInitialized.js ***! - \**************************************************************************/ - /*! exports provided: default */ - /***/ (function(module, __webpack_exports__, __webpack_require__) { - - "use strict"; - eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"default\", function() { return _assertThisInitialized; });\nfunction _assertThisInitialized(self) {\n if (self === void 0) {\n throw new ReferenceError(\"this hasn't been initialised - super() hasn't been called\");\n }\n return self;\n}\n\n//# sourceURL=webpack:///./node_modules/@babel/runtime/helpers/esm/assertThisInitialized.js?"); - - /***/ }), - - /***/ "./node_modules/@babel/runtime/helpers/esm/defineProperty.js": - /*!*******************************************************************!*\ - !*** ./node_modules/@babel/runtime/helpers/esm/defineProperty.js ***! - \*******************************************************************/ - /*! exports provided: default */ - /***/ (function(module, __webpack_exports__, __webpack_require__) { - - "use strict"; - eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"default\", function() { return _defineProperty; });\n/* harmony import */ var _toPropertyKey_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./toPropertyKey.js */ \"./node_modules/@babel/runtime/helpers/esm/toPropertyKey.js\");\n\nfunction _defineProperty(obj, key, value) {\n key = Object(_toPropertyKey_js__WEBPACK_IMPORTED_MODULE_0__[\"default\"])(key);\n if (key in obj) {\n Object.defineProperty(obj, key, {\n value: value,\n enumerable: true,\n configurable: true,\n writable: true\n });\n } else {\n obj[key] = value;\n }\n return obj;\n}\n\n//# sourceURL=webpack:///./node_modules/@babel/runtime/helpers/esm/defineProperty.js?"); - - /***/ }), - - /***/ "./node_modules/@babel/runtime/helpers/esm/extends.js": - /*!************************************************************!*\ - !*** ./node_modules/@babel/runtime/helpers/esm/extends.js ***! - \************************************************************/ - /*! exports provided: default */ - /***/ (function(module, __webpack_exports__, __webpack_require__) { - - "use strict"; - eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"default\", function() { return _extends; });\nfunction _extends() {\n _extends = Object.assign ? Object.assign.bind() : function (target) {\n for (var i = 1; i < arguments.length; i++) {\n var source = arguments[i];\n for (var key in source) {\n if (Object.prototype.hasOwnProperty.call(source, key)) {\n target[key] = source[key];\n }\n }\n }\n return target;\n };\n return _extends.apply(this, arguments);\n}\n\n//# sourceURL=webpack:///./node_modules/@babel/runtime/helpers/esm/extends.js?"); - - /***/ }), - - /***/ "./node_modules/@babel/runtime/helpers/esm/inheritsLoose.js": - /*!******************************************************************!*\ - !*** ./node_modules/@babel/runtime/helpers/esm/inheritsLoose.js ***! - \******************************************************************/ - /*! exports provided: default */ - /***/ (function(module, __webpack_exports__, __webpack_require__) { - - "use strict"; - eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"default\", function() { return _inheritsLoose; });\n/* harmony import */ var _setPrototypeOf_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./setPrototypeOf.js */ \"./node_modules/@babel/runtime/helpers/esm/setPrototypeOf.js\");\n\nfunction _inheritsLoose(subClass, superClass) {\n subClass.prototype = Object.create(superClass.prototype);\n subClass.prototype.constructor = subClass;\n Object(_setPrototypeOf_js__WEBPACK_IMPORTED_MODULE_0__[\"default\"])(subClass, superClass);\n}\n\n//# sourceURL=webpack:///./node_modules/@babel/runtime/helpers/esm/inheritsLoose.js?"); - - /***/ }), - - /***/ "./node_modules/@babel/runtime/helpers/esm/objectSpread2.js": - /*!******************************************************************!*\ - !*** ./node_modules/@babel/runtime/helpers/esm/objectSpread2.js ***! - \******************************************************************/ - /*! exports provided: default */ - /***/ (function(module, __webpack_exports__, __webpack_require__) { - - "use strict"; - eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"default\", function() { return _objectSpread2; });\n/* harmony import */ var _defineProperty_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./defineProperty.js */ \"./node_modules/@babel/runtime/helpers/esm/defineProperty.js\");\n\nfunction ownKeys(object, enumerableOnly) {\n var keys = Object.keys(object);\n if (Object.getOwnPropertySymbols) {\n var symbols = Object.getOwnPropertySymbols(object);\n enumerableOnly && (symbols = symbols.filter(function (sym) {\n return Object.getOwnPropertyDescriptor(object, sym).enumerable;\n })), keys.push.apply(keys, symbols);\n }\n return keys;\n}\nfunction _objectSpread2(target) {\n for (var i = 1; i < arguments.length; i++) {\n var source = null != arguments[i] ? arguments[i] : {};\n i % 2 ? ownKeys(Object(source), !0).forEach(function (key) {\n Object(_defineProperty_js__WEBPACK_IMPORTED_MODULE_0__[\"default\"])(target, key, source[key]);\n }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) {\n Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key));\n });\n }\n return target;\n}\n\n//# sourceURL=webpack:///./node_modules/@babel/runtime/helpers/esm/objectSpread2.js?"); - - /***/ }), - - /***/ "./node_modules/@babel/runtime/helpers/esm/objectWithoutPropertiesLoose.js": - /*!*********************************************************************************!*\ - !*** ./node_modules/@babel/runtime/helpers/esm/objectWithoutPropertiesLoose.js ***! - \*********************************************************************************/ - /*! exports provided: default */ - /***/ (function(module, __webpack_exports__, __webpack_require__) { - - "use strict"; - eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"default\", function() { return _objectWithoutPropertiesLoose; });\nfunction _objectWithoutPropertiesLoose(source, excluded) {\n if (source == null) return {};\n var target = {};\n var sourceKeys = Object.keys(source);\n var key, i;\n for (i = 0; i < sourceKeys.length; i++) {\n key = sourceKeys[i];\n if (excluded.indexOf(key) >= 0) continue;\n target[key] = source[key];\n }\n return target;\n}\n\n//# sourceURL=webpack:///./node_modules/@babel/runtime/helpers/esm/objectWithoutPropertiesLoose.js?"); - - /***/ }), - - /***/ "./node_modules/@babel/runtime/helpers/esm/setPrototypeOf.js": - /*!*******************************************************************!*\ - !*** ./node_modules/@babel/runtime/helpers/esm/setPrototypeOf.js ***! - \*******************************************************************/ - /*! exports provided: default */ - /***/ (function(module, __webpack_exports__, __webpack_require__) { - - "use strict"; - eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"default\", function() { return _setPrototypeOf; });\nfunction _setPrototypeOf(o, p) {\n _setPrototypeOf = Object.setPrototypeOf ? Object.setPrototypeOf.bind() : function _setPrototypeOf(o, p) {\n o.__proto__ = p;\n return o;\n };\n return _setPrototypeOf(o, p);\n}\n\n//# sourceURL=webpack:///./node_modules/@babel/runtime/helpers/esm/setPrototypeOf.js?"); - - /***/ }), - - /***/ "./node_modules/@babel/runtime/helpers/esm/toPrimitive.js": - /*!****************************************************************!*\ - !*** ./node_modules/@babel/runtime/helpers/esm/toPrimitive.js ***! - \****************************************************************/ - /*! exports provided: default */ - /***/ (function(module, __webpack_exports__, __webpack_require__) { - - "use strict"; - eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"default\", function() { return _toPrimitive; });\n/* harmony import */ var _typeof_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./typeof.js */ \"./node_modules/@babel/runtime/helpers/esm/typeof.js\");\n\nfunction _toPrimitive(input, hint) {\n if (Object(_typeof_js__WEBPACK_IMPORTED_MODULE_0__[\"default\"])(input) !== \"object\" || input === null) return input;\n var prim = input[Symbol.toPrimitive];\n if (prim !== undefined) {\n var res = prim.call(input, hint || \"default\");\n if (Object(_typeof_js__WEBPACK_IMPORTED_MODULE_0__[\"default\"])(res) !== \"object\") return res;\n throw new TypeError(\"@@toPrimitive must return a primitive value.\");\n }\n return (hint === \"string\" ? String : Number)(input);\n}\n\n//# sourceURL=webpack:///./node_modules/@babel/runtime/helpers/esm/toPrimitive.js?"); - - /***/ }), - - /***/ "./node_modules/@babel/runtime/helpers/esm/toPropertyKey.js": - /*!******************************************************************!*\ - !*** ./node_modules/@babel/runtime/helpers/esm/toPropertyKey.js ***! - \******************************************************************/ - /*! exports provided: default */ - /***/ (function(module, __webpack_exports__, __webpack_require__) { - - "use strict"; - eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"default\", function() { return _toPropertyKey; });\n/* harmony import */ var _typeof_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./typeof.js */ \"./node_modules/@babel/runtime/helpers/esm/typeof.js\");\n/* harmony import */ var _toPrimitive_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./toPrimitive.js */ \"./node_modules/@babel/runtime/helpers/esm/toPrimitive.js\");\n\n\nfunction _toPropertyKey(arg) {\n var key = Object(_toPrimitive_js__WEBPACK_IMPORTED_MODULE_1__[\"default\"])(arg, \"string\");\n return Object(_typeof_js__WEBPACK_IMPORTED_MODULE_0__[\"default\"])(key) === \"symbol\" ? key : String(key);\n}\n\n//# sourceURL=webpack:///./node_modules/@babel/runtime/helpers/esm/toPropertyKey.js?"); - - /***/ }), - - /***/ "./node_modules/@babel/runtime/helpers/esm/typeof.js": - /*!***********************************************************!*\ - !*** ./node_modules/@babel/runtime/helpers/esm/typeof.js ***! - \***********************************************************/ - /*! exports provided: default */ - /***/ (function(module, __webpack_exports__, __webpack_require__) { - - "use strict"; - eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"default\", function() { return _typeof; });\nfunction _typeof(obj) {\n \"@babel/helpers - typeof\";\n\n return _typeof = \"function\" == typeof Symbol && \"symbol\" == typeof Symbol.iterator ? function (obj) {\n return typeof obj;\n } : function (obj) {\n return obj && \"function\" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? \"symbol\" : typeof obj;\n }, _typeof(obj);\n}\n\n//# sourceURL=webpack:///./node_modules/@babel/runtime/helpers/esm/typeof.js?"); - - /***/ }), - - /***/ "./node_modules/@babel/runtime/helpers/extends.js": - /*!********************************************************!*\ - !*** ./node_modules/@babel/runtime/helpers/extends.js ***! - \********************************************************/ - /*! no static exports found */ - /***/ (function(module, exports) { - - eval("function _extends() {\n module.exports = _extends = Object.assign ? Object.assign.bind() : function (target) {\n for (var i = 1; i < arguments.length; i++) {\n var source = arguments[i];\n for (var key in source) {\n if (Object.prototype.hasOwnProperty.call(source, key)) {\n target[key] = source[key];\n }\n }\n }\n return target;\n }, module.exports.__esModule = true, module.exports[\"default\"] = module.exports;\n return _extends.apply(this, arguments);\n}\nmodule.exports = _extends, module.exports.__esModule = true, module.exports[\"default\"] = module.exports;\n\n//# sourceURL=webpack:///./node_modules/@babel/runtime/helpers/extends.js?"); - - /***/ }), - - /***/ "./node_modules/@babel/runtime/helpers/getPrototypeOf.js": - /*!***************************************************************!*\ - !*** ./node_modules/@babel/runtime/helpers/getPrototypeOf.js ***! - \***************************************************************/ - /*! no static exports found */ - /***/ (function(module, exports) { - - eval("function _getPrototypeOf(o) {\n module.exports = _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf.bind() : function _getPrototypeOf(o) {\n return o.__proto__ || Object.getPrototypeOf(o);\n }, module.exports.__esModule = true, module.exports[\"default\"] = module.exports;\n return _getPrototypeOf(o);\n}\nmodule.exports = _getPrototypeOf, module.exports.__esModule = true, module.exports[\"default\"] = module.exports;\n\n//# sourceURL=webpack:///./node_modules/@babel/runtime/helpers/getPrototypeOf.js?"); - - /***/ }), - - /***/ "./node_modules/@babel/runtime/helpers/inherits.js": - /*!*********************************************************!*\ - !*** ./node_modules/@babel/runtime/helpers/inherits.js ***! - \*********************************************************/ - /*! no static exports found */ - /***/ (function(module, exports, __webpack_require__) { - - eval("var setPrototypeOf = __webpack_require__(/*! ./setPrototypeOf.js */ \"./node_modules/@babel/runtime/helpers/setPrototypeOf.js\");\nfunction _inherits(subClass, superClass) {\n if (typeof superClass !== \"function\" && superClass !== null) {\n throw new TypeError(\"Super expression must either be null or a function\");\n }\n subClass.prototype = Object.create(superClass && superClass.prototype, {\n constructor: {\n value: subClass,\n writable: true,\n configurable: true\n }\n });\n Object.defineProperty(subClass, \"prototype\", {\n writable: false\n });\n if (superClass) setPrototypeOf(subClass, superClass);\n}\nmodule.exports = _inherits, module.exports.__esModule = true, module.exports[\"default\"] = module.exports;\n\n//# sourceURL=webpack:///./node_modules/@babel/runtime/helpers/inherits.js?"); - - /***/ }), - - /***/ "./node_modules/@babel/runtime/helpers/inheritsLoose.js": - /*!**************************************************************!*\ - !*** ./node_modules/@babel/runtime/helpers/inheritsLoose.js ***! - \**************************************************************/ - /*! no static exports found */ - /***/ (function(module, exports, __webpack_require__) { - - eval("var setPrototypeOf = __webpack_require__(/*! ./setPrototypeOf.js */ \"./node_modules/@babel/runtime/helpers/setPrototypeOf.js\");\nfunction _inheritsLoose(subClass, superClass) {\n subClass.prototype = Object.create(superClass.prototype);\n subClass.prototype.constructor = subClass;\n setPrototypeOf(subClass, superClass);\n}\nmodule.exports = _inheritsLoose, module.exports.__esModule = true, module.exports[\"default\"] = module.exports;\n\n//# sourceURL=webpack:///./node_modules/@babel/runtime/helpers/inheritsLoose.js?"); - - /***/ }), - - /***/ "./node_modules/@babel/runtime/helpers/interopRequireDefault.js": - /*!**********************************************************************!*\ - !*** ./node_modules/@babel/runtime/helpers/interopRequireDefault.js ***! - \**********************************************************************/ - /*! no static exports found */ - /***/ (function(module, exports) { - - eval("function _interopRequireDefault(obj) {\n return obj && obj.__esModule ? obj : {\n \"default\": obj\n };\n}\nmodule.exports = _interopRequireDefault, module.exports.__esModule = true, module.exports[\"default\"] = module.exports;\n\n//# sourceURL=webpack:///./node_modules/@babel/runtime/helpers/interopRequireDefault.js?"); - - /***/ }), - - /***/ "./node_modules/@babel/runtime/helpers/interopRequireWildcard.js": - /*!***********************************************************************!*\ - !*** ./node_modules/@babel/runtime/helpers/interopRequireWildcard.js ***! - \***********************************************************************/ - /*! no static exports found */ - /***/ (function(module, exports, __webpack_require__) { - - eval("var _typeof = __webpack_require__(/*! ./typeof.js */ \"./node_modules/@babel/runtime/helpers/typeof.js\")[\"default\"];\nfunction _getRequireWildcardCache(nodeInterop) {\n if (typeof WeakMap !== \"function\") return null;\n var cacheBabelInterop = new WeakMap();\n var cacheNodeInterop = new WeakMap();\n return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) {\n return nodeInterop ? cacheNodeInterop : cacheBabelInterop;\n })(nodeInterop);\n}\nfunction _interopRequireWildcard(obj, nodeInterop) {\n if (!nodeInterop && obj && obj.__esModule) {\n return obj;\n }\n if (obj === null || _typeof(obj) !== \"object\" && typeof obj !== \"function\") {\n return {\n \"default\": obj\n };\n }\n var cache = _getRequireWildcardCache(nodeInterop);\n if (cache && cache.has(obj)) {\n return cache.get(obj);\n }\n var newObj = {};\n var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor;\n for (var key in obj) {\n if (key !== \"default\" && Object.prototype.hasOwnProperty.call(obj, key)) {\n var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null;\n if (desc && (desc.get || desc.set)) {\n Object.defineProperty(newObj, key, desc);\n } else {\n newObj[key] = obj[key];\n }\n }\n }\n newObj[\"default\"] = obj;\n if (cache) {\n cache.set(obj, newObj);\n }\n return newObj;\n}\nmodule.exports = _interopRequireWildcard, module.exports.__esModule = true, module.exports[\"default\"] = module.exports;\n\n//# sourceURL=webpack:///./node_modules/@babel/runtime/helpers/interopRequireWildcard.js?"); - - /***/ }), - - /***/ "./node_modules/@babel/runtime/helpers/iterableToArray.js": - /*!****************************************************************!*\ - !*** ./node_modules/@babel/runtime/helpers/iterableToArray.js ***! - \****************************************************************/ - /*! no static exports found */ - /***/ (function(module, exports) { - - eval("function _iterableToArray(iter) {\n if (typeof Symbol !== \"undefined\" && iter[Symbol.iterator] != null || iter[\"@@iterator\"] != null) return Array.from(iter);\n}\nmodule.exports = _iterableToArray, module.exports.__esModule = true, module.exports[\"default\"] = module.exports;\n\n//# sourceURL=webpack:///./node_modules/@babel/runtime/helpers/iterableToArray.js?"); - - /***/ }), - - /***/ "./node_modules/@babel/runtime/helpers/nonIterableSpread.js": - /*!******************************************************************!*\ - !*** ./node_modules/@babel/runtime/helpers/nonIterableSpread.js ***! - \******************************************************************/ - /*! no static exports found */ - /***/ (function(module, exports) { - - eval("function _nonIterableSpread() {\n throw new TypeError(\"Invalid attempt to spread non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.\");\n}\nmodule.exports = _nonIterableSpread, module.exports.__esModule = true, module.exports[\"default\"] = module.exports;\n\n//# sourceURL=webpack:///./node_modules/@babel/runtime/helpers/nonIterableSpread.js?"); - - /***/ }), - - /***/ "./node_modules/@babel/runtime/helpers/objectWithoutProperties.js": - /*!************************************************************************!*\ - !*** ./node_modules/@babel/runtime/helpers/objectWithoutProperties.js ***! - \************************************************************************/ - /*! no static exports found */ - /***/ (function(module, exports, __webpack_require__) { - - eval("var objectWithoutPropertiesLoose = __webpack_require__(/*! ./objectWithoutPropertiesLoose.js */ \"./node_modules/@babel/runtime/helpers/objectWithoutPropertiesLoose.js\");\nfunction _objectWithoutProperties(source, excluded) {\n if (source == null) return {};\n var target = objectWithoutPropertiesLoose(source, excluded);\n var key, i;\n if (Object.getOwnPropertySymbols) {\n var sourceSymbolKeys = Object.getOwnPropertySymbols(source);\n for (i = 0; i < sourceSymbolKeys.length; i++) {\n key = sourceSymbolKeys[i];\n if (excluded.indexOf(key) >= 0) continue;\n if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue;\n target[key] = source[key];\n }\n }\n return target;\n}\nmodule.exports = _objectWithoutProperties, module.exports.__esModule = true, module.exports[\"default\"] = module.exports;\n\n//# sourceURL=webpack:///./node_modules/@babel/runtime/helpers/objectWithoutProperties.js?"); - - /***/ }), - - /***/ "./node_modules/@babel/runtime/helpers/objectWithoutPropertiesLoose.js": - /*!*****************************************************************************!*\ - !*** ./node_modules/@babel/runtime/helpers/objectWithoutPropertiesLoose.js ***! - \*****************************************************************************/ - /*! no static exports found */ - /***/ (function(module, exports) { - - eval("function _objectWithoutPropertiesLoose(source, excluded) {\n if (source == null) return {};\n var target = {};\n var sourceKeys = Object.keys(source);\n var key, i;\n for (i = 0; i < sourceKeys.length; i++) {\n key = sourceKeys[i];\n if (excluded.indexOf(key) >= 0) continue;\n target[key] = source[key];\n }\n return target;\n}\nmodule.exports = _objectWithoutPropertiesLoose, module.exports.__esModule = true, module.exports[\"default\"] = module.exports;\n\n//# sourceURL=webpack:///./node_modules/@babel/runtime/helpers/objectWithoutPropertiesLoose.js?"); - - /***/ }), - - /***/ "./node_modules/@babel/runtime/helpers/possibleConstructorReturn.js": - /*!**************************************************************************!*\ - !*** ./node_modules/@babel/runtime/helpers/possibleConstructorReturn.js ***! - \**************************************************************************/ - /*! no static exports found */ - /***/ (function(module, exports, __webpack_require__) { - - eval("var _typeof = __webpack_require__(/*! ./typeof.js */ \"./node_modules/@babel/runtime/helpers/typeof.js\")[\"default\"];\nvar assertThisInitialized = __webpack_require__(/*! ./assertThisInitialized.js */ \"./node_modules/@babel/runtime/helpers/assertThisInitialized.js\");\nfunction _possibleConstructorReturn(self, call) {\n if (call && (_typeof(call) === \"object\" || typeof call === \"function\")) {\n return call;\n } else if (call !== void 0) {\n throw new TypeError(\"Derived constructors may only return object or undefined\");\n }\n return assertThisInitialized(self);\n}\nmodule.exports = _possibleConstructorReturn, module.exports.__esModule = true, module.exports[\"default\"] = module.exports;\n\n//# sourceURL=webpack:///./node_modules/@babel/runtime/helpers/possibleConstructorReturn.js?"); - - /***/ }), - - /***/ "./node_modules/@babel/runtime/helpers/setPrototypeOf.js": - /*!***************************************************************!*\ - !*** ./node_modules/@babel/runtime/helpers/setPrototypeOf.js ***! - \***************************************************************/ - /*! no static exports found */ - /***/ (function(module, exports) { - - eval("function _setPrototypeOf(o, p) {\n module.exports = _setPrototypeOf = Object.setPrototypeOf ? Object.setPrototypeOf.bind() : function _setPrototypeOf(o, p) {\n o.__proto__ = p;\n return o;\n }, module.exports.__esModule = true, module.exports[\"default\"] = module.exports;\n return _setPrototypeOf(o, p);\n}\nmodule.exports = _setPrototypeOf, module.exports.__esModule = true, module.exports[\"default\"] = module.exports;\n\n//# sourceURL=webpack:///./node_modules/@babel/runtime/helpers/setPrototypeOf.js?"); - - /***/ }), - - /***/ "./node_modules/@babel/runtime/helpers/toConsumableArray.js": - /*!******************************************************************!*\ - !*** ./node_modules/@babel/runtime/helpers/toConsumableArray.js ***! - \******************************************************************/ - /*! no static exports found */ - /***/ (function(module, exports, __webpack_require__) { - - eval("var arrayWithoutHoles = __webpack_require__(/*! ./arrayWithoutHoles.js */ \"./node_modules/@babel/runtime/helpers/arrayWithoutHoles.js\");\nvar iterableToArray = __webpack_require__(/*! ./iterableToArray.js */ \"./node_modules/@babel/runtime/helpers/iterableToArray.js\");\nvar unsupportedIterableToArray = __webpack_require__(/*! ./unsupportedIterableToArray.js */ \"./node_modules/@babel/runtime/helpers/unsupportedIterableToArray.js\");\nvar nonIterableSpread = __webpack_require__(/*! ./nonIterableSpread.js */ \"./node_modules/@babel/runtime/helpers/nonIterableSpread.js\");\nfunction _toConsumableArray(arr) {\n return arrayWithoutHoles(arr) || iterableToArray(arr) || unsupportedIterableToArray(arr) || nonIterableSpread();\n}\nmodule.exports = _toConsumableArray, module.exports.__esModule = true, module.exports[\"default\"] = module.exports;\n\n//# sourceURL=webpack:///./node_modules/@babel/runtime/helpers/toConsumableArray.js?"); - - /***/ }), - - /***/ "./node_modules/@babel/runtime/helpers/toPrimitive.js": - /*!************************************************************!*\ - !*** ./node_modules/@babel/runtime/helpers/toPrimitive.js ***! - \************************************************************/ - /*! no static exports found */ - /***/ (function(module, exports, __webpack_require__) { - - eval("var _typeof = __webpack_require__(/*! ./typeof.js */ \"./node_modules/@babel/runtime/helpers/typeof.js\")[\"default\"];\nfunction _toPrimitive(input, hint) {\n if (_typeof(input) !== \"object\" || input === null) return input;\n var prim = input[Symbol.toPrimitive];\n if (prim !== undefined) {\n var res = prim.call(input, hint || \"default\");\n if (_typeof(res) !== \"object\") return res;\n throw new TypeError(\"@@toPrimitive must return a primitive value.\");\n }\n return (hint === \"string\" ? String : Number)(input);\n}\nmodule.exports = _toPrimitive, module.exports.__esModule = true, module.exports[\"default\"] = module.exports;\n\n//# sourceURL=webpack:///./node_modules/@babel/runtime/helpers/toPrimitive.js?"); - - /***/ }), - - /***/ "./node_modules/@babel/runtime/helpers/toPropertyKey.js": - /*!**************************************************************!*\ - !*** ./node_modules/@babel/runtime/helpers/toPropertyKey.js ***! - \**************************************************************/ - /*! no static exports found */ - /***/ (function(module, exports, __webpack_require__) { - - eval("var _typeof = __webpack_require__(/*! ./typeof.js */ \"./node_modules/@babel/runtime/helpers/typeof.js\")[\"default\"];\nvar toPrimitive = __webpack_require__(/*! ./toPrimitive.js */ \"./node_modules/@babel/runtime/helpers/toPrimitive.js\");\nfunction _toPropertyKey(arg) {\n var key = toPrimitive(arg, \"string\");\n return _typeof(key) === \"symbol\" ? key : String(key);\n}\nmodule.exports = _toPropertyKey, module.exports.__esModule = true, module.exports[\"default\"] = module.exports;\n\n//# sourceURL=webpack:///./node_modules/@babel/runtime/helpers/toPropertyKey.js?"); - - /***/ }), - - /***/ "./node_modules/@babel/runtime/helpers/typeof.js": - /*!*******************************************************!*\ - !*** ./node_modules/@babel/runtime/helpers/typeof.js ***! - \*******************************************************/ - /*! no static exports found */ - /***/ (function(module, exports) { - - eval("function _typeof(obj) {\n \"@babel/helpers - typeof\";\n\n return (module.exports = _typeof = \"function\" == typeof Symbol && \"symbol\" == typeof Symbol.iterator ? function (obj) {\n return typeof obj;\n } : function (obj) {\n return obj && \"function\" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? \"symbol\" : typeof obj;\n }, module.exports.__esModule = true, module.exports[\"default\"] = module.exports), _typeof(obj);\n}\nmodule.exports = _typeof, module.exports.__esModule = true, module.exports[\"default\"] = module.exports;\n\n//# sourceURL=webpack:///./node_modules/@babel/runtime/helpers/typeof.js?"); - - /***/ }), - - /***/ "./node_modules/@babel/runtime/helpers/unsupportedIterableToArray.js": - /*!***************************************************************************!*\ - !*** ./node_modules/@babel/runtime/helpers/unsupportedIterableToArray.js ***! - \***************************************************************************/ - /*! no static exports found */ - /***/ (function(module, exports, __webpack_require__) { - - eval("var arrayLikeToArray = __webpack_require__(/*! ./arrayLikeToArray.js */ \"./node_modules/@babel/runtime/helpers/arrayLikeToArray.js\");\nfunction _unsupportedIterableToArray(o, minLen) {\n if (!o) return;\n if (typeof o === \"string\") return arrayLikeToArray(o, minLen);\n var n = Object.prototype.toString.call(o).slice(8, -1);\n if (n === \"Object\" && o.constructor) n = o.constructor.name;\n if (n === \"Map\" || n === \"Set\") return Array.from(o);\n if (n === \"Arguments\" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return arrayLikeToArray(o, minLen);\n}\nmodule.exports = _unsupportedIterableToArray, module.exports.__esModule = true, module.exports[\"default\"] = module.exports;\n\n//# sourceURL=webpack:///./node_modules/@babel/runtime/helpers/unsupportedIterableToArray.js?"); - - /***/ }), - - /***/ "./node_modules/@fesk/bem-js/lib/index.js": - /*!************************************************!*\ - !*** ./node_modules/@fesk/bem-js/lib/index.js ***! - \************************************************/ - /*! no static exports found */ - /***/ (function(module, exports, __webpack_require__) { - - "use strict"; - eval("\n\nexports.__esModule = true;\n\nfunction _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError(\"this hasn't been initialised - super() hasn't been called\"); } return call && (typeof call === \"object\" || typeof call === \"function\") ? call : self; }\n\nfunction _inherits(subClass, superClass) { if (typeof superClass !== \"function\" && superClass !== null) { throw new TypeError(\"Super expression must either be null or a function, not \" + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }\n\nfunction _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError(\"Cannot call a class as a function\"); } }\n\n/**\n * Copyright (c) 2017-present, Digirati Limited.\n * All rights reserved.\n */\n\nvar Element = function () {\n function Element(name) {\n _classCallCheck(this, Element);\n\n this.name = name;\n }\n\n Element.prototype.modifier = function modifier(m, c) {\n if (m.toString() === '[object Object]') return this.ms(m);\n if (!c && c !== undefined) {\n return this.name;\n }\n return this.name + ' ' + this.name + '--' + m;\n };\n\n Element.prototype.m = function m(_m, c) {\n this.modifier(_m, c);\n };\n\n Element.prototype.modifiers = function modifiers(m) {\n var join = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;\n\n var ms = [this.name];\n for (var k in m) {\n if (m.hasOwnProperty(k) && m[k]) {\n ms.push(this.name + '--' + k);\n }\n }\n return join ? ms.join(' ') : ms;\n };\n\n Element.prototype.ms = function ms(m, join) {\n this.modifiers(m, join);\n };\n\n Element.prototype.toString = function toString() {\n return this.name;\n };\n\n return Element;\n}();\n\nvar Block = function (_Element) {\n _inherits(Block, _Element);\n\n function Block() {\n var _temp, _this, _ret;\n\n _classCallCheck(this, Block);\n\n for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {\n args[_key] = arguments[_key];\n }\n\n return _ret = (_temp = (_this = _possibleConstructorReturn(this, _Element.call.apply(_Element, [this].concat(args))), _this), _this.e = _this.element, _temp), _possibleConstructorReturn(_this, _ret);\n }\n\n Block.prototype.element = function element(name) {\n return new Element(this.name + '__' + name);\n };\n\n return Block;\n}(Element);\n\nvar BEM = {\n block: function block(blockName) {\n // Block.\n return new Block(blockName);\n }\n};\n\nBEM.b = BEM.block;\n\nexports.default = BEM;\nmodule.exports = exports['default'];\n\n//# sourceURL=webpack:///./node_modules/@fesk/bem-js/lib/index.js?"); - - /***/ }), - - /***/ "./node_modules/@icons/material/CheckIcon.js": - /*!***************************************************!*\ - !*** ./node_modules/@icons/material/CheckIcon.js ***! - \***************************************************/ - /*! no static exports found */ - /***/ (function(module, exports, __webpack_require__) { - - "use strict"; - eval("\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\n\nvar _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };\n\nvar _react = __webpack_require__(/*! react */ \"./node_modules/react/index.js\");\n\nvar _react2 = _interopRequireDefault(_react);\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\nfunction _objectWithoutProperties(obj, keys) { var target = {}; for (var i in obj) { if (keys.indexOf(i) >= 0) continue; if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; target[i] = obj[i]; } return target; }\n\nvar DEFAULT_SIZE = 24;\n\nexports.default = function (_ref) {\n var _ref$fill = _ref.fill,\n fill = _ref$fill === undefined ? 'currentColor' : _ref$fill,\n _ref$width = _ref.width,\n width = _ref$width === undefined ? DEFAULT_SIZE : _ref$width,\n _ref$height = _ref.height,\n height = _ref$height === undefined ? DEFAULT_SIZE : _ref$height,\n _ref$style = _ref.style,\n style = _ref$style === undefined ? {} : _ref$style,\n props = _objectWithoutProperties(_ref, ['fill', 'width', 'height', 'style']);\n\n return _react2.default.createElement(\n 'svg',\n _extends({\n viewBox: '0 0 ' + DEFAULT_SIZE + ' ' + DEFAULT_SIZE,\n style: _extends({ fill: fill, width: width, height: height }, style)\n }, props),\n _react2.default.createElement('path', { d: 'M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z' })\n );\n};\n\n//# sourceURL=webpack:///./node_modules/@icons/material/CheckIcon.js?"); - - /***/ }), - - /***/ "./node_modules/@icons/material/UnfoldMoreHorizontalIcon.js": - /*!******************************************************************!*\ - !*** ./node_modules/@icons/material/UnfoldMoreHorizontalIcon.js ***! - \******************************************************************/ - /*! no static exports found */ - /***/ (function(module, exports, __webpack_require__) { - - "use strict"; - eval("\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\n\nvar _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };\n\nvar _react = __webpack_require__(/*! react */ \"./node_modules/react/index.js\");\n\nvar _react2 = _interopRequireDefault(_react);\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\nfunction _objectWithoutProperties(obj, keys) { var target = {}; for (var i in obj) { if (keys.indexOf(i) >= 0) continue; if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; target[i] = obj[i]; } return target; }\n\nvar DEFAULT_SIZE = 24;\n\nexports.default = function (_ref) {\n var _ref$fill = _ref.fill,\n fill = _ref$fill === undefined ? 'currentColor' : _ref$fill,\n _ref$width = _ref.width,\n width = _ref$width === undefined ? DEFAULT_SIZE : _ref$width,\n _ref$height = _ref.height,\n height = _ref$height === undefined ? DEFAULT_SIZE : _ref$height,\n _ref$style = _ref.style,\n style = _ref$style === undefined ? {} : _ref$style,\n props = _objectWithoutProperties(_ref, ['fill', 'width', 'height', 'style']);\n\n return _react2.default.createElement(\n 'svg',\n _extends({\n viewBox: '0 0 ' + DEFAULT_SIZE + ' ' + DEFAULT_SIZE,\n style: _extends({ fill: fill, width: width, height: height }, style)\n }, props),\n _react2.default.createElement('path', { d: 'M12,18.17L8.83,15L7.42,16.41L12,21L16.59,16.41L15.17,15M12,5.83L15.17,9L16.58,7.59L12,3L7.41,7.59L8.83,9L12,5.83Z' })\n );\n};\n\n//# sourceURL=webpack:///./node_modules/@icons/material/UnfoldMoreHorizontalIcon.js?"); - - /***/ }), - - /***/ "./node_modules/@material-ui/core/Backdrop/Backdrop.js": - /*!*************************************************************!*\ - !*** ./node_modules/@material-ui/core/Backdrop/Backdrop.js ***! - \*************************************************************/ - /*! no static exports found */ - /***/ (function(module, exports, __webpack_require__) { - - "use strict"; - eval("\n\nvar _interopRequireDefault = __webpack_require__(/*! @babel/runtime/helpers/interopRequireDefault */ \"./node_modules/@babel/runtime/helpers/interopRequireDefault.js\");\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.default = exports.styles = void 0;\n\nvar _extends2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/extends */ \"./node_modules/@babel/runtime/helpers/extends.js\"));\n\nvar _defineProperty2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/defineProperty */ \"./node_modules/@babel/runtime/helpers/defineProperty.js\"));\n\nvar _objectWithoutProperties2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/objectWithoutProperties */ \"./node_modules/@babel/runtime/helpers/objectWithoutProperties.js\"));\n\nvar _react = _interopRequireDefault(__webpack_require__(/*! react */ \"./node_modules/react/index.js\"));\n\nvar _propTypes = _interopRequireDefault(__webpack_require__(/*! prop-types */ \"./node_modules/prop-types/index.js\"));\n\nvar _classnames = _interopRequireDefault(__webpack_require__(/*! classnames */ \"./node_modules/classnames/index.js\"));\n\nvar _withStyles = _interopRequireDefault(__webpack_require__(/*! ../styles/withStyles */ \"./node_modules/@material-ui/core/styles/withStyles.js\"));\n\nvar _Fade = _interopRequireDefault(__webpack_require__(/*! ../Fade */ \"./node_modules/@material-ui/core/Fade/index.js\"));\n\nvar styles = {\n /* Styles applied to the root element. */\n root: {\n zIndex: -1,\n position: 'fixed',\n right: 0,\n bottom: 0,\n top: 0,\n left: 0,\n backgroundColor: 'rgba(0, 0, 0, 0.5)',\n // Remove grey highlight\n WebkitTapHighlightColor: 'transparent',\n // Disable scroll capabilities.\n touchAction: 'none'\n },\n\n /* Styles applied to the root element if `invisible={true}`. */\n invisible: {\n backgroundColor: 'transparent'\n }\n};\nexports.styles = styles;\n\nfunction Backdrop(props) {\n var classes = props.classes,\n className = props.className,\n invisible = props.invisible,\n open = props.open,\n transitionDuration = props.transitionDuration,\n other = (0, _objectWithoutProperties2.default)(props, [\"classes\", \"className\", \"invisible\", \"open\", \"transitionDuration\"]);\n return _react.default.createElement(_Fade.default, (0, _extends2.default)({\n in: open,\n timeout: transitionDuration\n }, other), _react.default.createElement(\"div\", {\n className: (0, _classnames.default)(classes.root, (0, _defineProperty2.default)({}, classes.invisible, invisible), className),\n \"aria-hidden\": \"true\"\n }));\n}\n\n true ? Backdrop.propTypes = {\n /**\n * Override or extend the styles applied to the component.\n * See [CSS API](#css-api) below for more details.\n */\n classes: _propTypes.default.object.isRequired,\n\n /**\n * @ignore\n */\n className: _propTypes.default.string,\n\n /**\n * If `true`, the backdrop is invisible.\n * It can be used when rendering a popover or a custom select component.\n */\n invisible: _propTypes.default.bool,\n\n /**\n * If `true`, the backdrop is open.\n */\n open: _propTypes.default.bool.isRequired,\n\n /**\n * The duration for the transition, in milliseconds.\n * You may specify a single timeout for all transitions, or individually with an object.\n */\n transitionDuration: _propTypes.default.oneOfType([_propTypes.default.number, _propTypes.default.shape({\n enter: _propTypes.default.number,\n exit: _propTypes.default.number\n })])\n} : undefined;\nBackdrop.defaultProps = {\n invisible: false\n};\n\nvar _default = (0, _withStyles.default)(styles, {\n name: 'MuiBackdrop'\n})(Backdrop);\n\nexports.default = _default;\n\n//# sourceURL=webpack:///./node_modules/@material-ui/core/Backdrop/Backdrop.js?"); - - /***/ }), - - /***/ "./node_modules/@material-ui/core/Backdrop/index.js": - /*!**********************************************************!*\ - !*** ./node_modules/@material-ui/core/Backdrop/index.js ***! - \**********************************************************/ - /*! no static exports found */ - /***/ (function(module, exports, __webpack_require__) { - - "use strict"; - eval("\n\nvar _interopRequireDefault = __webpack_require__(/*! @babel/runtime/helpers/interopRequireDefault */ \"./node_modules/@babel/runtime/helpers/interopRequireDefault.js\");\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nObject.defineProperty(exports, \"default\", {\n enumerable: true,\n get: function get() {\n return _Backdrop.default;\n }\n});\n\nvar _Backdrop = _interopRequireDefault(__webpack_require__(/*! ./Backdrop */ \"./node_modules/@material-ui/core/Backdrop/Backdrop.js\"));\n\n//# sourceURL=webpack:///./node_modules/@material-ui/core/Backdrop/index.js?"); - - /***/ }), - - /***/ "./node_modules/@material-ui/core/Button/Button.js": - /*!*********************************************************!*\ - !*** ./node_modules/@material-ui/core/Button/Button.js ***! - \*********************************************************/ - /*! no static exports found */ - /***/ (function(module, exports, __webpack_require__) { - - "use strict"; - eval("\n\nvar _interopRequireDefault = __webpack_require__(/*! @babel/runtime/helpers/interopRequireDefault */ \"./node_modules/@babel/runtime/helpers/interopRequireDefault.js\");\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.default = exports.styles = void 0;\n\nvar _defineProperty2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/defineProperty */ \"./node_modules/@babel/runtime/helpers/defineProperty.js\"));\n\nvar _objectWithoutProperties2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/objectWithoutProperties */ \"./node_modules/@babel/runtime/helpers/objectWithoutProperties.js\"));\n\nvar _extends2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/extends */ \"./node_modules/@babel/runtime/helpers/extends.js\"));\n\nvar _react = _interopRequireDefault(__webpack_require__(/*! react */ \"./node_modules/react/index.js\"));\n\nvar _propTypes = _interopRequireDefault(__webpack_require__(/*! prop-types */ \"./node_modules/prop-types/index.js\"));\n\nvar _classnames = _interopRequireDefault(__webpack_require__(/*! classnames */ \"./node_modules/classnames/index.js\"));\n\nvar _utils = __webpack_require__(/*! @material-ui/utils */ \"./node_modules/@material-ui/utils/index.es.js\");\n\nvar _withStyles = _interopRequireDefault(__webpack_require__(/*! ../styles/withStyles */ \"./node_modules/@material-ui/core/styles/withStyles.js\"));\n\nvar _colorManipulator = __webpack_require__(/*! ../styles/colorManipulator */ \"./node_modules/@material-ui/core/styles/colorManipulator.js\");\n\nvar _ButtonBase = _interopRequireDefault(__webpack_require__(/*! ../ButtonBase */ \"./node_modules/@material-ui/core/ButtonBase/index.js\"));\n\nvar _helpers = __webpack_require__(/*! ../utils/helpers */ \"./node_modules/@material-ui/core/utils/helpers.js\");\n\n// @inheritedComponent ButtonBase\nvar styles = function styles(theme) {\n return {\n /* Styles applied to the root element. */\n root: (0, _extends2.default)({\n lineHeight: 1.75\n }, theme.typography.button, {\n boxSizing: 'border-box',\n minWidth: 64,\n padding: '6px 16px',\n borderRadius: theme.shape.borderRadius,\n color: theme.palette.text.primary,\n transition: theme.transitions.create(['background-color', 'box-shadow', 'border'], {\n duration: theme.transitions.duration.short\n }),\n '&:hover': {\n textDecoration: 'none',\n backgroundColor: (0, _colorManipulator.fade)(theme.palette.text.primary, theme.palette.action.hoverOpacity),\n // Reset on touch devices, it doesn't add specificity\n '@media (hover: none)': {\n backgroundColor: 'transparent'\n },\n '&$disabled': {\n backgroundColor: 'transparent'\n }\n },\n '&$disabled': {\n color: theme.palette.action.disabled\n }\n }),\n\n /* Styles applied to the span element that wraps the children. */\n label: {\n width: '100%',\n // assure the correct width for iOS Safari\n display: 'inherit',\n alignItems: 'inherit',\n justifyContent: 'inherit'\n },\n\n /* Styles applied to the root element if `variant=\"text\"`. */\n text: {\n padding: '6px 8px'\n },\n\n /* Styles applied to the root element if `variant=\"text\"` and `color=\"primary\"`. */\n textPrimary: {\n color: theme.palette.primary.main,\n '&:hover': {\n backgroundColor: (0, _colorManipulator.fade)(theme.palette.primary.main, theme.palette.action.hoverOpacity),\n // Reset on touch devices, it doesn't add specificity\n '@media (hover: none)': {\n backgroundColor: 'transparent'\n }\n }\n },\n\n /* Styles applied to the root element if `variant=\"text\"` and `color=\"secondary\"`. */\n textSecondary: {\n color: theme.palette.secondary.main,\n '&:hover': {\n backgroundColor: (0, _colorManipulator.fade)(theme.palette.secondary.main, theme.palette.action.hoverOpacity),\n // Reset on touch devices, it doesn't add specificity\n '@media (hover: none)': {\n backgroundColor: 'transparent'\n }\n }\n },\n\n /* Styles applied to the root element for backwards compatibility with legacy variant naming. */\n flat: {},\n\n /* Styles applied to the root element for backwards compatibility with legacy variant naming. */\n flatPrimary: {},\n\n /* Styles applied to the root element for backwards compatibility with legacy variant naming. */\n flatSecondary: {},\n\n /* Styles applied to the root element if `variant=\"outlined\"`. */\n outlined: {\n padding: '5px 16px',\n border: \"1px solid \".concat(theme.palette.type === 'light' ? 'rgba(0, 0, 0, 0.23)' : 'rgba(255, 255, 255, 0.23)'),\n '&$disabled': {\n border: \"1px solid \".concat(theme.palette.action.disabled)\n }\n },\n\n /* Styles applied to the root element if `variant=\"outlined\"` and `color=\"primary\"`. */\n outlinedPrimary: {\n color: theme.palette.primary.main,\n border: \"1px solid \".concat((0, _colorManipulator.fade)(theme.palette.primary.main, 0.5)),\n '&:hover': {\n border: \"1px solid \".concat(theme.palette.primary.main),\n backgroundColor: (0, _colorManipulator.fade)(theme.palette.primary.main, theme.palette.action.hoverOpacity),\n // Reset on touch devices, it doesn't add specificity\n '@media (hover: none)': {\n backgroundColor: 'transparent'\n }\n }\n },\n\n /* Styles applied to the root element if `variant=\"outlined\"` and `color=\"secondary\"`. */\n outlinedSecondary: {\n color: theme.palette.secondary.main,\n border: \"1px solid \".concat((0, _colorManipulator.fade)(theme.palette.secondary.main, 0.5)),\n '&:hover': {\n border: \"1px solid \".concat(theme.palette.secondary.main),\n backgroundColor: (0, _colorManipulator.fade)(theme.palette.secondary.main, theme.palette.action.hoverOpacity),\n // Reset on touch devices, it doesn't add specificity\n '@media (hover: none)': {\n backgroundColor: 'transparent'\n }\n },\n '&$disabled': {\n border: \"1px solid \".concat(theme.palette.action.disabled)\n }\n },\n\n /* Styles applied to the root element if `variant=\"[contained | fab]\"`. */\n contained: {\n color: theme.palette.getContrastText(theme.palette.grey[300]),\n backgroundColor: theme.palette.grey[300],\n boxShadow: theme.shadows[2],\n '&$focusVisible': {\n boxShadow: theme.shadows[6]\n },\n '&:active': {\n boxShadow: theme.shadows[8]\n },\n '&$disabled': {\n color: theme.palette.action.disabled,\n boxShadow: theme.shadows[0],\n backgroundColor: theme.palette.action.disabledBackground\n },\n '&:hover': {\n backgroundColor: theme.palette.grey.A100,\n // Reset on touch devices, it doesn't add specificity\n '@media (hover: none)': {\n backgroundColor: theme.palette.grey[300]\n },\n '&$disabled': {\n backgroundColor: theme.palette.action.disabledBackground\n }\n }\n },\n\n /* Styles applied to the root element if `variant=\"[contained | fab]\"` and `color=\"primary\"`. */\n containedPrimary: {\n color: theme.palette.primary.contrastText,\n backgroundColor: theme.palette.primary.main,\n '&:hover': {\n backgroundColor: theme.palette.primary.dark,\n // Reset on touch devices, it doesn't add specificity\n '@media (hover: none)': {\n backgroundColor: theme.palette.primary.main\n }\n }\n },\n\n /* Styles applied to the root element if `variant=\"[contained | fab]\"` and `color=\"secondary\"`. */\n containedSecondary: {\n color: theme.palette.secondary.contrastText,\n backgroundColor: theme.palette.secondary.main,\n '&:hover': {\n backgroundColor: theme.palette.secondary.dark,\n // Reset on touch devices, it doesn't add specificity\n '@media (hover: none)': {\n backgroundColor: theme.palette.secondary.main\n }\n }\n },\n\n /* Styles applied to the root element for backwards compatibility with legacy variant naming. */\n raised: {},\n // legacy\n\n /* Styles applied to the root element for backwards compatibility with legacy variant naming. */\n raisedPrimary: {},\n // legacy\n\n /* Styles applied to the root element for backwards compatibility with legacy variant naming. */\n raisedSecondary: {},\n // legacy\n\n /* Styles applied to the root element if `variant=\"[fab | extendedFab]\"`. */\n fab: {\n borderRadius: '50%',\n padding: 0,\n minWidth: 0,\n width: 56,\n height: 56,\n boxShadow: theme.shadows[6],\n '&:active': {\n boxShadow: theme.shadows[12]\n }\n },\n\n /* Styles applied to the root element if `variant=\"extendedFab\"`. */\n extendedFab: {\n borderRadius: 48 / 2,\n padding: '0 16px',\n width: 'auto',\n minWidth: 48,\n height: 48\n },\n\n /* Styles applied to the ButtonBase root element if the button is keyboard focused. */\n focusVisible: {},\n\n /* Styles applied to the root element if `disabled={true}`. */\n disabled: {},\n\n /* Styles applied to the root element if `color=\"inherit\"`. */\n colorInherit: {\n color: 'inherit',\n borderColor: 'currentColor'\n },\n\n /* Styles applied to the root element if `mini={true}` & `variant=\"[fab | extendedFab]\"`. */\n mini: {\n width: 40,\n height: 40\n },\n\n /* Styles applied to the root element if `size=\"small\"`. */\n sizeSmall: {\n padding: '4px 8px',\n minWidth: 64,\n fontSize: theme.typography.pxToRem(13)\n },\n\n /* Styles applied to the root element if `size=\"large\"`. */\n sizeLarge: {\n padding: '8px 24px',\n fontSize: theme.typography.pxToRem(15)\n },\n\n /* Styles applied to the root element if `fullWidth={true}`. */\n fullWidth: {\n width: '100%'\n }\n };\n};\n\nexports.styles = styles;\n\nfunction Button(props) {\n var _classNames;\n\n var children = props.children,\n classes = props.classes,\n classNameProp = props.className,\n color = props.color,\n disabled = props.disabled,\n disableFocusRipple = props.disableFocusRipple,\n focusVisibleClassName = props.focusVisibleClassName,\n fullWidth = props.fullWidth,\n mini = props.mini,\n size = props.size,\n variant = props.variant,\n other = (0, _objectWithoutProperties2.default)(props, [\"children\", \"classes\", \"className\", \"color\", \"disabled\", \"disableFocusRipple\", \"focusVisibleClassName\", \"fullWidth\", \"mini\", \"size\", \"variant\"]);\n var fab = variant === 'fab' || variant === 'extendedFab';\n var contained = variant === 'contained' || variant === 'raised';\n var text = variant === 'text' || variant === 'flat';\n var className = (0, _classnames.default)(classes.root, (_classNames = {}, (0, _defineProperty2.default)(_classNames, classes.fab, fab), (0, _defineProperty2.default)(_classNames, classes.mini, fab && mini), (0, _defineProperty2.default)(_classNames, classes.extendedFab, variant === 'extendedFab'), (0, _defineProperty2.default)(_classNames, classes.text, text), (0, _defineProperty2.default)(_classNames, classes.textPrimary, text && color === 'primary'), (0, _defineProperty2.default)(_classNames, classes.textSecondary, text && color === 'secondary'), (0, _defineProperty2.default)(_classNames, classes.flat, text), (0, _defineProperty2.default)(_classNames, classes.flatPrimary, text && color === 'primary'), (0, _defineProperty2.default)(_classNames, classes.flatSecondary, text && color === 'secondary'), (0, _defineProperty2.default)(_classNames, classes.contained, contained || fab), (0, _defineProperty2.default)(_classNames, classes.containedPrimary, (contained || fab) && color === 'primary'), (0, _defineProperty2.default)(_classNames, classes.containedSecondary, (contained || fab) && color === 'secondary'), (0, _defineProperty2.default)(_classNames, classes.raised, contained || fab), (0, _defineProperty2.default)(_classNames, classes.raisedPrimary, (contained || fab) && color === 'primary'), (0, _defineProperty2.default)(_classNames, classes.raisedSecondary, (contained || fab) && color === 'secondary'), (0, _defineProperty2.default)(_classNames, classes.outlined, variant === 'outlined'), (0, _defineProperty2.default)(_classNames, classes.outlinedPrimary, variant === 'outlined' && color === 'primary'), (0, _defineProperty2.default)(_classNames, classes.outlinedSecondary, variant === 'outlined' && color === 'secondary'), (0, _defineProperty2.default)(_classNames, classes[\"size\".concat((0, _helpers.capitalize)(size))], size !== 'medium'), (0, _defineProperty2.default)(_classNames, classes.disabled, disabled), (0, _defineProperty2.default)(_classNames, classes.fullWidth, fullWidth), (0, _defineProperty2.default)(_classNames, classes.colorInherit, color === 'inherit'), _classNames), classNameProp);\n return _react.default.createElement(_ButtonBase.default, (0, _extends2.default)({\n className: className,\n disabled: disabled,\n focusRipple: !disableFocusRipple,\n focusVisibleClassName: (0, _classnames.default)(classes.focusVisible, focusVisibleClassName)\n }, other), _react.default.createElement(\"span\", {\n className: classes.label\n }, children));\n}\n\n true ? Button.propTypes = {\n /**\n * The content of the button.\n */\n children: _propTypes.default.node.isRequired,\n\n /**\n * Override or extend the styles applied to the component.\n * See [CSS API](#css-api) below for more details.\n */\n classes: _propTypes.default.object.isRequired,\n\n /**\n * @ignore\n */\n className: _propTypes.default.string,\n\n /**\n * The color of the component. It supports those theme colors that make sense for this component.\n */\n color: _propTypes.default.oneOf(['default', 'inherit', 'primary', 'secondary']),\n\n /**\n * The component used for the root node.\n * Either a string to use a DOM element or a component.\n */\n component: _utils.componentPropType,\n\n /**\n * If `true`, the button will be disabled.\n */\n disabled: _propTypes.default.bool,\n\n /**\n * If `true`, the keyboard focus ripple will be disabled.\n * `disableRipple` must also be true.\n */\n disableFocusRipple: _propTypes.default.bool,\n\n /**\n * If `true`, the ripple effect will be disabled.\n */\n disableRipple: _propTypes.default.bool,\n\n /**\n * @ignore\n */\n focusVisibleClassName: _propTypes.default.string,\n\n /**\n * If `true`, the button will take up the full width of its container.\n */\n fullWidth: _propTypes.default.bool,\n\n /**\n * The URL to link to when the button is clicked.\n * If defined, an `a` element will be used as the root node.\n */\n href: _propTypes.default.string,\n\n /**\n * If `true`, and `variant` is `'fab'`, will use mini floating action button styling.\n */\n mini: _propTypes.default.bool,\n\n /**\n * The size of the button.\n * `small` is equivalent to the dense button styling.\n */\n size: _propTypes.default.oneOf(['small', 'medium', 'large']),\n\n /**\n * @ignore\n */\n type: _propTypes.default.string,\n\n /**\n * The variant to use.\n * __WARNING__: `flat` and `raised` are deprecated.\n * Instead use `text` and `contained` respectively.\n * `fab` and `extendedFab` are deprecated.\n * Instead use `` and ``\n */\n variant: (0, _utils.chainPropTypes)(_propTypes.default.oneOf(['text', 'outlined', 'contained', 'fab', 'extendedFab', 'flat', 'raised']), function (props) {\n if (props.variant === 'flat') {\n return new Error('Material-UI: the `flat` variant will be removed in the next major release. ' + '`text` is equivalent and should be used instead.');\n }\n\n if (props.variant === 'raised') {\n return new Error('Material-UI: the `raised` variant will be removed in the next major release. ' + '`contained` is equivalent and should be used instead.');\n }\n\n if (props.variant === 'fab') {\n return new Error('Material-UI: the `fab` variant will be removed in the next major release. ' + 'The `` component is equivalent and should be used instead.');\n }\n\n if (props.variant === 'extendedFab') {\n return new Error('Material-UI: the `fab` variant will be removed in the next major release. ' + 'The `` component with `variant=\"extended\"` is equivalent ' + 'and should be used instead.');\n }\n\n return null;\n })\n} : undefined;\nButton.defaultProps = {\n color: 'default',\n component: 'button',\n disabled: false,\n disableFocusRipple: false,\n fullWidth: false,\n mini: false,\n size: 'medium',\n type: 'button',\n variant: 'text'\n};\n\nvar _default = (0, _withStyles.default)(styles, {\n name: 'MuiButton'\n})(Button);\n\nexports.default = _default;\n\n//# sourceURL=webpack:///./node_modules/@material-ui/core/Button/Button.js?"); - - /***/ }), - - /***/ "./node_modules/@material-ui/core/Button/index.js": - /*!********************************************************!*\ - !*** ./node_modules/@material-ui/core/Button/index.js ***! - \********************************************************/ - /*! no static exports found */ - /***/ (function(module, exports, __webpack_require__) { - - "use strict"; - eval("\n\nvar _interopRequireDefault = __webpack_require__(/*! @babel/runtime/helpers/interopRequireDefault */ \"./node_modules/@babel/runtime/helpers/interopRequireDefault.js\");\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nObject.defineProperty(exports, \"default\", {\n enumerable: true,\n get: function get() {\n return _Button.default;\n }\n});\n\nvar _Button = _interopRequireDefault(__webpack_require__(/*! ./Button */ \"./node_modules/@material-ui/core/Button/Button.js\"));\n\n//# sourceURL=webpack:///./node_modules/@material-ui/core/Button/index.js?"); - - /***/ }), - - /***/ "./node_modules/@material-ui/core/ButtonBase/ButtonBase.js": - /*!*****************************************************************!*\ - !*** ./node_modules/@material-ui/core/ButtonBase/ButtonBase.js ***! - \*****************************************************************/ - /*! no static exports found */ - /***/ (function(module, exports, __webpack_require__) { - - "use strict"; - eval("\n\nvar _interopRequireDefault = __webpack_require__(/*! @babel/runtime/helpers/interopRequireDefault */ \"./node_modules/@babel/runtime/helpers/interopRequireDefault.js\");\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.default = exports.styles = void 0;\n\nvar _extends2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/extends */ \"./node_modules/@babel/runtime/helpers/extends.js\"));\n\nvar _defineProperty2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/defineProperty */ \"./node_modules/@babel/runtime/helpers/defineProperty.js\"));\n\nvar _objectWithoutProperties2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/objectWithoutProperties */ \"./node_modules/@babel/runtime/helpers/objectWithoutProperties.js\"));\n\nvar _classCallCheck2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/classCallCheck */ \"./node_modules/@babel/runtime/helpers/classCallCheck.js\"));\n\nvar _createClass2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/createClass */ \"./node_modules/@babel/runtime/helpers/createClass.js\"));\n\nvar _possibleConstructorReturn2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/possibleConstructorReturn */ \"./node_modules/@babel/runtime/helpers/possibleConstructorReturn.js\"));\n\nvar _getPrototypeOf3 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/getPrototypeOf */ \"./node_modules/@babel/runtime/helpers/getPrototypeOf.js\"));\n\nvar _inherits2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/inherits */ \"./node_modules/@babel/runtime/helpers/inherits.js\"));\n\nvar _assertThisInitialized2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/assertThisInitialized */ \"./node_modules/@babel/runtime/helpers/assertThisInitialized.js\"));\n\nvar _react = _interopRequireDefault(__webpack_require__(/*! react */ \"./node_modules/react/index.js\"));\n\nvar _propTypes = _interopRequireDefault(__webpack_require__(/*! prop-types */ \"./node_modules/prop-types/index.js\"));\n\nvar _reactDom = _interopRequireDefault(__webpack_require__(/*! react-dom */ \"./node_modules/react-dom/index.js\"));\n\nvar _classnames = _interopRequireDefault(__webpack_require__(/*! classnames */ \"./node_modules/classnames/index.js\"));\n\nvar _utils = __webpack_require__(/*! @material-ui/utils */ \"./node_modules/@material-ui/utils/index.es.js\");\n\nvar _ownerWindow = _interopRequireDefault(__webpack_require__(/*! ../utils/ownerWindow */ \"./node_modules/@material-ui/core/utils/ownerWindow.js\"));\n\nvar _withStyles = _interopRequireDefault(__webpack_require__(/*! ../styles/withStyles */ \"./node_modules/@material-ui/core/styles/withStyles.js\"));\n\nvar _NoSsr = _interopRequireDefault(__webpack_require__(/*! ../NoSsr */ \"./node_modules/@material-ui/core/NoSsr/index.js\"));\n\nvar _focusVisible = __webpack_require__(/*! ./focusVisible */ \"./node_modules/@material-ui/core/ButtonBase/focusVisible.js\");\n\nvar _TouchRipple = _interopRequireDefault(__webpack_require__(/*! ./TouchRipple */ \"./node_modules/@material-ui/core/ButtonBase/TouchRipple.js\"));\n\nvar _createRippleHandler = _interopRequireDefault(__webpack_require__(/*! ./createRippleHandler */ \"./node_modules/@material-ui/core/ButtonBase/createRippleHandler.js\"));\n\nvar styles = {\n /* Styles applied to the root element. */\n root: {\n display: 'inline-flex',\n alignItems: 'center',\n justifyContent: 'center',\n position: 'relative',\n // Remove grey highlight\n WebkitTapHighlightColor: 'transparent',\n backgroundColor: 'transparent',\n // Reset default value\n // We disable the focus ring for mouse, touch and keyboard users.\n outline: 'none',\n border: 0,\n margin: 0,\n // Remove the margin in Safari\n borderRadius: 0,\n padding: 0,\n // Remove the padding in Firefox\n cursor: 'pointer',\n userSelect: 'none',\n verticalAlign: 'middle',\n '-moz-appearance': 'none',\n // Reset\n '-webkit-appearance': 'none',\n // Reset\n textDecoration: 'none',\n // So we take precedent over the style of a native element.\n color: 'inherit',\n '&::-moz-focus-inner': {\n borderStyle: 'none' // Remove Firefox dotted outline.\n\n },\n '&$disabled': {\n pointerEvents: 'none',\n // Disable link interactions\n cursor: 'default'\n }\n },\n\n /* Styles applied to the root element if `disabled={true}`. */\n disabled: {},\n\n /* Styles applied to the root element if keyboard focused. */\n focusVisible: {}\n};\n/* istanbul ignore if */\n\nexports.styles = styles;\n\nif ( true && !_react.default.createContext) {\n throw new Error('Material-UI: react@16.3.0 or greater is required.');\n}\n/**\n * `ButtonBase` contains as few styles as possible.\n * It aims to be a simple building block for creating a button.\n * It contains a load of style reset and some focus/ripple logic.\n */\n\n\nvar ButtonBase =\n/*#__PURE__*/\nfunction (_React$Component) {\n (0, _inherits2.default)(ButtonBase, _React$Component);\n\n function ButtonBase() {\n var _getPrototypeOf2;\n\n var _this;\n\n (0, _classCallCheck2.default)(this, ButtonBase);\n\n for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {\n args[_key] = arguments[_key];\n }\n\n _this = (0, _possibleConstructorReturn2.default)(this, (_getPrototypeOf2 = (0, _getPrototypeOf3.default)(ButtonBase)).call.apply(_getPrototypeOf2, [this].concat(args)));\n _this.state = {};\n _this.keyDown = false;\n _this.focusVisibleCheckTime = 50;\n _this.focusVisibleMaxCheckTimes = 5;\n _this.handleMouseDown = (0, _createRippleHandler.default)((0, _assertThisInitialized2.default)((0, _assertThisInitialized2.default)(_this)), 'MouseDown', 'start', function () {\n clearTimeout(_this.focusVisibleTimeout);\n\n if (_this.state.focusVisible) {\n _this.setState({\n focusVisible: false\n });\n }\n });\n _this.handleMouseUp = (0, _createRippleHandler.default)((0, _assertThisInitialized2.default)((0, _assertThisInitialized2.default)(_this)), 'MouseUp', 'stop');\n _this.handleMouseLeave = (0, _createRippleHandler.default)((0, _assertThisInitialized2.default)((0, _assertThisInitialized2.default)(_this)), 'MouseLeave', 'stop', function (event) {\n if (_this.state.focusVisible) {\n event.preventDefault();\n }\n });\n _this.handleTouchStart = (0, _createRippleHandler.default)((0, _assertThisInitialized2.default)((0, _assertThisInitialized2.default)(_this)), 'TouchStart', 'start');\n _this.handleTouchEnd = (0, _createRippleHandler.default)((0, _assertThisInitialized2.default)((0, _assertThisInitialized2.default)(_this)), 'TouchEnd', 'stop');\n _this.handleTouchMove = (0, _createRippleHandler.default)((0, _assertThisInitialized2.default)((0, _assertThisInitialized2.default)(_this)), 'TouchMove', 'stop');\n _this.handleContextMenu = (0, _createRippleHandler.default)((0, _assertThisInitialized2.default)((0, _assertThisInitialized2.default)(_this)), 'ContextMenu', 'stop');\n _this.handleBlur = (0, _createRippleHandler.default)((0, _assertThisInitialized2.default)((0, _assertThisInitialized2.default)(_this)), 'Blur', 'stop', function () {\n clearTimeout(_this.focusVisibleTimeout);\n\n if (_this.state.focusVisible) {\n _this.setState({\n focusVisible: false\n });\n }\n });\n\n _this.onRippleRef = function (node) {\n _this.ripple = node;\n };\n\n _this.onFocusVisibleHandler = function (event) {\n _this.keyDown = false;\n\n _this.setState({\n focusVisible: true\n });\n\n if (_this.props.onFocusVisible) {\n _this.props.onFocusVisible(event);\n }\n };\n\n _this.handleKeyDown = function (event) {\n var _this$props = _this.props,\n component = _this$props.component,\n focusRipple = _this$props.focusRipple,\n onKeyDown = _this$props.onKeyDown,\n onClick = _this$props.onClick; // Check if key is already down to avoid repeats being counted as multiple activations\n\n if (focusRipple && !_this.keyDown && _this.state.focusVisible && _this.ripple && event.key === ' ') {\n _this.keyDown = true;\n event.persist();\n\n _this.ripple.stop(event, function () {\n _this.ripple.start(event);\n });\n }\n\n if (onKeyDown) {\n onKeyDown(event);\n } // Keyboard accessibility for non interactive elements\n\n\n if (event.target === event.currentTarget && component && component !== 'button' && (event.key === ' ' || event.key === 'Enter') && !(_this.button.tagName === 'A' && _this.button.href)) {\n event.preventDefault();\n\n if (onClick) {\n onClick(event);\n }\n }\n };\n\n _this.handleKeyUp = function (event) {\n if (_this.props.focusRipple && event.key === ' ' && _this.ripple && _this.state.focusVisible) {\n _this.keyDown = false;\n event.persist();\n\n _this.ripple.stop(event, function () {\n _this.ripple.pulsate(event);\n });\n }\n\n if (_this.props.onKeyUp) {\n _this.props.onKeyUp(event);\n }\n };\n\n _this.handleFocus = function (event) {\n if (_this.props.disabled) {\n return;\n } // Fix for https://github.com/facebook/react/issues/7769\n\n\n if (!_this.button) {\n _this.button = event.currentTarget;\n }\n\n event.persist();\n (0, _focusVisible.detectFocusVisible)((0, _assertThisInitialized2.default)((0, _assertThisInitialized2.default)(_this)), _this.button, function () {\n _this.onFocusVisibleHandler(event);\n });\n\n if (_this.props.onFocus) {\n _this.props.onFocus(event);\n }\n };\n\n return _this;\n }\n\n (0, _createClass2.default)(ButtonBase, [{\n key: \"componentDidMount\",\n value: function componentDidMount() {\n var _this2 = this;\n\n this.button = _reactDom.default.findDOMNode(this);\n (0, _focusVisible.listenForFocusKeys)((0, _ownerWindow.default)(this.button));\n\n if (this.props.action) {\n this.props.action({\n focusVisible: function focusVisible() {\n _this2.setState({\n focusVisible: true\n });\n\n _this2.button.focus();\n }\n });\n }\n }\n }, {\n key: \"componentDidUpdate\",\n value: function componentDidUpdate(prevProps, prevState) {\n if (this.props.focusRipple && !this.props.disableRipple && !prevState.focusVisible && this.state.focusVisible) {\n this.ripple.pulsate();\n }\n }\n }, {\n key: \"componentWillUnmount\",\n value: function componentWillUnmount() {\n clearTimeout(this.focusVisibleTimeout);\n }\n }, {\n key: \"render\",\n value: function render() {\n var _classNames;\n\n var _this$props2 = this.props,\n action = _this$props2.action,\n buttonRef = _this$props2.buttonRef,\n centerRipple = _this$props2.centerRipple,\n children = _this$props2.children,\n classes = _this$props2.classes,\n classNameProp = _this$props2.className,\n component = _this$props2.component,\n disabled = _this$props2.disabled,\n disableRipple = _this$props2.disableRipple,\n disableTouchRipple = _this$props2.disableTouchRipple,\n focusRipple = _this$props2.focusRipple,\n focusVisibleClassName = _this$props2.focusVisibleClassName,\n onBlur = _this$props2.onBlur,\n onFocus = _this$props2.onFocus,\n onFocusVisible = _this$props2.onFocusVisible,\n onKeyDown = _this$props2.onKeyDown,\n onKeyUp = _this$props2.onKeyUp,\n onMouseDown = _this$props2.onMouseDown,\n onMouseLeave = _this$props2.onMouseLeave,\n onMouseUp = _this$props2.onMouseUp,\n onTouchEnd = _this$props2.onTouchEnd,\n onTouchMove = _this$props2.onTouchMove,\n onTouchStart = _this$props2.onTouchStart,\n tabIndex = _this$props2.tabIndex,\n TouchRippleProps = _this$props2.TouchRippleProps,\n type = _this$props2.type,\n other = (0, _objectWithoutProperties2.default)(_this$props2, [\"action\", \"buttonRef\", \"centerRipple\", \"children\", \"classes\", \"className\", \"component\", \"disabled\", \"disableRipple\", \"disableTouchRipple\", \"focusRipple\", \"focusVisibleClassName\", \"onBlur\", \"onFocus\", \"onFocusVisible\", \"onKeyDown\", \"onKeyUp\", \"onMouseDown\", \"onMouseLeave\", \"onMouseUp\", \"onTouchEnd\", \"onTouchMove\", \"onTouchStart\", \"tabIndex\", \"TouchRippleProps\", \"type\"]);\n var className = (0, _classnames.default)(classes.root, (_classNames = {}, (0, _defineProperty2.default)(_classNames, classes.disabled, disabled), (0, _defineProperty2.default)(_classNames, classes.focusVisible, this.state.focusVisible), (0, _defineProperty2.default)(_classNames, focusVisibleClassName, this.state.focusVisible), _classNames), classNameProp);\n var ComponentProp = component;\n\n if (ComponentProp === 'button' && other.href) {\n ComponentProp = 'a';\n }\n\n var buttonProps = {};\n\n if (ComponentProp === 'button') {\n buttonProps.type = type || 'button';\n buttonProps.disabled = disabled;\n } else {\n buttonProps.role = 'button';\n }\n\n return _react.default.createElement(ComponentProp, (0, _extends2.default)({\n className: className,\n onBlur: this.handleBlur,\n onFocus: this.handleFocus,\n onKeyDown: this.handleKeyDown,\n onKeyUp: this.handleKeyUp,\n onMouseDown: this.handleMouseDown,\n onMouseLeave: this.handleMouseLeave,\n onMouseUp: this.handleMouseUp,\n onTouchEnd: this.handleTouchEnd,\n onTouchMove: this.handleTouchMove,\n onTouchStart: this.handleTouchStart,\n onContextMenu: this.handleContextMenu,\n ref: buttonRef,\n tabIndex: disabled ? '-1' : tabIndex\n }, buttonProps, other), children, !disableRipple && !disabled ? _react.default.createElement(_NoSsr.default, null, _react.default.createElement(_TouchRipple.default, (0, _extends2.default)({\n innerRef: this.onRippleRef,\n center: centerRipple\n }, TouchRippleProps))) : null);\n }\n }], [{\n key: \"getDerivedStateFromProps\",\n value: function getDerivedStateFromProps(nextProps, prevState) {\n if (typeof prevState.focusVisible === 'undefined') {\n return {\n focusVisible: false,\n lastDisabled: nextProps.disabled\n };\n } // The blur won't fire when the disabled state is set on a focused input.\n // We need to book keep the focused state manually.\n\n\n if (!prevState.prevState && nextProps.disabled && prevState.focusVisible) {\n return {\n focusVisible: false,\n lastDisabled: nextProps.disabled\n };\n }\n\n return {\n lastDisabled: nextProps.disabled\n };\n }\n }]);\n return ButtonBase;\n}(_react.default.Component);\n\n true ? ButtonBase.propTypes = {\n /**\n * Callback fired when the component mounts.\n * This is useful when you want to trigger an action programmatically.\n * It currently only supports `focusVisible()` action.\n *\n * @param {object} actions This object contains all possible actions\n * that can be triggered programmatically.\n */\n action: _propTypes.default.func,\n\n /**\n * Use that property to pass a ref callback to the native button component.\n */\n buttonRef: _propTypes.default.oneOfType([_propTypes.default.func, _propTypes.default.object]),\n\n /**\n * If `true`, the ripples will be centered.\n * They won't start at the cursor interaction position.\n */\n centerRipple: _propTypes.default.bool,\n\n /**\n * The content of the component.\n */\n children: _propTypes.default.node,\n\n /**\n * Override or extend the styles applied to the component.\n * See [CSS API](#css-api) below for more details.\n */\n classes: _propTypes.default.object.isRequired,\n\n /**\n * @ignore\n */\n className: _propTypes.default.string,\n\n /**\n * The component used for the root node.\n * Either a string to use a DOM element or a component.\n */\n component: _utils.componentPropType,\n\n /**\n * If `true`, the base button will be disabled.\n */\n disabled: _propTypes.default.bool,\n\n /**\n * If `true`, the ripple effect will be disabled.\n */\n disableRipple: _propTypes.default.bool,\n\n /**\n * If `true`, the touch ripple effect will be disabled.\n */\n disableTouchRipple: _propTypes.default.bool,\n\n /**\n * If `true`, the base button will have a keyboard focus ripple.\n * `disableRipple` must also be `false`.\n */\n focusRipple: _propTypes.default.bool,\n\n /**\n * This property can help a person know which element has the keyboard focus.\n * The class name will be applied when the element gain the focus through a keyboard interaction.\n * It's a polyfill for the [CSS :focus-visible selector](https://drafts.csswg.org/selectors-4/#the-focus-visible-pseudo).\n * The rationale for using this feature [is explained here](https://github.com/WICG/focus-visible/blob/master/explainer.md).\n * A [polyfill can be used](https://github.com/WICG/focus-visible) to apply a `focus-visible` class to other components\n * if needed.\n */\n focusVisibleClassName: _propTypes.default.string,\n\n /**\n * @ignore\n */\n onBlur: _propTypes.default.func,\n\n /**\n * @ignore\n */\n onClick: _propTypes.default.func,\n\n /**\n * @ignore\n */\n onFocus: _propTypes.default.func,\n\n /**\n * Callback fired when the component is focused with a keyboard.\n * We trigger a `onFocus` callback too.\n */\n onFocusVisible: _propTypes.default.func,\n\n /**\n * @ignore\n */\n onKeyDown: _propTypes.default.func,\n\n /**\n * @ignore\n */\n onKeyUp: _propTypes.default.func,\n\n /**\n * @ignore\n */\n onMouseDown: _propTypes.default.func,\n\n /**\n * @ignore\n */\n onMouseLeave: _propTypes.default.func,\n\n /**\n * @ignore\n */\n onMouseUp: _propTypes.default.func,\n\n /**\n * @ignore\n */\n onTouchEnd: _propTypes.default.func,\n\n /**\n * @ignore\n */\n onTouchMove: _propTypes.default.func,\n\n /**\n * @ignore\n */\n onTouchStart: _propTypes.default.func,\n\n /**\n * @ignore\n */\n role: _propTypes.default.string,\n\n /**\n * @ignore\n */\n tabIndex: _propTypes.default.oneOfType([_propTypes.default.number, _propTypes.default.string]),\n\n /**\n * Properties applied to the `TouchRipple` element.\n */\n TouchRippleProps: _propTypes.default.object,\n\n /**\n * Used to control the button's purpose.\n * This property passes the value to the `type` attribute of the native button component.\n * Valid property values include `button`, `submit`, and `reset`.\n */\n type: _propTypes.default.string\n} : undefined;\nButtonBase.defaultProps = {\n centerRipple: false,\n component: 'button',\n disableRipple: false,\n disableTouchRipple: false,\n focusRipple: false,\n tabIndex: '0',\n type: 'button'\n};\n\nvar _default = (0, _withStyles.default)(styles, {\n name: 'MuiButtonBase'\n})(ButtonBase);\n\nexports.default = _default;\n\n//# sourceURL=webpack:///./node_modules/@material-ui/core/ButtonBase/ButtonBase.js?"); - - /***/ }), - - /***/ "./node_modules/@material-ui/core/ButtonBase/Ripple.js": - /*!*************************************************************!*\ - !*** ./node_modules/@material-ui/core/ButtonBase/Ripple.js ***! - \*************************************************************/ - /*! no static exports found */ - /***/ (function(module, exports, __webpack_require__) { - - "use strict"; - eval("\n\nvar _interopRequireDefault = __webpack_require__(/*! @babel/runtime/helpers/interopRequireDefault */ \"./node_modules/@babel/runtime/helpers/interopRequireDefault.js\");\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.default = void 0;\n\nvar _extends2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/extends */ \"./node_modules/@babel/runtime/helpers/extends.js\"));\n\nvar _defineProperty2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/defineProperty */ \"./node_modules/@babel/runtime/helpers/defineProperty.js\"));\n\nvar _objectWithoutProperties2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/objectWithoutProperties */ \"./node_modules/@babel/runtime/helpers/objectWithoutProperties.js\"));\n\nvar _classCallCheck2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/classCallCheck */ \"./node_modules/@babel/runtime/helpers/classCallCheck.js\"));\n\nvar _createClass2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/createClass */ \"./node_modules/@babel/runtime/helpers/createClass.js\"));\n\nvar _possibleConstructorReturn2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/possibleConstructorReturn */ \"./node_modules/@babel/runtime/helpers/possibleConstructorReturn.js\"));\n\nvar _getPrototypeOf3 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/getPrototypeOf */ \"./node_modules/@babel/runtime/helpers/getPrototypeOf.js\"));\n\nvar _inherits2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/inherits */ \"./node_modules/@babel/runtime/helpers/inherits.js\"));\n\nvar _react = _interopRequireDefault(__webpack_require__(/*! react */ \"./node_modules/react/index.js\"));\n\nvar _propTypes = _interopRequireDefault(__webpack_require__(/*! prop-types */ \"./node_modules/prop-types/index.js\"));\n\nvar _classnames = _interopRequireDefault(__webpack_require__(/*! classnames */ \"./node_modules/classnames/index.js\"));\n\nvar _Transition = _interopRequireDefault(__webpack_require__(/*! react-transition-group/Transition */ \"./node_modules/react-transition-group/Transition.js\"));\n\n/**\n * @ignore - internal component.\n */\nvar Ripple =\n/*#__PURE__*/\nfunction (_React$Component) {\n (0, _inherits2.default)(Ripple, _React$Component);\n\n function Ripple() {\n var _getPrototypeOf2;\n\n var _this;\n\n (0, _classCallCheck2.default)(this, Ripple);\n\n for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {\n args[_key] = arguments[_key];\n }\n\n _this = (0, _possibleConstructorReturn2.default)(this, (_getPrototypeOf2 = (0, _getPrototypeOf3.default)(Ripple)).call.apply(_getPrototypeOf2, [this].concat(args)));\n _this.state = {\n visible: false,\n leaving: false\n };\n\n _this.handleEnter = function () {\n _this.setState({\n visible: true\n });\n };\n\n _this.handleExit = function () {\n _this.setState({\n leaving: true\n });\n };\n\n return _this;\n }\n\n (0, _createClass2.default)(Ripple, [{\n key: \"render\",\n value: function render() {\n var _classNames, _classNames2;\n\n var _this$props = this.props,\n classes = _this$props.classes,\n classNameProp = _this$props.className,\n pulsate = _this$props.pulsate,\n rippleX = _this$props.rippleX,\n rippleY = _this$props.rippleY,\n rippleSize = _this$props.rippleSize,\n other = (0, _objectWithoutProperties2.default)(_this$props, [\"classes\", \"className\", \"pulsate\", \"rippleX\", \"rippleY\", \"rippleSize\"]);\n var _this$state = this.state,\n visible = _this$state.visible,\n leaving = _this$state.leaving;\n var rippleClassName = (0, _classnames.default)(classes.ripple, (_classNames = {}, (0, _defineProperty2.default)(_classNames, classes.rippleVisible, visible), (0, _defineProperty2.default)(_classNames, classes.ripplePulsate, pulsate), _classNames), classNameProp);\n var rippleStyles = {\n width: rippleSize,\n height: rippleSize,\n top: -(rippleSize / 2) + rippleY,\n left: -(rippleSize / 2) + rippleX\n };\n var childClassName = (0, _classnames.default)(classes.child, (_classNames2 = {}, (0, _defineProperty2.default)(_classNames2, classes.childLeaving, leaving), (0, _defineProperty2.default)(_classNames2, classes.childPulsate, pulsate), _classNames2));\n return _react.default.createElement(_Transition.default, (0, _extends2.default)({\n onEnter: this.handleEnter,\n onExit: this.handleExit\n }, other), _react.default.createElement(\"span\", {\n className: rippleClassName,\n style: rippleStyles\n }, _react.default.createElement(\"span\", {\n className: childClassName\n })));\n }\n }]);\n return Ripple;\n}(_react.default.Component);\n\n true ? Ripple.propTypes = {\n /**\n * Override or extend the styles applied to the component.\n * See [CSS API](#css-api) below for more details.\n */\n classes: _propTypes.default.object.isRequired,\n\n /**\n * @ignore\n */\n className: _propTypes.default.string,\n\n /**\n * If `true`, the ripple pulsates, typically indicating the keyboard focus state of an element.\n */\n pulsate: _propTypes.default.bool,\n\n /**\n * Diameter of the ripple.\n */\n rippleSize: _propTypes.default.number,\n\n /**\n * Horizontal position of the ripple center.\n */\n rippleX: _propTypes.default.number,\n\n /**\n * Vertical position of the ripple center.\n */\n rippleY: _propTypes.default.number\n} : undefined;\nRipple.defaultProps = {\n pulsate: false\n};\nvar _default = Ripple;\nexports.default = _default;\n\n//# sourceURL=webpack:///./node_modules/@material-ui/core/ButtonBase/Ripple.js?"); - - /***/ }), - - /***/ "./node_modules/@material-ui/core/ButtonBase/TouchRipple.js": - /*!******************************************************************!*\ - !*** ./node_modules/@material-ui/core/ButtonBase/TouchRipple.js ***! - \******************************************************************/ - /*! no static exports found */ - /***/ (function(module, exports, __webpack_require__) { - - "use strict"; - eval("\n\nvar _interopRequireDefault = __webpack_require__(/*! @babel/runtime/helpers/interopRequireDefault */ \"./node_modules/@babel/runtime/helpers/interopRequireDefault.js\");\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.default = exports.styles = exports.DELAY_RIPPLE = void 0;\n\nvar _extends2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/extends */ \"./node_modules/@babel/runtime/helpers/extends.js\"));\n\nvar _objectWithoutProperties2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/objectWithoutProperties */ \"./node_modules/@babel/runtime/helpers/objectWithoutProperties.js\"));\n\nvar _toConsumableArray2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/toConsumableArray */ \"./node_modules/@babel/runtime/helpers/toConsumableArray.js\"));\n\nvar _classCallCheck2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/classCallCheck */ \"./node_modules/@babel/runtime/helpers/classCallCheck.js\"));\n\nvar _createClass2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/createClass */ \"./node_modules/@babel/runtime/helpers/createClass.js\"));\n\nvar _possibleConstructorReturn2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/possibleConstructorReturn */ \"./node_modules/@babel/runtime/helpers/possibleConstructorReturn.js\"));\n\nvar _getPrototypeOf3 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/getPrototypeOf */ \"./node_modules/@babel/runtime/helpers/getPrototypeOf.js\"));\n\nvar _inherits2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/inherits */ \"./node_modules/@babel/runtime/helpers/inherits.js\"));\n\nvar _assertThisInitialized2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/assertThisInitialized */ \"./node_modules/@babel/runtime/helpers/assertThisInitialized.js\"));\n\nvar _react = _interopRequireDefault(__webpack_require__(/*! react */ \"./node_modules/react/index.js\"));\n\nvar _propTypes = _interopRequireDefault(__webpack_require__(/*! prop-types */ \"./node_modules/prop-types/index.js\"));\n\nvar _reactDom = _interopRequireDefault(__webpack_require__(/*! react-dom */ \"./node_modules/react-dom/index.js\"));\n\nvar _TransitionGroup = _interopRequireDefault(__webpack_require__(/*! react-transition-group/TransitionGroup */ \"./node_modules/react-transition-group/TransitionGroup.js\"));\n\nvar _classnames = _interopRequireDefault(__webpack_require__(/*! classnames */ \"./node_modules/classnames/index.js\"));\n\nvar _withStyles = _interopRequireDefault(__webpack_require__(/*! ../styles/withStyles */ \"./node_modules/@material-ui/core/styles/withStyles.js\"));\n\nvar _Ripple = _interopRequireDefault(__webpack_require__(/*! ./Ripple */ \"./node_modules/@material-ui/core/ButtonBase/Ripple.js\"));\n\nvar DURATION = 550;\nvar DELAY_RIPPLE = 80;\nexports.DELAY_RIPPLE = DELAY_RIPPLE;\n\nvar styles = function styles(theme) {\n return {\n /* Styles applied to the root element. */\n root: {\n display: 'block',\n position: 'absolute',\n overflow: 'hidden',\n borderRadius: 'inherit',\n width: '100%',\n height: '100%',\n left: 0,\n top: 0,\n pointerEvents: 'none',\n zIndex: 0\n },\n\n /* Styles applied to the internal `Ripple` components `ripple` class. */\n ripple: {\n width: 50,\n height: 50,\n left: 0,\n top: 0,\n opacity: 0,\n position: 'absolute'\n },\n\n /* Styles applied to the internal `Ripple` components `rippleVisible` class. */\n rippleVisible: {\n opacity: 0.3,\n transform: 'scale(1)',\n animation: \"mui-ripple-enter \".concat(DURATION, \"ms \").concat(theme.transitions.easing.easeInOut),\n // Backward compatible logic between JSS v9 and v10.\n // To remove with the release of Material-UI v4\n animationName: '$mui-ripple-enter'\n },\n\n /* Styles applied to the internal `Ripple` components `ripplePulsate` class. */\n ripplePulsate: {\n animationDuration: \"\".concat(theme.transitions.duration.shorter, \"ms\")\n },\n\n /* Styles applied to the internal `Ripple` components `child` class. */\n child: {\n opacity: 1,\n display: 'block',\n width: '100%',\n height: '100%',\n borderRadius: '50%',\n backgroundColor: 'currentColor'\n },\n\n /* Styles applied to the internal `Ripple` components `childLeaving` class. */\n childLeaving: {\n opacity: 0,\n animation: \"mui-ripple-exit \".concat(DURATION, \"ms \").concat(theme.transitions.easing.easeInOut),\n // Backward compatible logic between JSS v9 and v10.\n // To remove with the release of Material-UI v4\n animationName: '$mui-ripple-exit'\n },\n\n /* Styles applied to the internal `Ripple` components `childPulsate` class. */\n childPulsate: {\n position: 'absolute',\n left: 0,\n top: 0,\n animation: \"mui-ripple-pulsate 2500ms \".concat(theme.transitions.easing.easeInOut, \" 200ms infinite\"),\n // Backward compatible logic between JSS v9 and v10.\n // To remove with the release of Material-UI v4\n animationName: '$mui-ripple-pulsate'\n },\n '@keyframes mui-ripple-enter': {\n '0%': {\n transform: 'scale(0)',\n opacity: 0.1\n },\n '100%': {\n transform: 'scale(1)',\n opacity: 0.3\n }\n },\n '@keyframes mui-ripple-exit': {\n '0%': {\n opacity: 1\n },\n '100%': {\n opacity: 0\n }\n },\n '@keyframes mui-ripple-pulsate': {\n '0%': {\n transform: 'scale(1)'\n },\n '50%': {\n transform: 'scale(0.92)'\n },\n '100%': {\n transform: 'scale(1)'\n }\n }\n };\n};\n\nexports.styles = styles;\n\nvar TouchRipple =\n/*#__PURE__*/\nfunction (_React$PureComponent) {\n (0, _inherits2.default)(TouchRipple, _React$PureComponent);\n\n function TouchRipple() {\n var _getPrototypeOf2;\n\n var _this;\n\n (0, _classCallCheck2.default)(this, TouchRipple);\n\n for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {\n args[_key] = arguments[_key];\n }\n\n _this = (0, _possibleConstructorReturn2.default)(this, (_getPrototypeOf2 = (0, _getPrototypeOf3.default)(TouchRipple)).call.apply(_getPrototypeOf2, [this].concat(args)));\n _this.state = {\n nextKey: 0,\n ripples: []\n };\n\n _this.pulsate = function () {\n _this.start({}, {\n pulsate: true\n });\n };\n\n _this.start = function () {\n var event = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};\n var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};\n var cb = arguments.length > 2 ? arguments[2] : undefined;\n var _options$pulsate = options.pulsate,\n pulsate = _options$pulsate === void 0 ? false : _options$pulsate,\n _options$center = options.center,\n center = _options$center === void 0 ? _this.props.center || options.pulsate : _options$center,\n _options$fakeElement = options.fakeElement,\n fakeElement = _options$fakeElement === void 0 ? false : _options$fakeElement;\n\n if (event.type === 'mousedown' && _this.ignoringMouseDown) {\n _this.ignoringMouseDown = false;\n return;\n }\n\n if (event.type === 'touchstart') {\n _this.ignoringMouseDown = true;\n }\n\n var element = fakeElement ? null : _reactDom.default.findDOMNode((0, _assertThisInitialized2.default)((0, _assertThisInitialized2.default)(_this)));\n var rect = element ? element.getBoundingClientRect() : {\n width: 0,\n height: 0,\n left: 0,\n top: 0\n }; // Get the size of the ripple\n\n var rippleX;\n var rippleY;\n var rippleSize;\n\n if (center || event.clientX === 0 && event.clientY === 0 || !event.clientX && !event.touches) {\n rippleX = Math.round(rect.width / 2);\n rippleY = Math.round(rect.height / 2);\n } else {\n var clientX = event.clientX ? event.clientX : event.touches[0].clientX;\n var clientY = event.clientY ? event.clientY : event.touches[0].clientY;\n rippleX = Math.round(clientX - rect.left);\n rippleY = Math.round(clientY - rect.top);\n }\n\n if (center) {\n rippleSize = Math.sqrt((2 * Math.pow(rect.width, 2) + Math.pow(rect.height, 2)) / 3); // For some reason the animation is broken on Mobile Chrome if the size if even.\n\n if (rippleSize % 2 === 0) {\n rippleSize += 1;\n }\n } else {\n var sizeX = Math.max(Math.abs((element ? element.clientWidth : 0) - rippleX), rippleX) * 2 + 2;\n var sizeY = Math.max(Math.abs((element ? element.clientHeight : 0) - rippleY), rippleY) * 2 + 2;\n rippleSize = Math.sqrt(Math.pow(sizeX, 2) + Math.pow(sizeY, 2));\n } // Touche devices\n\n\n if (event.touches) {\n // Prepare the ripple effect.\n _this.startTimerCommit = function () {\n _this.startCommit({\n pulsate: pulsate,\n rippleX: rippleX,\n rippleY: rippleY,\n rippleSize: rippleSize,\n cb: cb\n });\n }; // Delay the execution of the ripple effect.\n\n\n _this.startTimer = setTimeout(function () {\n if (_this.startTimerCommit) {\n _this.startTimerCommit();\n\n _this.startTimerCommit = null;\n }\n }, DELAY_RIPPLE); // We have to make a tradeoff with this value.\n } else {\n _this.startCommit({\n pulsate: pulsate,\n rippleX: rippleX,\n rippleY: rippleY,\n rippleSize: rippleSize,\n cb: cb\n });\n }\n };\n\n _this.startCommit = function (params) {\n var pulsate = params.pulsate,\n rippleX = params.rippleX,\n rippleY = params.rippleY,\n rippleSize = params.rippleSize,\n cb = params.cb;\n\n _this.setState(function (state) {\n return {\n nextKey: state.nextKey + 1,\n ripples: [].concat((0, _toConsumableArray2.default)(state.ripples), [_react.default.createElement(_Ripple.default, {\n key: state.nextKey,\n classes: _this.props.classes,\n timeout: {\n exit: DURATION,\n enter: DURATION\n },\n pulsate: pulsate,\n rippleX: rippleX,\n rippleY: rippleY,\n rippleSize: rippleSize\n })])\n };\n }, cb);\n };\n\n _this.stop = function (event, cb) {\n clearTimeout(_this.startTimer);\n var ripples = _this.state.ripples; // The touch interaction occurs too quickly.\n // We still want to show ripple effect.\n\n if (event.type === 'touchend' && _this.startTimerCommit) {\n event.persist();\n\n _this.startTimerCommit();\n\n _this.startTimerCommit = null;\n _this.startTimer = setTimeout(function () {\n _this.stop(event, cb);\n });\n return;\n }\n\n _this.startTimerCommit = null;\n\n if (ripples && ripples.length) {\n _this.setState({\n ripples: ripples.slice(1)\n }, cb);\n }\n };\n\n return _this;\n }\n\n (0, _createClass2.default)(TouchRipple, [{\n key: \"componentWillUnmount\",\n value: function componentWillUnmount() {\n clearTimeout(this.startTimer);\n }\n }, {\n key: \"render\",\n value: function render() {\n var _this$props = this.props,\n center = _this$props.center,\n classes = _this$props.classes,\n className = _this$props.className,\n other = (0, _objectWithoutProperties2.default)(_this$props, [\"center\", \"classes\", \"className\"]);\n return _react.default.createElement(_TransitionGroup.default, (0, _extends2.default)({\n component: \"span\",\n enter: true,\n exit: true,\n className: (0, _classnames.default)(classes.root, className)\n }, other), this.state.ripples);\n }\n }]);\n return TouchRipple;\n}(_react.default.PureComponent);\n\n true ? TouchRipple.propTypes = {\n /**\n * If `true`, the ripple starts at the center of the component\n * rather than at the point of interaction.\n */\n center: _propTypes.default.bool,\n\n /**\n * Override or extend the styles applied to the component.\n * See [CSS API](#css-api) below for more details.\n */\n classes: _propTypes.default.object.isRequired,\n\n /**\n * @ignore\n */\n className: _propTypes.default.string\n} : undefined;\nTouchRipple.defaultProps = {\n center: false\n};\n\nvar _default = (0, _withStyles.default)(styles, {\n flip: false,\n name: 'MuiTouchRipple'\n})(TouchRipple);\n\nexports.default = _default;\n\n//# sourceURL=webpack:///./node_modules/@material-ui/core/ButtonBase/TouchRipple.js?"); - - /***/ }), - - /***/ "./node_modules/@material-ui/core/ButtonBase/createRippleHandler.js": - /*!**************************************************************************!*\ - !*** ./node_modules/@material-ui/core/ButtonBase/createRippleHandler.js ***! - \**************************************************************************/ - /*! no static exports found */ - /***/ (function(module, exports, __webpack_require__) { - - "use strict"; - eval("\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.default = void 0;\n\n/* eslint-disable import/no-mutable-exports */\nvar createRippleHandler = function createRippleHandler(instance, eventName, action, cb) {\n return function (event) {\n if (cb) {\n cb.call(instance, event);\n }\n\n var ignore = false; // Ignore events that have been `event.preventDefault()` marked.\n\n if (event.defaultPrevented) {\n ignore = true;\n }\n\n if (instance.props.disableTouchRipple && eventName !== 'Blur') {\n ignore = true;\n }\n\n if (!ignore && instance.ripple) {\n instance.ripple[action](event);\n }\n\n if (typeof instance.props[\"on\".concat(eventName)] === 'function') {\n instance.props[\"on\".concat(eventName)](event);\n }\n\n return true;\n };\n};\n/* istanbul ignore if */\n\n\nif (typeof window === 'undefined') {\n createRippleHandler = function createRippleHandler() {\n return function () {};\n };\n}\n\nvar _default = createRippleHandler;\nexports.default = _default;\n\n//# sourceURL=webpack:///./node_modules/@material-ui/core/ButtonBase/createRippleHandler.js?"); - - /***/ }), - - /***/ "./node_modules/@material-ui/core/ButtonBase/focusVisible.js": - /*!*******************************************************************!*\ - !*** ./node_modules/@material-ui/core/ButtonBase/focusVisible.js ***! - \*******************************************************************/ - /*! no static exports found */ - /***/ (function(module, exports, __webpack_require__) { - - "use strict"; - eval("\n\nvar _interopRequireDefault = __webpack_require__(/*! @babel/runtime/helpers/interopRequireDefault */ \"./node_modules/@babel/runtime/helpers/interopRequireDefault.js\");\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.detectFocusVisible = detectFocusVisible;\nexports.listenForFocusKeys = listenForFocusKeys;\n\nvar _warning = _interopRequireDefault(__webpack_require__(/*! warning */ \"./node_modules/warning/warning.js\"));\n\nvar _ownerDocument = _interopRequireDefault(__webpack_require__(/*! ../utils/ownerDocument */ \"./node_modules/@material-ui/core/utils/ownerDocument.js\"));\n\nvar internal = {\n focusKeyPressed: false,\n keyUpEventTimeout: -1\n};\n\nfunction findActiveElement(doc) {\n var activeElement = doc.activeElement;\n\n while (activeElement && activeElement.shadowRoot && activeElement.shadowRoot.activeElement) {\n activeElement = activeElement.shadowRoot.activeElement;\n }\n\n return activeElement;\n}\n\nfunction detectFocusVisible(instance, element, callback) {\n var attempt = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 1;\n true ? (0, _warning.default)(instance.focusVisibleCheckTime, 'Material-UI: missing instance.focusVisibleCheckTime.') : undefined;\n true ? (0, _warning.default)(instance.focusVisibleMaxCheckTimes, 'Material-UI: missing instance.focusVisibleMaxCheckTimes.') : undefined;\n instance.focusVisibleTimeout = setTimeout(function () {\n var doc = (0, _ownerDocument.default)(element);\n var activeElement = findActiveElement(doc);\n\n if (internal.focusKeyPressed && (activeElement === element || element.contains(activeElement))) {\n callback();\n } else if (attempt < instance.focusVisibleMaxCheckTimes) {\n detectFocusVisible(instance, element, callback, attempt + 1);\n }\n }, instance.focusVisibleCheckTime);\n}\n\nvar FOCUS_KEYS = [9, // 'Tab',\n13, // 'Enter',\n27, // 'Escape',\n32, // ' ',\n37, // 'ArrowLeft',\n38, // 'ArrowUp',\n39, // 'ArrowRight',\n40];\n\nfunction isFocusKey(event) {\n // Use event.keyCode to support IE 11\n return FOCUS_KEYS.indexOf(event.keyCode) > -1;\n}\n\nvar handleKeyUpEvent = function handleKeyUpEvent(event) {\n if (isFocusKey(event)) {\n internal.focusKeyPressed = true; // Let's consider that the user is using a keyboard during a window frame of 500ms.\n\n clearTimeout(internal.keyUpEventTimeout);\n internal.keyUpEventTimeout = setTimeout(function () {\n internal.focusKeyPressed = false;\n }, 500);\n }\n};\n\nfunction listenForFocusKeys(win) {\n // The event listener will only be added once per window.\n // Duplicate event listeners will be ignored by addEventListener.\n // Also, this logic is client side only, we don't need a teardown.\n win.addEventListener('keyup', handleKeyUpEvent);\n}\n\n//# sourceURL=webpack:///./node_modules/@material-ui/core/ButtonBase/focusVisible.js?"); - - /***/ }), - - /***/ "./node_modules/@material-ui/core/ButtonBase/index.js": - /*!************************************************************!*\ - !*** ./node_modules/@material-ui/core/ButtonBase/index.js ***! - \************************************************************/ - /*! no static exports found */ - /***/ (function(module, exports, __webpack_require__) { - - "use strict"; - eval("\n\nvar _interopRequireDefault = __webpack_require__(/*! @babel/runtime/helpers/interopRequireDefault */ \"./node_modules/@babel/runtime/helpers/interopRequireDefault.js\");\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nObject.defineProperty(exports, \"default\", {\n enumerable: true,\n get: function get() {\n return _ButtonBase.default;\n }\n});\n\nvar _ButtonBase = _interopRequireDefault(__webpack_require__(/*! ./ButtonBase */ \"./node_modules/@material-ui/core/ButtonBase/ButtonBase.js\"));\n\n//# sourceURL=webpack:///./node_modules/@material-ui/core/ButtonBase/index.js?"); - - /***/ }), - - /***/ "./node_modules/@material-ui/core/Card/Card.js": - /*!*****************************************************!*\ - !*** ./node_modules/@material-ui/core/Card/Card.js ***! - \*****************************************************/ - /*! no static exports found */ - /***/ (function(module, exports, __webpack_require__) { - - "use strict"; - eval("\n\nvar _interopRequireDefault = __webpack_require__(/*! @babel/runtime/helpers/interopRequireDefault */ \"./node_modules/@babel/runtime/helpers/interopRequireDefault.js\");\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.default = exports.styles = void 0;\n\nvar _extends2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/extends */ \"./node_modules/@babel/runtime/helpers/extends.js\"));\n\nvar _objectWithoutProperties2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/objectWithoutProperties */ \"./node_modules/@babel/runtime/helpers/objectWithoutProperties.js\"));\n\nvar _react = _interopRequireDefault(__webpack_require__(/*! react */ \"./node_modules/react/index.js\"));\n\nvar _propTypes = _interopRequireDefault(__webpack_require__(/*! prop-types */ \"./node_modules/prop-types/index.js\"));\n\nvar _classnames = _interopRequireDefault(__webpack_require__(/*! classnames */ \"./node_modules/classnames/index.js\"));\n\nvar _Paper = _interopRequireDefault(__webpack_require__(/*! ../Paper */ \"./node_modules/@material-ui/core/Paper/index.js\"));\n\nvar _withStyles = _interopRequireDefault(__webpack_require__(/*! ../styles/withStyles */ \"./node_modules/@material-ui/core/styles/withStyles.js\"));\n\n// @inheritedComponent Paper\nvar styles = {\n /* Styles applied to the root element. */\n root: {\n overflow: 'hidden'\n }\n};\nexports.styles = styles;\n\nfunction Card(props) {\n var classes = props.classes,\n className = props.className,\n raised = props.raised,\n other = (0, _objectWithoutProperties2.default)(props, [\"classes\", \"className\", \"raised\"]);\n return _react.default.createElement(_Paper.default, (0, _extends2.default)({\n className: (0, _classnames.default)(classes.root, className),\n elevation: raised ? 8 : 1\n }, other));\n}\n\n true ? Card.propTypes = {\n /**\n * Override or extend the styles applied to the component.\n * See [CSS API](#css-api) below for more details.\n */\n classes: _propTypes.default.object.isRequired,\n\n /**\n * @ignore\n */\n className: _propTypes.default.string,\n\n /**\n * If `true`, the card will use raised styling.\n */\n raised: _propTypes.default.bool\n} : undefined;\nCard.defaultProps = {\n raised: false\n};\n\nvar _default = (0, _withStyles.default)(styles, {\n name: 'MuiCard'\n})(Card);\n\nexports.default = _default;\n\n//# sourceURL=webpack:///./node_modules/@material-ui/core/Card/Card.js?"); - - /***/ }), - - /***/ "./node_modules/@material-ui/core/Card/index.js": - /*!******************************************************!*\ - !*** ./node_modules/@material-ui/core/Card/index.js ***! - \******************************************************/ - /*! no static exports found */ - /***/ (function(module, exports, __webpack_require__) { - - "use strict"; - eval("\n\nvar _interopRequireDefault = __webpack_require__(/*! @babel/runtime/helpers/interopRequireDefault */ \"./node_modules/@babel/runtime/helpers/interopRequireDefault.js\");\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nObject.defineProperty(exports, \"default\", {\n enumerable: true,\n get: function get() {\n return _Card.default;\n }\n});\n\nvar _Card = _interopRequireDefault(__webpack_require__(/*! ./Card */ \"./node_modules/@material-ui/core/Card/Card.js\"));\n\n//# sourceURL=webpack:///./node_modules/@material-ui/core/Card/index.js?"); - - /***/ }), - - /***/ "./node_modules/@material-ui/core/CardContent/CardContent.js": - /*!*******************************************************************!*\ - !*** ./node_modules/@material-ui/core/CardContent/CardContent.js ***! - \*******************************************************************/ - /*! no static exports found */ - /***/ (function(module, exports, __webpack_require__) { - - "use strict"; - eval("\n\nvar _interopRequireDefault = __webpack_require__(/*! @babel/runtime/helpers/interopRequireDefault */ \"./node_modules/@babel/runtime/helpers/interopRequireDefault.js\");\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.default = exports.styles = void 0;\n\nvar _extends2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/extends */ \"./node_modules/@babel/runtime/helpers/extends.js\"));\n\nvar _objectWithoutProperties2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/objectWithoutProperties */ \"./node_modules/@babel/runtime/helpers/objectWithoutProperties.js\"));\n\nvar _react = _interopRequireDefault(__webpack_require__(/*! react */ \"./node_modules/react/index.js\"));\n\nvar _propTypes = _interopRequireDefault(__webpack_require__(/*! prop-types */ \"./node_modules/prop-types/index.js\"));\n\nvar _classnames = _interopRequireDefault(__webpack_require__(/*! classnames */ \"./node_modules/classnames/index.js\"));\n\nvar _utils = __webpack_require__(/*! @material-ui/utils */ \"./node_modules/@material-ui/utils/index.es.js\");\n\nvar _withStyles = _interopRequireDefault(__webpack_require__(/*! ../styles/withStyles */ \"./node_modules/@material-ui/core/styles/withStyles.js\"));\n\nvar styles = {\n /* Styles applied to the root element. */\n root: {\n padding: 16,\n '&:last-child': {\n paddingBottom: 24\n }\n }\n};\nexports.styles = styles;\n\nfunction CardContent(props) {\n var classes = props.classes,\n className = props.className,\n Component = props.component,\n other = (0, _objectWithoutProperties2.default)(props, [\"classes\", \"className\", \"component\"]);\n return _react.default.createElement(Component, (0, _extends2.default)({\n className: (0, _classnames.default)(classes.root, className)\n }, other));\n}\n\n true ? CardContent.propTypes = {\n /**\n * Override or extend the styles applied to the component.\n * See [CSS API](#css-api) below for more details.\n */\n classes: _propTypes.default.object.isRequired,\n\n /**\n * @ignore\n */\n className: _propTypes.default.string,\n\n /**\n * The component used for the root node.\n * Either a string to use a DOM element or a component.\n */\n component: _utils.componentPropType\n} : undefined;\nCardContent.defaultProps = {\n component: 'div'\n};\n\nvar _default = (0, _withStyles.default)(styles, {\n name: 'MuiCardContent'\n})(CardContent);\n\nexports.default = _default;\n\n//# sourceURL=webpack:///./node_modules/@material-ui/core/CardContent/CardContent.js?"); - - /***/ }), - - /***/ "./node_modules/@material-ui/core/CardContent/index.js": - /*!*************************************************************!*\ - !*** ./node_modules/@material-ui/core/CardContent/index.js ***! - \*************************************************************/ - /*! no static exports found */ - /***/ (function(module, exports, __webpack_require__) { - - "use strict"; - eval("\n\nvar _interopRequireDefault = __webpack_require__(/*! @babel/runtime/helpers/interopRequireDefault */ \"./node_modules/@babel/runtime/helpers/interopRequireDefault.js\");\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nObject.defineProperty(exports, \"default\", {\n enumerable: true,\n get: function get() {\n return _CardContent.default;\n }\n});\n\nvar _CardContent = _interopRequireDefault(__webpack_require__(/*! ./CardContent */ \"./node_modules/@material-ui/core/CardContent/CardContent.js\"));\n\n//# sourceURL=webpack:///./node_modules/@material-ui/core/CardContent/index.js?"); - - /***/ }), - - /***/ "./node_modules/@material-ui/core/Checkbox/Checkbox.js": - /*!*************************************************************!*\ - !*** ./node_modules/@material-ui/core/Checkbox/Checkbox.js ***! - \*************************************************************/ - /*! no static exports found */ - /***/ (function(module, exports, __webpack_require__) { - - "use strict"; - eval("\n\nvar _interopRequireDefault = __webpack_require__(/*! @babel/runtime/helpers/interopRequireDefault */ \"./node_modules/@babel/runtime/helpers/interopRequireDefault.js\");\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.default = exports.styles = void 0;\n\nvar _extends2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/extends */ \"./node_modules/@babel/runtime/helpers/extends.js\"));\n\nvar _defineProperty2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/defineProperty */ \"./node_modules/@babel/runtime/helpers/defineProperty.js\"));\n\nvar _objectWithoutProperties2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/objectWithoutProperties */ \"./node_modules/@babel/runtime/helpers/objectWithoutProperties.js\"));\n\nvar _react = _interopRequireDefault(__webpack_require__(/*! react */ \"./node_modules/react/index.js\"));\n\nvar _propTypes = _interopRequireDefault(__webpack_require__(/*! prop-types */ \"./node_modules/prop-types/index.js\"));\n\nvar _classnames = _interopRequireDefault(__webpack_require__(/*! classnames */ \"./node_modules/classnames/index.js\"));\n\nvar _SwitchBase = _interopRequireDefault(__webpack_require__(/*! ../internal/SwitchBase */ \"./node_modules/@material-ui/core/internal/SwitchBase.js\"));\n\nvar _CheckBoxOutlineBlank = _interopRequireDefault(__webpack_require__(/*! ../internal/svg-icons/CheckBoxOutlineBlank */ \"./node_modules/@material-ui/core/internal/svg-icons/CheckBoxOutlineBlank.js\"));\n\nvar _CheckBox = _interopRequireDefault(__webpack_require__(/*! ../internal/svg-icons/CheckBox */ \"./node_modules/@material-ui/core/internal/svg-icons/CheckBox.js\"));\n\nvar _IndeterminateCheckBox = _interopRequireDefault(__webpack_require__(/*! ../internal/svg-icons/IndeterminateCheckBox */ \"./node_modules/@material-ui/core/internal/svg-icons/IndeterminateCheckBox.js\"));\n\nvar _helpers = __webpack_require__(/*! ../utils/helpers */ \"./node_modules/@material-ui/core/utils/helpers.js\");\n\nvar _withStyles = _interopRequireDefault(__webpack_require__(/*! ../styles/withStyles */ \"./node_modules/@material-ui/core/styles/withStyles.js\"));\n\nvar styles = function styles(theme) {\n return {\n /* Styles applied to the root element. */\n root: {\n color: theme.palette.text.secondary\n },\n\n /* Styles applied to the root element if `checked={true}`. */\n checked: {},\n\n /* Styles applied to the root element if `disabled={true}`. */\n disabled: {},\n\n /* Styles applied to the root element if `indeterminate={true}`. */\n indeterminate: {},\n\n /* Styles applied to the root element if `color=\"primary\"`. */\n colorPrimary: {\n '&$checked': {\n color: theme.palette.primary.main\n },\n '&$disabled': {\n color: theme.palette.action.disabled\n }\n },\n\n /* Styles applied to the root element if `color=\"secondary\"`. */\n colorSecondary: {\n '&$checked': {\n color: theme.palette.secondary.main\n },\n '&$disabled': {\n color: theme.palette.action.disabled\n }\n }\n };\n};\n\nexports.styles = styles;\n\nfunction Checkbox(props) {\n var checkedIcon = props.checkedIcon,\n classes = props.classes,\n className = props.className,\n color = props.color,\n icon = props.icon,\n indeterminate = props.indeterminate,\n indeterminateIcon = props.indeterminateIcon,\n inputProps = props.inputProps,\n other = (0, _objectWithoutProperties2.default)(props, [\"checkedIcon\", \"classes\", \"className\", \"color\", \"icon\", \"indeterminate\", \"indeterminateIcon\", \"inputProps\"]);\n return _react.default.createElement(_SwitchBase.default, (0, _extends2.default)({\n type: \"checkbox\",\n checkedIcon: indeterminate ? indeterminateIcon : checkedIcon,\n className: (0, _classnames.default)((0, _defineProperty2.default)({}, classes.indeterminate, indeterminate), className),\n classes: {\n root: (0, _classnames.default)(classes.root, classes[\"color\".concat((0, _helpers.capitalize)(color))]),\n checked: classes.checked,\n disabled: classes.disabled\n },\n inputProps: (0, _extends2.default)({\n 'data-indeterminate': indeterminate\n }, inputProps),\n icon: indeterminate ? indeterminateIcon : icon\n }, other));\n}\n\n true ? Checkbox.propTypes = {\n /**\n * If `true`, the component is checked.\n */\n checked: _propTypes.default.oneOfType([_propTypes.default.bool, _propTypes.default.string]),\n\n /**\n * The icon to display when the component is checked.\n */\n checkedIcon: _propTypes.default.node,\n\n /**\n * Override or extend the styles applied to the component.\n * See [CSS API](#css-api) below for more details.\n */\n classes: _propTypes.default.object.isRequired,\n\n /**\n * @ignore\n */\n className: _propTypes.default.string,\n\n /**\n * The color of the component. It supports those theme colors that make sense for this component.\n */\n color: _propTypes.default.oneOf(['primary', 'secondary', 'default']),\n\n /**\n * If `true`, the switch will be disabled.\n */\n disabled: _propTypes.default.bool,\n\n /**\n * If `true`, the ripple effect will be disabled.\n */\n disableRipple: _propTypes.default.bool,\n\n /**\n * The icon to display when the component is unchecked.\n */\n icon: _propTypes.default.node,\n\n /**\n * The id of the `input` element.\n */\n id: _propTypes.default.string,\n\n /**\n * If `true`, the component appears indeterminate.\n * This does not set the native input element to indeterminate due\n * to inconsistent behavior across browsers.\n * However, we set a `data-indeterminate` attribute on the input.\n */\n indeterminate: _propTypes.default.bool,\n\n /**\n * The icon to display when the component is indeterminate.\n */\n indeterminateIcon: _propTypes.default.node,\n\n /**\n * Properties applied to the `input` element.\n */\n inputProps: _propTypes.default.object,\n\n /**\n * Use that property to pass a ref callback to the native input component.\n */\n inputRef: _propTypes.default.oneOfType([_propTypes.default.func, _propTypes.default.object]),\n\n /**\n * Callback fired when the state is changed.\n *\n * @param {object} event The event source of the callback.\n * You can pull out the new value by accessing `event.target.checked`.\n * @param {boolean} checked The `checked` value of the switch\n */\n onChange: _propTypes.default.func,\n\n /**\n * The input component property `type`.\n */\n type: _propTypes.default.string,\n\n /**\n * The value of the component.\n */\n value: _propTypes.default.string\n} : undefined;\nCheckbox.defaultProps = {\n checkedIcon: _react.default.createElement(_CheckBox.default, null),\n color: 'secondary',\n icon: _react.default.createElement(_CheckBoxOutlineBlank.default, null),\n indeterminate: false,\n indeterminateIcon: _react.default.createElement(_IndeterminateCheckBox.default, null)\n};\n\nvar _default = (0, _withStyles.default)(styles, {\n name: 'MuiCheckbox'\n})(Checkbox);\n\nexports.default = _default;\n\n//# sourceURL=webpack:///./node_modules/@material-ui/core/Checkbox/Checkbox.js?"); - - /***/ }), - - /***/ "./node_modules/@material-ui/core/Checkbox/index.js": - /*!**********************************************************!*\ - !*** ./node_modules/@material-ui/core/Checkbox/index.js ***! - \**********************************************************/ - /*! no static exports found */ - /***/ (function(module, exports, __webpack_require__) { - - "use strict"; - eval("\n\nvar _interopRequireDefault = __webpack_require__(/*! @babel/runtime/helpers/interopRequireDefault */ \"./node_modules/@babel/runtime/helpers/interopRequireDefault.js\");\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nObject.defineProperty(exports, \"default\", {\n enumerable: true,\n get: function get() {\n return _Checkbox.default;\n }\n});\n\nvar _Checkbox = _interopRequireDefault(__webpack_require__(/*! ./Checkbox */ \"./node_modules/@material-ui/core/Checkbox/Checkbox.js\"));\n\n//# sourceURL=webpack:///./node_modules/@material-ui/core/Checkbox/index.js?"); - - /***/ }), - - /***/ "./node_modules/@material-ui/core/CircularProgress/CircularProgress.js": - /*!*****************************************************************************!*\ - !*** ./node_modules/@material-ui/core/CircularProgress/CircularProgress.js ***! - \*****************************************************************************/ - /*! no static exports found */ - /***/ (function(module, exports, __webpack_require__) { - - "use strict"; - eval("\n\nvar _interopRequireDefault = __webpack_require__(/*! @babel/runtime/helpers/interopRequireDefault */ \"./node_modules/@babel/runtime/helpers/interopRequireDefault.js\");\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.default = exports.styles = void 0;\n\nvar _extends2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/extends */ \"./node_modules/@babel/runtime/helpers/extends.js\"));\n\nvar _defineProperty2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/defineProperty */ \"./node_modules/@babel/runtime/helpers/defineProperty.js\"));\n\nvar _objectWithoutProperties2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/objectWithoutProperties */ \"./node_modules/@babel/runtime/helpers/objectWithoutProperties.js\"));\n\nvar _react = _interopRequireDefault(__webpack_require__(/*! react */ \"./node_modules/react/index.js\"));\n\nvar _propTypes = _interopRequireDefault(__webpack_require__(/*! prop-types */ \"./node_modules/prop-types/index.js\"));\n\nvar _classnames = _interopRequireDefault(__webpack_require__(/*! classnames */ \"./node_modules/classnames/index.js\"));\n\nvar _utils = __webpack_require__(/*! @material-ui/utils */ \"./node_modules/@material-ui/utils/index.es.js\");\n\nvar _withStyles = _interopRequireDefault(__webpack_require__(/*! ../styles/withStyles */ \"./node_modules/@material-ui/core/styles/withStyles.js\"));\n\nvar _helpers = __webpack_require__(/*! ../utils/helpers */ \"./node_modules/@material-ui/core/utils/helpers.js\");\n\nvar SIZE = 44;\n\nfunction getRelativeValue(value, min, max) {\n var clampedValue = Math.min(Math.max(min, value), max);\n return (clampedValue - min) / (max - min);\n}\n\nfunction easeOut(t) {\n t = getRelativeValue(t, 0, 1); // https://gist.github.com/gre/1650294\n\n t = (t -= 1) * t * t + 1;\n return t;\n}\n\nfunction easeIn(t) {\n return t * t;\n}\n\nvar styles = function styles(theme) {\n return {\n /* Styles applied to the root element. */\n root: {\n display: 'inline-block',\n lineHeight: 1 // Keep the progress centered\n\n },\n\n /* Styles applied to the root element if `variant=\"static\"`. */\n static: {\n transition: theme.transitions.create('transform')\n },\n\n /* Styles applied to the root element if `variant=\"indeterminate\"`. */\n indeterminate: {\n animation: 'mui-progress-circular-rotate 1.4s linear infinite',\n // Backward compatible logic between JSS v9 and v10.\n // To remove with the release of Material-UI v4\n animationName: '$mui-progress-circular-rotate'\n },\n\n /* Styles applied to the root element if `color=\"primary\"`. */\n colorPrimary: {\n color: theme.palette.primary.main\n },\n\n /* Styles applied to the root element if `color=\"secondary\"`. */\n colorSecondary: {\n color: theme.palette.secondary.main\n },\n\n /* Styles applied to the `svg` element. */\n svg: {},\n\n /* Styles applied to the `circle` svg path. */\n circle: {\n stroke: 'currentColor' // Use butt to follow the specification, by chance, it's already the default CSS value.\n // strokeLinecap: 'butt',\n\n },\n\n /* Styles applied to the `circle` svg path if `variant=\"static\"`. */\n circleStatic: {\n transition: theme.transitions.create('stroke-dashoffset')\n },\n\n /* Styles applied to the `circle` svg path if `variant=\"indeterminate\"`. */\n circleIndeterminate: {\n animation: 'mui-progress-circular-dash 1.4s ease-in-out infinite',\n // Backward compatible logic between JSS v9 and v10.\n // To remove with the release of Material-UI v4\n animationName: '$mui-progress-circular-dash',\n // Some default value that looks fine waiting for the animation to kicks in.\n strokeDasharray: '80px, 200px',\n strokeDashoffset: '0px' // Add the unit to fix a Edge 16 and below bug.\n\n },\n '@keyframes mui-progress-circular-rotate': {\n '100%': {\n transform: 'rotate(360deg)'\n }\n },\n '@keyframes mui-progress-circular-dash': {\n '0%': {\n strokeDasharray: '1px, 200px',\n strokeDashoffset: '0px'\n },\n '50%': {\n strokeDasharray: '100px, 200px',\n strokeDashoffset: '-15px'\n },\n '100%': {\n strokeDasharray: '100px, 200px',\n strokeDashoffset: '-125px'\n }\n },\n\n /* Styles applied to the `circle` svg path if `disableShrink={true}`. */\n circleDisableShrink: {\n animation: 'none'\n }\n };\n};\n/**\n * ## ARIA\n *\n * If the progress bar is describing the loading progress of a particular region of a page,\n * you should use `aria-describedby` to point to the progress bar, and set the `aria-busy`\n * attribute to `true` on that region until it has finished loading.\n */\n\n\nexports.styles = styles;\n\nfunction CircularProgress(props) {\n var _classNames, _classNames2;\n\n var classes = props.classes,\n className = props.className,\n color = props.color,\n disableShrink = props.disableShrink,\n size = props.size,\n style = props.style,\n thickness = props.thickness,\n value = props.value,\n variant = props.variant,\n other = (0, _objectWithoutProperties2.default)(props, [\"classes\", \"className\", \"color\", \"disableShrink\", \"size\", \"style\", \"thickness\", \"value\", \"variant\"]);\n var circleStyle = {};\n var rootStyle = {};\n var rootProps = {};\n\n if (variant === 'determinate' || variant === 'static') {\n var circumference = 2 * Math.PI * ((SIZE - thickness) / 2);\n circleStyle.strokeDasharray = circumference.toFixed(3);\n rootProps['aria-valuenow'] = Math.round(value);\n\n if (variant === 'static') {\n circleStyle.strokeDashoffset = \"\".concat(((100 - value) / 100 * circumference).toFixed(3), \"px\");\n rootStyle.transform = 'rotate(-90deg)';\n } else {\n circleStyle.strokeDashoffset = \"\".concat((easeIn((100 - value) / 100) * circumference).toFixed(3), \"px\");\n rootStyle.transform = \"rotate(\".concat((easeOut(value / 70) * 270).toFixed(3), \"deg)\");\n }\n }\n\n return _react.default.createElement(\"div\", (0, _extends2.default)({\n className: (0, _classnames.default)(classes.root, (_classNames = {}, (0, _defineProperty2.default)(_classNames, classes[\"color\".concat((0, _helpers.capitalize)(color))], color !== 'inherit'), (0, _defineProperty2.default)(_classNames, classes.indeterminate, variant === 'indeterminate'), (0, _defineProperty2.default)(_classNames, classes.static, variant === 'static'), _classNames), className),\n style: (0, _extends2.default)({\n width: size,\n height: size\n }, rootStyle, style),\n role: \"progressbar\"\n }, rootProps, other), _react.default.createElement(\"svg\", {\n className: classes.svg,\n viewBox: \"\".concat(SIZE / 2, \" \").concat(SIZE / 2, \" \").concat(SIZE, \" \").concat(SIZE)\n }, _react.default.createElement(\"circle\", {\n className: (0, _classnames.default)(classes.circle, (_classNames2 = {}, (0, _defineProperty2.default)(_classNames2, classes.circleIndeterminate, variant === 'indeterminate'), (0, _defineProperty2.default)(_classNames2, classes.circleStatic, variant === 'static'), (0, _defineProperty2.default)(_classNames2, classes.circleDisableShrink, disableShrink), _classNames2)),\n style: circleStyle,\n cx: SIZE,\n cy: SIZE,\n r: (SIZE - thickness) / 2,\n fill: \"none\",\n strokeWidth: thickness\n })));\n}\n\n true ? CircularProgress.propTypes = {\n /**\n * Override or extend the styles applied to the component.\n * See [CSS API](#css-api) below for more details.\n */\n classes: _propTypes.default.object.isRequired,\n\n /**\n * @ignore\n */\n className: _propTypes.default.string,\n\n /**\n * The color of the component. It supports those theme colors that make sense for this component.\n */\n color: _propTypes.default.oneOf(['primary', 'secondary', 'inherit']),\n\n /**\n * If `true`, the shrink animation is disabled.\n * This only works if variant is `indeterminate`.\n */\n disableShrink: (0, _utils.chainPropTypes)(_propTypes.default.bool, function (props) {\n /* istanbul ignore if */\n if (props.disableShrink && props.variant !== 'indeterminate') {\n return new Error('Material-UI: you have provided the `disableShrink` property ' + 'with a variant other than `indeterminate`. This will have no effect.');\n }\n\n return null;\n }),\n\n /**\n * The size of the circle.\n */\n size: _propTypes.default.oneOfType([_propTypes.default.number, _propTypes.default.string]),\n\n /**\n * @ignore\n */\n style: _propTypes.default.object,\n\n /**\n * The thickness of the circle.\n */\n thickness: _propTypes.default.number,\n\n /**\n * The value of the progress indicator for the determinate and static variants.\n * Value between 0 and 100.\n */\n value: _propTypes.default.number,\n\n /**\n * The variant to use.\n * Use indeterminate when there is no progress value.\n */\n variant: _propTypes.default.oneOf(['determinate', 'indeterminate', 'static'])\n} : undefined;\nCircularProgress.defaultProps = {\n color: 'primary',\n disableShrink: false,\n size: 40,\n thickness: 3.6,\n value: 0,\n variant: 'indeterminate'\n};\n\nvar _default = (0, _withStyles.default)(styles, {\n name: 'MuiCircularProgress',\n flip: false\n})(CircularProgress);\n\nexports.default = _default;\n\n//# sourceURL=webpack:///./node_modules/@material-ui/core/CircularProgress/CircularProgress.js?"); - - /***/ }), - - /***/ "./node_modules/@material-ui/core/CircularProgress/index.js": - /*!******************************************************************!*\ - !*** ./node_modules/@material-ui/core/CircularProgress/index.js ***! - \******************************************************************/ - /*! no static exports found */ - /***/ (function(module, exports, __webpack_require__) { - - "use strict"; - eval("\n\nvar _interopRequireDefault = __webpack_require__(/*! @babel/runtime/helpers/interopRequireDefault */ \"./node_modules/@babel/runtime/helpers/interopRequireDefault.js\");\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nObject.defineProperty(exports, \"default\", {\n enumerable: true,\n get: function get() {\n return _CircularProgress.default;\n }\n});\n\nvar _CircularProgress = _interopRequireDefault(__webpack_require__(/*! ./CircularProgress */ \"./node_modules/@material-ui/core/CircularProgress/CircularProgress.js\"));\n\n//# sourceURL=webpack:///./node_modules/@material-ui/core/CircularProgress/index.js?"); - - /***/ }), - - /***/ "./node_modules/@material-ui/core/Dialog/Dialog.js": - /*!*********************************************************!*\ - !*** ./node_modules/@material-ui/core/Dialog/Dialog.js ***! - \*********************************************************/ - /*! no static exports found */ - /***/ (function(module, exports, __webpack_require__) { - - "use strict"; - eval("\n\nvar _interopRequireDefault = __webpack_require__(/*! @babel/runtime/helpers/interopRequireDefault */ \"./node_modules/@babel/runtime/helpers/interopRequireDefault.js\");\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.default = exports.styles = void 0;\n\nvar _extends2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/extends */ \"./node_modules/@babel/runtime/helpers/extends.js\"));\n\nvar _objectWithoutProperties2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/objectWithoutProperties */ \"./node_modules/@babel/runtime/helpers/objectWithoutProperties.js\"));\n\nvar _classCallCheck2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/classCallCheck */ \"./node_modules/@babel/runtime/helpers/classCallCheck.js\"));\n\nvar _createClass2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/createClass */ \"./node_modules/@babel/runtime/helpers/createClass.js\"));\n\nvar _possibleConstructorReturn2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/possibleConstructorReturn */ \"./node_modules/@babel/runtime/helpers/possibleConstructorReturn.js\"));\n\nvar _getPrototypeOf3 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/getPrototypeOf */ \"./node_modules/@babel/runtime/helpers/getPrototypeOf.js\"));\n\nvar _inherits2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/inherits */ \"./node_modules/@babel/runtime/helpers/inherits.js\"));\n\nvar _defineProperty2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/defineProperty */ \"./node_modules/@babel/runtime/helpers/defineProperty.js\"));\n\nvar _react = _interopRequireDefault(__webpack_require__(/*! react */ \"./node_modules/react/index.js\"));\n\nvar _propTypes = _interopRequireDefault(__webpack_require__(/*! prop-types */ \"./node_modules/prop-types/index.js\"));\n\nvar _classnames = _interopRequireDefault(__webpack_require__(/*! classnames */ \"./node_modules/classnames/index.js\"));\n\nvar _utils = __webpack_require__(/*! @material-ui/utils */ \"./node_modules/@material-ui/utils/index.es.js\");\n\nvar _withStyles = _interopRequireDefault(__webpack_require__(/*! ../styles/withStyles */ \"./node_modules/@material-ui/core/styles/withStyles.js\"));\n\nvar _helpers = __webpack_require__(/*! ../utils/helpers */ \"./node_modules/@material-ui/core/utils/helpers.js\");\n\nvar _Modal = _interopRequireDefault(__webpack_require__(/*! ../Modal */ \"./node_modules/@material-ui/core/Modal/index.js\"));\n\nvar _Fade = _interopRequireDefault(__webpack_require__(/*! ../Fade */ \"./node_modules/@material-ui/core/Fade/index.js\"));\n\nvar _transitions = __webpack_require__(/*! ../styles/transitions */ \"./node_modules/@material-ui/core/styles/transitions.js\");\n\nvar _Paper = _interopRequireDefault(__webpack_require__(/*! ../Paper */ \"./node_modules/@material-ui/core/Paper/index.js\"));\n\n/* eslint-disable jsx-a11y/click-events-have-key-events */\n\n/* eslint-disable jsx-a11y/no-noninteractive-element-interactions */\n// @inheritedComponent Modal\nvar styles = function styles(theme) {\n return {\n /* Styles applied to the root element. */\n root: {},\n\n /* Styles applied to the root element if `scroll=\"paper\"`. */\n scrollPaper: {\n display: 'flex',\n justifyContent: 'center',\n alignItems: 'center'\n },\n\n /* Styles applied to the root element if `scroll=\"body\"`. */\n scrollBody: {\n overflowY: 'auto',\n overflowX: 'hidden'\n },\n\n /* Styles applied to the container element. */\n container: {\n height: '100%',\n // We disable the focus ring for mouse, touch and keyboard users.\n outline: 'none'\n },\n\n /* Styles applied to the `Paper` component. */\n paper: {\n display: 'flex',\n flexDirection: 'column',\n margin: 48,\n position: 'relative',\n overflowY: 'auto' // Fix IE 11 issue, to remove at some point.\n\n },\n\n /* Styles applied to the `Paper` component if `scroll=\"paper\"`. */\n paperScrollPaper: {\n flex: '0 1 auto',\n maxHeight: 'calc(100% - 96px)'\n },\n\n /* Styles applied to the `Paper` component if `scroll=\"body\"`. */\n paperScrollBody: {\n margin: '48px auto'\n },\n\n /* Styles applied to the `Paper` component if `maxWidth=\"xs\"`. */\n paperWidthXs: {\n maxWidth: Math.max(theme.breakpoints.values.xs, 360),\n '&$paperScrollBody': (0, _defineProperty2.default)({}, theme.breakpoints.down(Math.max(theme.breakpoints.values.xs, 360) + 48 * 2), {\n margin: 48\n })\n },\n\n /* Styles applied to the `Paper` component if `maxWidth=\"sm\"`. */\n paperWidthSm: {\n maxWidth: theme.breakpoints.values.sm,\n '&$paperScrollBody': (0, _defineProperty2.default)({}, theme.breakpoints.down(theme.breakpoints.values.sm + 48 * 2), {\n margin: 48\n })\n },\n\n /* Styles applied to the `Paper` component if `maxWidth=\"md\"`. */\n paperWidthMd: {\n maxWidth: theme.breakpoints.values.md,\n '&$paperScrollBody': (0, _defineProperty2.default)({}, theme.breakpoints.down(theme.breakpoints.values.md + 48 * 2), {\n margin: 48\n })\n },\n\n /* Styles applied to the `Paper` component if `maxWidth=\"lg\"`. */\n paperWidthLg: {\n maxWidth: theme.breakpoints.values.lg,\n '&$paperScrollBody': (0, _defineProperty2.default)({}, theme.breakpoints.down(theme.breakpoints.values.lg + 48 * 2), {\n margin: 48\n })\n },\n\n /* Styles applied to the `Paper` component if `maxWidth=\"xl\"`. */\n paperWidthXl: {\n maxWidth: theme.breakpoints.values.xl,\n '&$paperScrollBody': (0, _defineProperty2.default)({}, theme.breakpoints.down(theme.breakpoints.values.xl + 48 * 2), {\n margin: 48\n })\n },\n\n /* Styles applied to the `Paper` component if `fullWidth={true}`. */\n paperFullWidth: {\n width: '100%'\n },\n\n /* Styles applied to the `Paper` component if `fullScreen={true}`. */\n paperFullScreen: {\n margin: 0,\n width: '100%',\n maxWidth: '100%',\n height: '100%',\n maxHeight: 'none',\n borderRadius: 0,\n '&$paperScrollBody': {\n margin: 0\n }\n }\n };\n};\n/**\n * Dialogs are overlaid modal paper based components with a backdrop.\n */\n\n\nexports.styles = styles;\n\nvar Dialog =\n/*#__PURE__*/\nfunction (_React$Component) {\n (0, _inherits2.default)(Dialog, _React$Component);\n\n function Dialog() {\n var _getPrototypeOf2;\n\n var _this;\n\n (0, _classCallCheck2.default)(this, Dialog);\n\n for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {\n args[_key] = arguments[_key];\n }\n\n _this = (0, _possibleConstructorReturn2.default)(this, (_getPrototypeOf2 = (0, _getPrototypeOf3.default)(Dialog)).call.apply(_getPrototypeOf2, [this].concat(args)));\n\n _this.handleMouseDown = function (event) {\n _this.mouseDownTarget = event.target;\n };\n\n _this.handleBackdropClick = function (event) {\n // Ignore the events not coming from the \"backdrop\"\n // We don't want to close the dialog when clicking the dialog content.\n if (event.target !== event.currentTarget) {\n return;\n } // Make sure the event starts and ends on the same DOM element.\n\n\n if (event.target !== _this.mouseDownTarget) {\n return;\n }\n\n _this.mouseDownTarget = null;\n\n if (_this.props.onBackdropClick) {\n _this.props.onBackdropClick(event);\n }\n\n if (!_this.props.disableBackdropClick && _this.props.onClose) {\n _this.props.onClose(event, 'backdropClick');\n }\n };\n\n return _this;\n }\n\n (0, _createClass2.default)(Dialog, [{\n key: \"render\",\n value: function render() {\n var _classNames;\n\n var _this$props = this.props,\n BackdropProps = _this$props.BackdropProps,\n children = _this$props.children,\n classes = _this$props.classes,\n className = _this$props.className,\n disableBackdropClick = _this$props.disableBackdropClick,\n disableEscapeKeyDown = _this$props.disableEscapeKeyDown,\n fullScreen = _this$props.fullScreen,\n fullWidth = _this$props.fullWidth,\n maxWidth = _this$props.maxWidth,\n onBackdropClick = _this$props.onBackdropClick,\n onClose = _this$props.onClose,\n onEnter = _this$props.onEnter,\n onEntered = _this$props.onEntered,\n onEntering = _this$props.onEntering,\n onEscapeKeyDown = _this$props.onEscapeKeyDown,\n onExit = _this$props.onExit,\n onExited = _this$props.onExited,\n onExiting = _this$props.onExiting,\n open = _this$props.open,\n PaperComponent = _this$props.PaperComponent,\n _this$props$PaperProp = _this$props.PaperProps,\n PaperProps = _this$props$PaperProp === void 0 ? {} : _this$props$PaperProp,\n scroll = _this$props.scroll,\n TransitionComponent = _this$props.TransitionComponent,\n transitionDuration = _this$props.transitionDuration,\n TransitionProps = _this$props.TransitionProps,\n other = (0, _objectWithoutProperties2.default)(_this$props, [\"BackdropProps\", \"children\", \"classes\", \"className\", \"disableBackdropClick\", \"disableEscapeKeyDown\", \"fullScreen\", \"fullWidth\", \"maxWidth\", \"onBackdropClick\", \"onClose\", \"onEnter\", \"onEntered\", \"onEntering\", \"onEscapeKeyDown\", \"onExit\", \"onExited\", \"onExiting\", \"open\", \"PaperComponent\", \"PaperProps\", \"scroll\", \"TransitionComponent\", \"transitionDuration\", \"TransitionProps\"]);\n return _react.default.createElement(_Modal.default, (0, _extends2.default)({\n className: (0, _classnames.default)(classes.root, className),\n BackdropProps: (0, _extends2.default)({\n transitionDuration: transitionDuration\n }, BackdropProps),\n closeAfterTransition: true,\n disableBackdropClick: disableBackdropClick,\n disableEscapeKeyDown: disableEscapeKeyDown,\n onBackdropClick: onBackdropClick,\n onEscapeKeyDown: onEscapeKeyDown,\n onClose: onClose,\n open: open,\n role: \"dialog\"\n }, other), _react.default.createElement(TransitionComponent, (0, _extends2.default)({\n appear: true,\n in: open,\n timeout: transitionDuration,\n onEnter: onEnter,\n onEntering: onEntering,\n onEntered: onEntered,\n onExit: onExit,\n onExiting: onExiting,\n onExited: onExited\n }, TransitionProps), _react.default.createElement(\"div\", {\n className: (0, _classnames.default)(classes.container, classes[\"scroll\".concat((0, _helpers.capitalize)(scroll))]),\n onClick: this.handleBackdropClick,\n onMouseDown: this.handleMouseDown,\n role: \"document\"\n }, _react.default.createElement(PaperComponent, (0, _extends2.default)({\n elevation: 24\n }, PaperProps, {\n className: (0, _classnames.default)(classes.paper, classes[\"paperScroll\".concat((0, _helpers.capitalize)(scroll))], (_classNames = {}, (0, _defineProperty2.default)(_classNames, classes[\"paperWidth\".concat(maxWidth ? (0, _helpers.capitalize)(maxWidth) : '')], maxWidth), (0, _defineProperty2.default)(_classNames, classes.paperFullScreen, fullScreen), (0, _defineProperty2.default)(_classNames, classes.paperFullWidth, fullWidth), _classNames), PaperProps.className)\n }), children))));\n }\n }]);\n return Dialog;\n}(_react.default.Component);\n\n true ? Dialog.propTypes = {\n /**\n * @ignore\n */\n BackdropProps: _propTypes.default.object,\n\n /**\n * Dialog children, usually the included sub-components.\n */\n children: _propTypes.default.node.isRequired,\n\n /**\n * Override or extend the styles applied to the component.\n * See [CSS API](#css-api) below for more details.\n */\n classes: _propTypes.default.object.isRequired,\n\n /**\n * @ignore\n */\n className: _propTypes.default.string,\n\n /**\n * If `true`, clicking the backdrop will not fire the `onClose` callback.\n */\n disableBackdropClick: _propTypes.default.bool,\n\n /**\n * If `true`, hitting escape will not fire the `onClose` callback.\n */\n disableEscapeKeyDown: _propTypes.default.bool,\n\n /**\n * If `true`, the dialog will be full-screen\n */\n fullScreen: _propTypes.default.bool,\n\n /**\n * If `true`, the dialog stretches to `maxWidth`.\n */\n fullWidth: _propTypes.default.bool,\n\n /**\n * Determine the max width of the dialog.\n * The dialog width grows with the size of the screen, this property is useful\n * on the desktop where you might need some coherent different width size across your\n * application. Set to `false` to disable `maxWidth`.\n */\n maxWidth: _propTypes.default.oneOf(['xs', 'sm', 'md', 'lg', 'xl', false]),\n\n /**\n * Callback fired when the backdrop is clicked.\n */\n onBackdropClick: _propTypes.default.func,\n\n /**\n * Callback fired when the component requests to be closed.\n *\n * @param {object} event The event source of the callback\n * @param {string} reason Can be:`\"escapeKeyDown\"`, `\"backdropClick\"`\n */\n onClose: _propTypes.default.func,\n\n /**\n * Callback fired before the dialog enters.\n */\n onEnter: _propTypes.default.func,\n\n /**\n * Callback fired when the dialog has entered.\n */\n onEntered: _propTypes.default.func,\n\n /**\n * Callback fired when the dialog is entering.\n */\n onEntering: _propTypes.default.func,\n\n /**\n * Callback fired when the escape key is pressed,\n * `disableKeyboard` is false and the modal is in focus.\n */\n onEscapeKeyDown: _propTypes.default.func,\n\n /**\n * Callback fired before the dialog exits.\n */\n onExit: _propTypes.default.func,\n\n /**\n * Callback fired when the dialog has exited.\n */\n onExited: _propTypes.default.func,\n\n /**\n * Callback fired when the dialog is exiting.\n */\n onExiting: _propTypes.default.func,\n\n /**\n * If `true`, the Dialog is open.\n */\n open: _propTypes.default.bool.isRequired,\n\n /**\n * The component used to render the body of the dialog.\n */\n PaperComponent: _utils.componentPropType,\n\n /**\n * Properties applied to the [`Paper`](/api/paper/) element.\n */\n PaperProps: _propTypes.default.object,\n\n /**\n * Determine the container for scrolling the dialog.\n */\n scroll: _propTypes.default.oneOf(['body', 'paper']),\n\n /**\n * The component used for the transition.\n */\n TransitionComponent: _utils.componentPropType,\n\n /**\n * The duration for the transition, in milliseconds.\n * You may specify a single timeout for all transitions, or individually with an object.\n */\n transitionDuration: _propTypes.default.oneOfType([_propTypes.default.number, _propTypes.default.shape({\n enter: _propTypes.default.number,\n exit: _propTypes.default.number\n })]),\n\n /**\n * Properties applied to the `Transition` element.\n */\n TransitionProps: _propTypes.default.object\n} : undefined;\nDialog.defaultProps = {\n disableBackdropClick: false,\n disableEscapeKeyDown: false,\n fullScreen: false,\n fullWidth: false,\n maxWidth: 'sm',\n PaperComponent: _Paper.default,\n scroll: 'paper',\n TransitionComponent: _Fade.default,\n transitionDuration: {\n enter: _transitions.duration.enteringScreen,\n exit: _transitions.duration.leavingScreen\n }\n};\n\nvar _default = (0, _withStyles.default)(styles, {\n name: 'MuiDialog'\n})(Dialog);\n\nexports.default = _default;\n\n//# sourceURL=webpack:///./node_modules/@material-ui/core/Dialog/Dialog.js?"); - - /***/ }), - - /***/ "./node_modules/@material-ui/core/Dialog/index.js": - /*!********************************************************!*\ - !*** ./node_modules/@material-ui/core/Dialog/index.js ***! - \********************************************************/ - /*! no static exports found */ - /***/ (function(module, exports, __webpack_require__) { - - "use strict"; - eval("\n\nvar _interopRequireDefault = __webpack_require__(/*! @babel/runtime/helpers/interopRequireDefault */ \"./node_modules/@babel/runtime/helpers/interopRequireDefault.js\");\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nObject.defineProperty(exports, \"default\", {\n enumerable: true,\n get: function get() {\n return _Dialog.default;\n }\n});\n\nvar _Dialog = _interopRequireDefault(__webpack_require__(/*! ./Dialog */ \"./node_modules/@material-ui/core/Dialog/Dialog.js\"));\n\n//# sourceURL=webpack:///./node_modules/@material-ui/core/Dialog/index.js?"); - - /***/ }), - - /***/ "./node_modules/@material-ui/core/DialogActions/DialogActions.js": - /*!***********************************************************************!*\ - !*** ./node_modules/@material-ui/core/DialogActions/DialogActions.js ***! - \***********************************************************************/ - /*! no static exports found */ - /***/ (function(module, exports, __webpack_require__) { - - "use strict"; - eval("\n\nvar _interopRequireDefault = __webpack_require__(/*! @babel/runtime/helpers/interopRequireDefault */ \"./node_modules/@babel/runtime/helpers/interopRequireDefault.js\");\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.default = exports.styles = void 0;\n\nvar _extends2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/extends */ \"./node_modules/@babel/runtime/helpers/extends.js\"));\n\nvar _objectWithoutProperties2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/objectWithoutProperties */ \"./node_modules/@babel/runtime/helpers/objectWithoutProperties.js\"));\n\nvar _react = _interopRequireDefault(__webpack_require__(/*! react */ \"./node_modules/react/index.js\"));\n\nvar _propTypes = _interopRequireDefault(__webpack_require__(/*! prop-types */ \"./node_modules/prop-types/index.js\"));\n\nvar _classnames = _interopRequireDefault(__webpack_require__(/*! classnames */ \"./node_modules/classnames/index.js\"));\n\nvar _withStyles = _interopRequireDefault(__webpack_require__(/*! ../styles/withStyles */ \"./node_modules/@material-ui/core/styles/withStyles.js\"));\n\nvar _reactHelpers = __webpack_require__(/*! ../utils/reactHelpers */ \"./node_modules/@material-ui/core/utils/reactHelpers.js\");\n\n__webpack_require__(/*! ../Button */ \"./node_modules/@material-ui/core/Button/index.js\");\n\n// So we don't have any override priority issue.\nvar styles = {\n /* Styles applied to the root element. */\n root: {\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'flex-end',\n flex: '0 0 auto',\n margin: '8px 4px'\n },\n\n /* Styles applied to the children. */\n action: {\n margin: '0 4px'\n }\n};\nexports.styles = styles;\n\nfunction DialogActions(props) {\n var disableActionSpacing = props.disableActionSpacing,\n children = props.children,\n classes = props.classes,\n className = props.className,\n other = (0, _objectWithoutProperties2.default)(props, [\"disableActionSpacing\", \"children\", \"classes\", \"className\"]);\n return _react.default.createElement(\"div\", (0, _extends2.default)({\n className: (0, _classnames.default)(classes.root, className)\n }, other), disableActionSpacing ? children : (0, _reactHelpers.cloneChildrenWithClassName)(children, classes.action));\n}\n\n true ? DialogActions.propTypes = {\n /**\n * The content of the component.\n */\n children: _propTypes.default.node,\n\n /**\n * Override or extend the styles applied to the component.\n * See [CSS API](#css-api) below for more details.\n */\n classes: _propTypes.default.object.isRequired,\n\n /**\n * @ignore\n */\n className: _propTypes.default.string,\n\n /**\n * If `true`, the dialog actions do not have additional margin.\n */\n disableActionSpacing: _propTypes.default.bool\n} : undefined;\nDialogActions.defaultProps = {\n disableActionSpacing: false\n};\n\nvar _default = (0, _withStyles.default)(styles, {\n name: 'MuiDialogActions'\n})(DialogActions);\n\nexports.default = _default;\n\n//# sourceURL=webpack:///./node_modules/@material-ui/core/DialogActions/DialogActions.js?"); - - /***/ }), - - /***/ "./node_modules/@material-ui/core/DialogActions/index.js": - /*!***************************************************************!*\ - !*** ./node_modules/@material-ui/core/DialogActions/index.js ***! - \***************************************************************/ - /*! no static exports found */ - /***/ (function(module, exports, __webpack_require__) { - - "use strict"; - eval("\n\nvar _interopRequireDefault = __webpack_require__(/*! @babel/runtime/helpers/interopRequireDefault */ \"./node_modules/@babel/runtime/helpers/interopRequireDefault.js\");\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nObject.defineProperty(exports, \"default\", {\n enumerable: true,\n get: function get() {\n return _DialogActions.default;\n }\n});\n\nvar _DialogActions = _interopRequireDefault(__webpack_require__(/*! ./DialogActions */ \"./node_modules/@material-ui/core/DialogActions/DialogActions.js\"));\n\n//# sourceURL=webpack:///./node_modules/@material-ui/core/DialogActions/index.js?"); - - /***/ }), - - /***/ "./node_modules/@material-ui/core/DialogContent/DialogContent.js": - /*!***********************************************************************!*\ - !*** ./node_modules/@material-ui/core/DialogContent/DialogContent.js ***! - \***********************************************************************/ - /*! no static exports found */ - /***/ (function(module, exports, __webpack_require__) { - - "use strict"; - eval("\n\nvar _interopRequireDefault = __webpack_require__(/*! @babel/runtime/helpers/interopRequireDefault */ \"./node_modules/@babel/runtime/helpers/interopRequireDefault.js\");\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.default = exports.styles = void 0;\n\nvar _extends2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/extends */ \"./node_modules/@babel/runtime/helpers/extends.js\"));\n\nvar _objectWithoutProperties2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/objectWithoutProperties */ \"./node_modules/@babel/runtime/helpers/objectWithoutProperties.js\"));\n\nvar _react = _interopRequireDefault(__webpack_require__(/*! react */ \"./node_modules/react/index.js\"));\n\nvar _propTypes = _interopRequireDefault(__webpack_require__(/*! prop-types */ \"./node_modules/prop-types/index.js\"));\n\nvar _classnames = _interopRequireDefault(__webpack_require__(/*! classnames */ \"./node_modules/classnames/index.js\"));\n\nvar _withStyles = _interopRequireDefault(__webpack_require__(/*! ../styles/withStyles */ \"./node_modules/@material-ui/core/styles/withStyles.js\"));\n\nvar styles = {\n /* Styles applied to the root element. */\n root: {\n flex: '1 1 auto',\n overflowY: 'auto',\n WebkitOverflowScrolling: 'touch',\n // Add iOS momentum scrolling.\n padding: '0 24px 24px',\n '&:first-child': {\n paddingTop: 24\n }\n }\n};\nexports.styles = styles;\n\nfunction DialogContent(props) {\n var classes = props.classes,\n children = props.children,\n className = props.className,\n other = (0, _objectWithoutProperties2.default)(props, [\"classes\", \"children\", \"className\"]);\n return _react.default.createElement(\"div\", (0, _extends2.default)({\n className: (0, _classnames.default)(classes.root, className)\n }, other), children);\n}\n\n true ? DialogContent.propTypes = {\n /**\n * The content of the component.\n */\n children: _propTypes.default.node,\n\n /**\n * Override or extend the styles applied to the component.\n * See [CSS API](#css-api) below for more details.\n */\n classes: _propTypes.default.object.isRequired,\n\n /**\n * @ignore\n */\n className: _propTypes.default.string\n} : undefined;\n\nvar _default = (0, _withStyles.default)(styles, {\n name: 'MuiDialogContent'\n})(DialogContent);\n\nexports.default = _default;\n\n//# sourceURL=webpack:///./node_modules/@material-ui/core/DialogContent/DialogContent.js?"); - - /***/ }), - - /***/ "./node_modules/@material-ui/core/DialogContent/index.js": - /*!***************************************************************!*\ - !*** ./node_modules/@material-ui/core/DialogContent/index.js ***! - \***************************************************************/ - /*! no static exports found */ - /***/ (function(module, exports, __webpack_require__) { - - "use strict"; - eval("\n\nvar _interopRequireDefault = __webpack_require__(/*! @babel/runtime/helpers/interopRequireDefault */ \"./node_modules/@babel/runtime/helpers/interopRequireDefault.js\");\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nObject.defineProperty(exports, \"default\", {\n enumerable: true,\n get: function get() {\n return _DialogContent.default;\n }\n});\n\nvar _DialogContent = _interopRequireDefault(__webpack_require__(/*! ./DialogContent */ \"./node_modules/@material-ui/core/DialogContent/DialogContent.js\"));\n\n//# sourceURL=webpack:///./node_modules/@material-ui/core/DialogContent/index.js?"); - - /***/ }), - - /***/ "./node_modules/@material-ui/core/DialogContentText/DialogContentText.js": - /*!*******************************************************************************!*\ - !*** ./node_modules/@material-ui/core/DialogContentText/DialogContentText.js ***! - \*******************************************************************************/ - /*! no static exports found */ - /***/ (function(module, exports, __webpack_require__) { - - "use strict"; - eval("\n\nvar _interopRequireDefault = __webpack_require__(/*! @babel/runtime/helpers/interopRequireDefault */ \"./node_modules/@babel/runtime/helpers/interopRequireDefault.js\");\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.default = exports.styles = void 0;\n\nvar _extends2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/extends */ \"./node_modules/@babel/runtime/helpers/extends.js\"));\n\nvar _react = _interopRequireDefault(__webpack_require__(/*! react */ \"./node_modules/react/index.js\"));\n\nvar _propTypes = _interopRequireDefault(__webpack_require__(/*! prop-types */ \"./node_modules/prop-types/index.js\"));\n\nvar _withStyles = _interopRequireDefault(__webpack_require__(/*! ../styles/withStyles */ \"./node_modules/@material-ui/core/styles/withStyles.js\"));\n\nvar _Typography = _interopRequireDefault(__webpack_require__(/*! ../Typography */ \"./node_modules/@material-ui/core/Typography/index.js\"));\n\n// @inheritedComponent Typography\nvar styles = {\n /* Styles applied to the root element. */\n root: {\n // Should use variant=\"body1\" in v4\n lineHeight: 1.5\n }\n};\nexports.styles = styles;\n\nfunction DialogContentText(props) {\n return _react.default.createElement(_Typography.default, (0, _extends2.default)({\n component: \"p\",\n internalDeprecatedVariant: true,\n variant: \"subheading\",\n color: \"textSecondary\"\n }, props));\n}\n\n true ? DialogContentText.propTypes = {\n /**\n * The content of the component.\n */\n children: _propTypes.default.node,\n\n /**\n * Override or extend the styles applied to the component.\n * See [CSS API](#css-api) below for more details.\n */\n classes: _propTypes.default.object.isRequired\n} : undefined;\n\nvar _default = (0, _withStyles.default)(styles, {\n name: 'MuiDialogContentText'\n})(DialogContentText);\n\nexports.default = _default;\n\n//# sourceURL=webpack:///./node_modules/@material-ui/core/DialogContentText/DialogContentText.js?"); - - /***/ }), - - /***/ "./node_modules/@material-ui/core/DialogContentText/index.js": - /*!*******************************************************************!*\ - !*** ./node_modules/@material-ui/core/DialogContentText/index.js ***! - \*******************************************************************/ - /*! no static exports found */ - /***/ (function(module, exports, __webpack_require__) { - - "use strict"; - eval("\n\nvar _interopRequireDefault = __webpack_require__(/*! @babel/runtime/helpers/interopRequireDefault */ \"./node_modules/@babel/runtime/helpers/interopRequireDefault.js\");\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nObject.defineProperty(exports, \"default\", {\n enumerable: true,\n get: function get() {\n return _DialogContentText.default;\n }\n});\n\nvar _DialogContentText = _interopRequireDefault(__webpack_require__(/*! ./DialogContentText */ \"./node_modules/@material-ui/core/DialogContentText/DialogContentText.js\"));\n\n//# sourceURL=webpack:///./node_modules/@material-ui/core/DialogContentText/index.js?"); - - /***/ }), - - /***/ "./node_modules/@material-ui/core/DialogTitle/DialogTitle.js": - /*!*******************************************************************!*\ - !*** ./node_modules/@material-ui/core/DialogTitle/DialogTitle.js ***! - \*******************************************************************/ - /*! no static exports found */ - /***/ (function(module, exports, __webpack_require__) { - - "use strict"; - eval("\n\nvar _interopRequireDefault = __webpack_require__(/*! @babel/runtime/helpers/interopRequireDefault */ \"./node_modules/@babel/runtime/helpers/interopRequireDefault.js\");\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.default = exports.styles = void 0;\n\nvar _extends2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/extends */ \"./node_modules/@babel/runtime/helpers/extends.js\"));\n\nvar _objectWithoutProperties2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/objectWithoutProperties */ \"./node_modules/@babel/runtime/helpers/objectWithoutProperties.js\"));\n\nvar _react = _interopRequireDefault(__webpack_require__(/*! react */ \"./node_modules/react/index.js\"));\n\nvar _propTypes = _interopRequireDefault(__webpack_require__(/*! prop-types */ \"./node_modules/prop-types/index.js\"));\n\nvar _classnames = _interopRequireDefault(__webpack_require__(/*! classnames */ \"./node_modules/classnames/index.js\"));\n\nvar _withStyles = _interopRequireDefault(__webpack_require__(/*! ../styles/withStyles */ \"./node_modules/@material-ui/core/styles/withStyles.js\"));\n\nvar _Typography = _interopRequireDefault(__webpack_require__(/*! ../Typography */ \"./node_modules/@material-ui/core/Typography/index.js\"));\n\nvar styles = {\n /* Styles applied to the root element. */\n root: {\n margin: 0,\n padding: '24px 24px 20px',\n flex: '0 0 auto'\n }\n};\nexports.styles = styles;\n\nfunction DialogTitle(props) {\n var children = props.children,\n classes = props.classes,\n className = props.className,\n disableTypography = props.disableTypography,\n other = (0, _objectWithoutProperties2.default)(props, [\"children\", \"classes\", \"className\", \"disableTypography\"]);\n return _react.default.createElement(\"div\", (0, _extends2.default)({\n className: (0, _classnames.default)(classes.root, className)\n }, other), disableTypography ? children : _react.default.createElement(_Typography.default, {\n variant: \"title\",\n internalDeprecatedVariant: true\n }, children));\n}\n\n true ? DialogTitle.propTypes = {\n /**\n * The content of the component.\n */\n children: _propTypes.default.node.isRequired,\n\n /**\n * Override or extend the styles applied to the component.\n * See [CSS API](#css-api) below for more details.\n */\n classes: _propTypes.default.object.isRequired,\n\n /**\n * @ignore\n */\n className: _propTypes.default.string,\n\n /**\n * If `true`, the children won't be wrapped by a typography component.\n * For instance, this can be useful to render an h4 instead of the default h2.\n */\n disableTypography: _propTypes.default.bool\n} : undefined;\nDialogTitle.defaultProps = {\n disableTypography: false\n};\n\nvar _default = (0, _withStyles.default)(styles, {\n name: 'MuiDialogTitle'\n})(DialogTitle);\n\nexports.default = _default;\n\n//# sourceURL=webpack:///./node_modules/@material-ui/core/DialogTitle/DialogTitle.js?"); - - /***/ }), - - /***/ "./node_modules/@material-ui/core/DialogTitle/index.js": - /*!*************************************************************!*\ - !*** ./node_modules/@material-ui/core/DialogTitle/index.js ***! - \*************************************************************/ - /*! no static exports found */ - /***/ (function(module, exports, __webpack_require__) { - - "use strict"; - eval("\n\nvar _interopRequireDefault = __webpack_require__(/*! @babel/runtime/helpers/interopRequireDefault */ \"./node_modules/@babel/runtime/helpers/interopRequireDefault.js\");\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nObject.defineProperty(exports, \"default\", {\n enumerable: true,\n get: function get() {\n return _DialogTitle.default;\n }\n});\n\nvar _DialogTitle = _interopRequireDefault(__webpack_require__(/*! ./DialogTitle */ \"./node_modules/@material-ui/core/DialogTitle/DialogTitle.js\"));\n\n//# sourceURL=webpack:///./node_modules/@material-ui/core/DialogTitle/index.js?"); - - /***/ }), - - /***/ "./node_modules/@material-ui/core/Fade/Fade.js": - /*!*****************************************************!*\ - !*** ./node_modules/@material-ui/core/Fade/Fade.js ***! - \*****************************************************/ - /*! no static exports found */ - /***/ (function(module, exports, __webpack_require__) { - - "use strict"; - eval("\n\nvar _interopRequireDefault = __webpack_require__(/*! @babel/runtime/helpers/interopRequireDefault */ \"./node_modules/@babel/runtime/helpers/interopRequireDefault.js\");\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.default = void 0;\n\nvar _extends2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/extends */ \"./node_modules/@babel/runtime/helpers/extends.js\"));\n\nvar _objectWithoutProperties2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/objectWithoutProperties */ \"./node_modules/@babel/runtime/helpers/objectWithoutProperties.js\"));\n\nvar _classCallCheck2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/classCallCheck */ \"./node_modules/@babel/runtime/helpers/classCallCheck.js\"));\n\nvar _createClass2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/createClass */ \"./node_modules/@babel/runtime/helpers/createClass.js\"));\n\nvar _possibleConstructorReturn2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/possibleConstructorReturn */ \"./node_modules/@babel/runtime/helpers/possibleConstructorReturn.js\"));\n\nvar _getPrototypeOf3 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/getPrototypeOf */ \"./node_modules/@babel/runtime/helpers/getPrototypeOf.js\"));\n\nvar _inherits2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/inherits */ \"./node_modules/@babel/runtime/helpers/inherits.js\"));\n\nvar _react = _interopRequireDefault(__webpack_require__(/*! react */ \"./node_modules/react/index.js\"));\n\nvar _propTypes = _interopRequireDefault(__webpack_require__(/*! prop-types */ \"./node_modules/prop-types/index.js\"));\n\nvar _Transition = _interopRequireDefault(__webpack_require__(/*! react-transition-group/Transition */ \"./node_modules/react-transition-group/Transition.js\"));\n\nvar _transitions = __webpack_require__(/*! ../styles/transitions */ \"./node_modules/@material-ui/core/styles/transitions.js\");\n\nvar _withTheme = _interopRequireDefault(__webpack_require__(/*! ../styles/withTheme */ \"./node_modules/@material-ui/core/styles/withTheme.js\"));\n\nvar _utils = __webpack_require__(/*! ../transitions/utils */ \"./node_modules/@material-ui/core/transitions/utils.js\");\n\n// @inheritedComponent Transition\nvar styles = {\n entering: {\n opacity: 1\n },\n entered: {\n opacity: 1\n }\n};\n/**\n * The Fade transition is used by the [Modal](/utils/modal/) component.\n * It uses [react-transition-group](https://github.com/reactjs/react-transition-group) internally.\n */\n\nvar Fade =\n/*#__PURE__*/\nfunction (_React$Component) {\n (0, _inherits2.default)(Fade, _React$Component);\n\n function Fade() {\n var _getPrototypeOf2;\n\n var _this;\n\n (0, _classCallCheck2.default)(this, Fade);\n\n for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {\n args[_key] = arguments[_key];\n }\n\n _this = (0, _possibleConstructorReturn2.default)(this, (_getPrototypeOf2 = (0, _getPrototypeOf3.default)(Fade)).call.apply(_getPrototypeOf2, [this].concat(args)));\n\n _this.handleEnter = function (node) {\n var theme = _this.props.theme;\n (0, _utils.reflow)(node); // So the animation always start from the start.\n\n var transitionProps = (0, _utils.getTransitionProps)(_this.props, {\n mode: 'enter'\n });\n node.style.webkitTransition = theme.transitions.create('opacity', transitionProps);\n node.style.transition = theme.transitions.create('opacity', transitionProps);\n\n if (_this.props.onEnter) {\n _this.props.onEnter(node);\n }\n };\n\n _this.handleExit = function (node) {\n var theme = _this.props.theme;\n var transitionProps = (0, _utils.getTransitionProps)(_this.props, {\n mode: 'exit'\n });\n node.style.webkitTransition = theme.transitions.create('opacity', transitionProps);\n node.style.transition = theme.transitions.create('opacity', transitionProps);\n\n if (_this.props.onExit) {\n _this.props.onExit(node);\n }\n };\n\n return _this;\n }\n\n (0, _createClass2.default)(Fade, [{\n key: \"render\",\n value: function render() {\n var _this$props = this.props,\n children = _this$props.children,\n onEnter = _this$props.onEnter,\n onExit = _this$props.onExit,\n styleProp = _this$props.style,\n theme = _this$props.theme,\n other = (0, _objectWithoutProperties2.default)(_this$props, [\"children\", \"onEnter\", \"onExit\", \"style\", \"theme\"]);\n var style = (0, _extends2.default)({}, styleProp, _react.default.isValidElement(children) ? children.props.style : {});\n return _react.default.createElement(_Transition.default, (0, _extends2.default)({\n appear: true,\n onEnter: this.handleEnter,\n onExit: this.handleExit\n }, other), function (state, childProps) {\n return _react.default.cloneElement(children, (0, _extends2.default)({\n style: (0, _extends2.default)({\n opacity: 0\n }, styles[state], style)\n }, childProps));\n });\n }\n }]);\n return Fade;\n}(_react.default.Component);\n\n true ? Fade.propTypes = {\n /**\n * A single child content element.\n */\n children: _propTypes.default.oneOfType([_propTypes.default.element, _propTypes.default.func]),\n\n /**\n * If `true`, the component will transition in.\n */\n in: _propTypes.default.bool,\n\n /**\n * @ignore\n */\n onEnter: _propTypes.default.func,\n\n /**\n * @ignore\n */\n onExit: _propTypes.default.func,\n\n /**\n * @ignore\n */\n style: _propTypes.default.object,\n\n /**\n * @ignore\n */\n theme: _propTypes.default.object.isRequired,\n\n /**\n * The duration for the transition, in milliseconds.\n * You may specify a single timeout for all transitions, or individually with an object.\n */\n timeout: _propTypes.default.oneOfType([_propTypes.default.number, _propTypes.default.shape({\n enter: _propTypes.default.number,\n exit: _propTypes.default.number\n })])\n} : undefined;\nFade.defaultProps = {\n timeout: {\n enter: _transitions.duration.enteringScreen,\n exit: _transitions.duration.leavingScreen\n }\n};\n\nvar _default = (0, _withTheme.default)()(Fade);\n\nexports.default = _default;\n\n//# sourceURL=webpack:///./node_modules/@material-ui/core/Fade/Fade.js?"); - - /***/ }), - - /***/ "./node_modules/@material-ui/core/Fade/index.js": - /*!******************************************************!*\ - !*** ./node_modules/@material-ui/core/Fade/index.js ***! - \******************************************************/ - /*! no static exports found */ - /***/ (function(module, exports, __webpack_require__) { - - "use strict"; - eval("\n\nvar _interopRequireDefault = __webpack_require__(/*! @babel/runtime/helpers/interopRequireDefault */ \"./node_modules/@babel/runtime/helpers/interopRequireDefault.js\");\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nObject.defineProperty(exports, \"default\", {\n enumerable: true,\n get: function get() {\n return _Fade.default;\n }\n});\n\nvar _Fade = _interopRequireDefault(__webpack_require__(/*! ./Fade */ \"./node_modules/@material-ui/core/Fade/Fade.js\"));\n\n//# sourceURL=webpack:///./node_modules/@material-ui/core/Fade/index.js?"); - - /***/ }), - - /***/ "./node_modules/@material-ui/core/FilledInput/FilledInput.js": - /*!*******************************************************************!*\ - !*** ./node_modules/@material-ui/core/FilledInput/FilledInput.js ***! - \*******************************************************************/ - /*! no static exports found */ - /***/ (function(module, exports, __webpack_require__) { - - "use strict"; - eval("\n\nvar _interopRequireDefault = __webpack_require__(/*! @babel/runtime/helpers/interopRequireDefault */ \"./node_modules/@babel/runtime/helpers/interopRequireDefault.js\");\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.default = exports.styles = void 0;\n\nvar _defineProperty2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/defineProperty */ \"./node_modules/@babel/runtime/helpers/defineProperty.js\"));\n\nvar _extends2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/extends */ \"./node_modules/@babel/runtime/helpers/extends.js\"));\n\nvar _objectWithoutProperties2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/objectWithoutProperties */ \"./node_modules/@babel/runtime/helpers/objectWithoutProperties.js\"));\n\nvar _react = _interopRequireDefault(__webpack_require__(/*! react */ \"./node_modules/react/index.js\"));\n\nvar _propTypes = _interopRequireDefault(__webpack_require__(/*! prop-types */ \"./node_modules/prop-types/index.js\"));\n\nvar _classnames = _interopRequireDefault(__webpack_require__(/*! classnames */ \"./node_modules/classnames/index.js\"));\n\nvar _utils = __webpack_require__(/*! @material-ui/utils */ \"./node_modules/@material-ui/utils/index.es.js\");\n\nvar _InputBase = _interopRequireDefault(__webpack_require__(/*! ../InputBase */ \"./node_modules/@material-ui/core/InputBase/index.js\"));\n\nvar _withStyles = _interopRequireDefault(__webpack_require__(/*! ../styles/withStyles */ \"./node_modules/@material-ui/core/styles/withStyles.js\"));\n\n// @inheritedComponent InputBase\nvar styles = function styles(theme) {\n var light = theme.palette.type === 'light';\n var bottomLineColor = light ? 'rgba(0, 0, 0, 0.42)' : 'rgba(255, 255, 255, 0.7)';\n var backgroundColor = light ? 'rgba(0, 0, 0, 0.09)' : 'rgba(255, 255, 255, 0.09)';\n return {\n /* Styles applied to the root element. */\n root: {\n position: 'relative',\n backgroundColor: backgroundColor,\n borderTopLeftRadius: theme.shape.borderRadius,\n borderTopRightRadius: theme.shape.borderRadius,\n transition: theme.transitions.create('background-color', {\n duration: theme.transitions.duration.shorter,\n easing: theme.transitions.easing.easeOut\n }),\n '&:hover': {\n backgroundColor: light ? 'rgba(0, 0, 0, 0.13)' : 'rgba(255, 255, 255, 0.13)',\n // Reset on touch devices, it doesn't add specificity\n '@media (hover: none)': {\n backgroundColor: backgroundColor\n }\n },\n '&$focused': {\n backgroundColor: light ? 'rgba(0, 0, 0, 0.09)' : 'rgba(255, 255, 255, 0.09)'\n },\n '&$disabled': {\n backgroundColor: light ? 'rgba(0, 0, 0, 0.12)' : 'rgba(255, 255, 255, 0.12)'\n }\n },\n\n /* Styles applied to the root element if `disableUnderline={false}`. */\n underline: {\n '&:after': {\n borderBottom: \"2px solid \".concat(theme.palette.primary[light ? 'dark' : 'light']),\n left: 0,\n bottom: 0,\n // Doing the other way around crash on IE 11 \"''\" https://github.com/cssinjs/jss/issues/242\n content: '\"\"',\n position: 'absolute',\n right: 0,\n transform: 'scaleX(0)',\n transition: theme.transitions.create('transform', {\n duration: theme.transitions.duration.shorter,\n easing: theme.transitions.easing.easeOut\n }),\n pointerEvents: 'none' // Transparent to the hover style.\n\n },\n '&$focused:after': {\n transform: 'scaleX(1)'\n },\n '&$error:after': {\n borderBottomColor: theme.palette.error.main,\n transform: 'scaleX(1)' // error is always underlined in red\n\n },\n '&:before': {\n borderBottom: \"1px solid \".concat(bottomLineColor),\n left: 0,\n bottom: 0,\n // Doing the other way around crash on IE 11 \"''\" https://github.com/cssinjs/jss/issues/242\n content: '\"\\\\00a0\"',\n position: 'absolute',\n right: 0,\n transition: theme.transitions.create('border-bottom-color', {\n duration: theme.transitions.duration.shorter\n }),\n pointerEvents: 'none' // Transparent to the hover style.\n\n },\n '&:hover:not($disabled):not($focused):not($error):before': {\n borderBottom: \"1px solid \".concat(theme.palette.text.primary)\n },\n '&$disabled:before': {\n borderBottom: \"1px dotted \".concat(bottomLineColor)\n }\n },\n\n /* Styles applied to the root element if the component is focused. */\n focused: {},\n\n /* Styles applied to the root element if `disabled={true}`. */\n disabled: {},\n\n /* Styles applied to the root element if `startAdornment` is provided. */\n adornedStart: {\n paddingLeft: 12\n },\n\n /* Styles applied to the root element if `endAdornment` is provided. */\n adornedEnd: {\n paddingRight: 12\n },\n\n /* Styles applied to the root element if `error={true}`. */\n error: {},\n\n /* Styles applied to the root element if `multiline={true}`. */\n multiline: {\n padding: '27px 12px 10px',\n boxSizing: 'border-box' // Prevent padding issue with fullWidth.\n\n },\n\n /* Styles applied to the `input` element. */\n input: {\n padding: '27px 12px 10px'\n },\n\n /* Styles applied to the `input` element if `margin=\"dense\"`. */\n inputMarginDense: {\n paddingTop: 24,\n paddingBottom: 6\n },\n\n /* Styles applied to the `input` element if `multiline={true}`. */\n inputMultiline: {\n padding: 0\n },\n\n /* Styles applied to the `input` element if `startAdornment` is provided. */\n inputAdornedStart: {\n paddingLeft: 0\n },\n\n /* Styles applied to the `input` element if `endAdornment` is provided. */\n inputAdornedEnd: {\n paddingRight: 0\n }\n };\n};\n\nexports.styles = styles;\n\nfunction FilledInput(props) {\n var disableUnderline = props.disableUnderline,\n classes = props.classes,\n other = (0, _objectWithoutProperties2.default)(props, [\"disableUnderline\", \"classes\"]);\n return _react.default.createElement(_InputBase.default, (0, _extends2.default)({\n classes: (0, _extends2.default)({}, classes, {\n root: (0, _classnames.default)(classes.root, (0, _defineProperty2.default)({}, classes.underline, !disableUnderline)),\n underline: null\n })\n }, other));\n}\n\n true ? FilledInput.propTypes = {\n /**\n * This property helps users to fill forms faster, especially on mobile devices.\n * The name can be confusing, as it's more like an autofill.\n * You can learn more about it here:\n * https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofill\n */\n autoComplete: _propTypes.default.string,\n\n /**\n * If `true`, the input will be focused during the first mount.\n */\n autoFocus: _propTypes.default.bool,\n\n /**\n * Override or extend the styles applied to the component.\n * See [CSS API](#css-api) below for more details.\n */\n classes: _propTypes.default.object.isRequired,\n\n /**\n * The CSS class name of the wrapper element.\n */\n className: _propTypes.default.string,\n\n /**\n * The default input value, useful when not controlling the component.\n */\n defaultValue: _propTypes.default.oneOfType([_propTypes.default.string, _propTypes.default.number, _propTypes.default.bool, _propTypes.default.object, _propTypes.default.arrayOf(_propTypes.default.oneOfType([_propTypes.default.string, _propTypes.default.number, _propTypes.default.bool, _propTypes.default.object]))]),\n\n /**\n * If `true`, the input will be disabled.\n */\n disabled: _propTypes.default.bool,\n\n /**\n * If `true`, the input will not have an underline.\n */\n disableUnderline: _propTypes.default.bool,\n\n /**\n * End `InputAdornment` for this component.\n */\n endAdornment: _propTypes.default.node,\n\n /**\n * If `true`, the input will indicate an error. This is normally obtained via context from\n * FormControl.\n */\n error: _propTypes.default.bool,\n\n /**\n * If `true`, the input will take up the full width of its container.\n */\n fullWidth: _propTypes.default.bool,\n\n /**\n * The id of the `input` element.\n */\n id: _propTypes.default.string,\n\n /**\n * The component used for the native input.\n * Either a string to use a DOM element or a component.\n */\n inputComponent: _utils.componentPropType,\n\n /**\n * Attributes applied to the `input` element.\n */\n inputProps: _propTypes.default.object,\n\n /**\n * Use that property to pass a ref callback to the native input component.\n */\n inputRef: _propTypes.default.oneOfType([_propTypes.default.func, _propTypes.default.object]),\n\n /**\n * If `dense`, will adjust vertical spacing. This is normally obtained via context from\n * FormControl.\n */\n margin: _propTypes.default.oneOf(['dense', 'none']),\n\n /**\n * If `true`, a textarea element will be rendered.\n */\n multiline: _propTypes.default.bool,\n\n /**\n * Name attribute of the `input` element.\n */\n name: _propTypes.default.string,\n\n /**\n * Callback fired when the value is changed.\n *\n * @param {object} event The event source of the callback.\n * You can pull out the new value by accessing `event.target.value`.\n */\n onChange: _propTypes.default.func,\n\n /**\n * The short hint displayed in the input before the user enters a value.\n */\n placeholder: _propTypes.default.string,\n\n /**\n * It prevents the user from changing the value of the field\n * (not from interacting with the field).\n */\n readOnly: _propTypes.default.bool,\n\n /**\n * If `true`, the input will be required.\n */\n required: _propTypes.default.bool,\n\n /**\n * Number of rows to display when multiline option is set to true.\n */\n rows: _propTypes.default.oneOfType([_propTypes.default.string, _propTypes.default.number]),\n\n /**\n * Maximum number of rows to display when multiline option is set to true.\n */\n rowsMax: _propTypes.default.oneOfType([_propTypes.default.string, _propTypes.default.number]),\n\n /**\n * Start `InputAdornment` for this component.\n */\n startAdornment: _propTypes.default.node,\n\n /**\n * Type of the input element. It should be a valid HTML5 input type.\n */\n type: _propTypes.default.string,\n\n /**\n * The input value, required for a controlled component.\n */\n value: _propTypes.default.oneOfType([_propTypes.default.string, _propTypes.default.number, _propTypes.default.bool, _propTypes.default.object, _propTypes.default.arrayOf(_propTypes.default.oneOfType([_propTypes.default.string, _propTypes.default.number, _propTypes.default.bool, _propTypes.default.object]))])\n} : undefined;\n_InputBase.default.defaultProps = {\n fullWidth: false,\n inputComponent: 'input',\n multiline: false,\n type: 'text'\n};\nFilledInput.muiName = 'Input';\n\nvar _default = (0, _withStyles.default)(styles, {\n name: 'MuiFilledInput'\n})(FilledInput);\n\nexports.default = _default;\n\n//# sourceURL=webpack:///./node_modules/@material-ui/core/FilledInput/FilledInput.js?"); - - /***/ }), - - /***/ "./node_modules/@material-ui/core/FilledInput/index.js": - /*!*************************************************************!*\ - !*** ./node_modules/@material-ui/core/FilledInput/index.js ***! - \*************************************************************/ - /*! no static exports found */ - /***/ (function(module, exports, __webpack_require__) { - - "use strict"; - eval("\n\nvar _interopRequireDefault = __webpack_require__(/*! @babel/runtime/helpers/interopRequireDefault */ \"./node_modules/@babel/runtime/helpers/interopRequireDefault.js\");\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nObject.defineProperty(exports, \"default\", {\n enumerable: true,\n get: function get() {\n return _FilledInput.default;\n }\n});\n\nvar _FilledInput = _interopRequireDefault(__webpack_require__(/*! ./FilledInput */ \"./node_modules/@material-ui/core/FilledInput/FilledInput.js\"));\n\n//# sourceURL=webpack:///./node_modules/@material-ui/core/FilledInput/index.js?"); - - /***/ }), - - /***/ "./node_modules/@material-ui/core/FormControl/FormControl.js": - /*!*******************************************************************!*\ - !*** ./node_modules/@material-ui/core/FormControl/FormControl.js ***! - \*******************************************************************/ - /*! no static exports found */ - /***/ (function(module, exports, __webpack_require__) { - - "use strict"; - eval("\n\nvar _interopRequireDefault = __webpack_require__(/*! @babel/runtime/helpers/interopRequireDefault */ \"./node_modules/@babel/runtime/helpers/interopRequireDefault.js\");\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.default = exports.styles = void 0;\n\nvar _extends2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/extends */ \"./node_modules/@babel/runtime/helpers/extends.js\"));\n\nvar _defineProperty2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/defineProperty */ \"./node_modules/@babel/runtime/helpers/defineProperty.js\"));\n\nvar _objectWithoutProperties2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/objectWithoutProperties */ \"./node_modules/@babel/runtime/helpers/objectWithoutProperties.js\"));\n\nvar _classCallCheck2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/classCallCheck */ \"./node_modules/@babel/runtime/helpers/classCallCheck.js\"));\n\nvar _possibleConstructorReturn2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/possibleConstructorReturn */ \"./node_modules/@babel/runtime/helpers/possibleConstructorReturn.js\"));\n\nvar _getPrototypeOf2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/getPrototypeOf */ \"./node_modules/@babel/runtime/helpers/getPrototypeOf.js\"));\n\nvar _createClass2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/createClass */ \"./node_modules/@babel/runtime/helpers/createClass.js\"));\n\nvar _inherits2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/inherits */ \"./node_modules/@babel/runtime/helpers/inherits.js\"));\n\nvar _react = _interopRequireDefault(__webpack_require__(/*! react */ \"./node_modules/react/index.js\"));\n\nvar _propTypes = _interopRequireDefault(__webpack_require__(/*! prop-types */ \"./node_modules/prop-types/index.js\"));\n\nvar _classnames = _interopRequireDefault(__webpack_require__(/*! classnames */ \"./node_modules/classnames/index.js\"));\n\nvar _utils = __webpack_require__(/*! @material-ui/utils */ \"./node_modules/@material-ui/utils/index.es.js\");\n\nvar _utils2 = __webpack_require__(/*! ../InputBase/utils */ \"./node_modules/@material-ui/core/InputBase/utils.js\");\n\nvar _withStyles = _interopRequireDefault(__webpack_require__(/*! ../styles/withStyles */ \"./node_modules/@material-ui/core/styles/withStyles.js\"));\n\nvar _helpers = __webpack_require__(/*! ../utils/helpers */ \"./node_modules/@material-ui/core/utils/helpers.js\");\n\nvar _reactHelpers = __webpack_require__(/*! ../utils/reactHelpers */ \"./node_modules/@material-ui/core/utils/reactHelpers.js\");\n\nvar _FormControlContext = _interopRequireDefault(__webpack_require__(/*! ./FormControlContext */ \"./node_modules/@material-ui/core/FormControl/FormControlContext.js\"));\n\nvar styles = {\n /* Styles applied to the root element. */\n root: {\n display: 'inline-flex',\n flexDirection: 'column',\n position: 'relative',\n // Reset fieldset default style.\n minWidth: 0,\n padding: 0,\n margin: 0,\n border: 0,\n verticalAlign: 'top' // Fix alignment issue on Safari.\n\n },\n\n /* Styles applied to the root element if `margin=\"normal\"`. */\n marginNormal: {\n marginTop: 16,\n marginBottom: 8\n },\n\n /* Styles applied to the root element if `margin=\"dense\"`. */\n marginDense: {\n marginTop: 8,\n marginBottom: 4\n },\n\n /* Styles applied to the root element if `fullWidth={true}`. */\n fullWidth: {\n width: '100%'\n }\n};\n/**\n * Provides context such as filled/focused/error/required for form inputs.\n * Relying on the context provides high flexibility and ensures that the state always stays\n * consistent across the children of the `FormControl`.\n * This context is used by the following components:\n * - FormLabel\n * - FormHelperText\n * - Input\n * - InputLabel\n *\n * ⚠️ Only one input can be used within a FormControl.\n */\n\nexports.styles = styles;\n\nvar FormControl =\n/*#__PURE__*/\nfunction (_React$Component) {\n (0, _inherits2.default)(FormControl, _React$Component);\n (0, _createClass2.default)(FormControl, null, [{\n key: \"getDerivedStateFromProps\",\n value: function getDerivedStateFromProps(props, state) {\n if (props.disabled && state.focused) {\n return {\n focused: false\n };\n }\n\n return null;\n }\n }]);\n\n function FormControl(props) {\n var _this;\n\n (0, _classCallCheck2.default)(this, FormControl);\n _this = (0, _possibleConstructorReturn2.default)(this, (0, _getPrototypeOf2.default)(FormControl).call(this));\n\n _this.handleFocus = function () {\n _this.setState(function (state) {\n return !state.focused ? {\n focused: true\n } : null;\n });\n };\n\n _this.handleBlur = function () {\n _this.setState(function (state) {\n return state.focused ? {\n focused: false\n } : null;\n });\n };\n\n _this.handleDirty = function () {\n if (!_this.state.filled) {\n _this.setState({\n filled: true\n });\n }\n };\n\n _this.handleClean = function () {\n if (_this.state.filled) {\n _this.setState({\n filled: false\n });\n }\n };\n\n _this.state = {\n adornedStart: false,\n filled: false,\n focused: false\n }; // We need to iterate through the children and find the Input in order\n // to fully support server-side rendering.\n\n var children = props.children;\n\n if (children) {\n _react.default.Children.forEach(children, function (child) {\n if (!(0, _reactHelpers.isMuiElement)(child, ['Input', 'Select'])) {\n return;\n }\n\n if ((0, _utils2.isFilled)(child.props, true)) {\n _this.state.filled = true;\n }\n\n var input = (0, _reactHelpers.isMuiElement)(child, ['Select']) ? child.props.input : child;\n\n if (input && (0, _utils2.isAdornedStart)(input.props)) {\n _this.state.adornedStart = true;\n }\n });\n }\n\n return _this;\n }\n\n (0, _createClass2.default)(FormControl, [{\n key: \"render\",\n value: function render() {\n var _classNames;\n\n var _this$props = this.props,\n classes = _this$props.classes,\n className = _this$props.className,\n Component = _this$props.component,\n disabled = _this$props.disabled,\n error = _this$props.error,\n fullWidth = _this$props.fullWidth,\n margin = _this$props.margin,\n required = _this$props.required,\n variant = _this$props.variant,\n other = (0, _objectWithoutProperties2.default)(_this$props, [\"classes\", \"className\", \"component\", \"disabled\", \"error\", \"fullWidth\", \"margin\", \"required\", \"variant\"]);\n var _this$state = this.state,\n adornedStart = _this$state.adornedStart,\n filled = _this$state.filled,\n focused = _this$state.focused;\n var childContext = {\n adornedStart: adornedStart,\n disabled: disabled,\n error: error,\n filled: filled,\n focused: focused,\n margin: margin,\n onBlur: this.handleBlur,\n onEmpty: this.handleClean,\n onFilled: this.handleDirty,\n onFocus: this.handleFocus,\n required: required,\n variant: variant\n };\n return _react.default.createElement(_FormControlContext.default.Provider, {\n value: childContext\n }, _react.default.createElement(Component, (0, _extends2.default)({\n className: (0, _classnames.default)(classes.root, (_classNames = {}, (0, _defineProperty2.default)(_classNames, classes[\"margin\".concat((0, _helpers.capitalize)(margin))], margin !== 'none'), (0, _defineProperty2.default)(_classNames, classes.fullWidth, fullWidth), _classNames), className)\n }, other)));\n }\n }]);\n return FormControl;\n}(_react.default.Component);\n\n true ? FormControl.propTypes = {\n /**\n * The contents of the form control.\n */\n children: _propTypes.default.node,\n\n /**\n * Override or extend the styles applied to the component.\n * See [CSS API](#css-api) below for more details.\n */\n classes: _propTypes.default.object.isRequired,\n\n /**\n * @ignore\n */\n className: _propTypes.default.string,\n\n /**\n * The component used for the root node.\n * Either a string to use a DOM element or a component.\n */\n component: _utils.componentPropType,\n\n /**\n * If `true`, the label, input and helper text should be displayed in a disabled state.\n */\n disabled: _propTypes.default.bool,\n\n /**\n * If `true`, the label should be displayed in an error state.\n */\n error: _propTypes.default.bool,\n\n /**\n * If `true`, the component will take up the full width of its container.\n */\n fullWidth: _propTypes.default.bool,\n\n /**\n * If `dense` or `normal`, will adjust vertical spacing of this and contained components.\n */\n margin: _propTypes.default.oneOf(['none', 'dense', 'normal']),\n\n /**\n * If `true`, the label will indicate that the input is required.\n */\n required: _propTypes.default.bool,\n\n /**\n * The variant to use.\n */\n variant: _propTypes.default.oneOf(['standard', 'outlined', 'filled'])\n} : undefined;\nFormControl.defaultProps = {\n component: 'div',\n disabled: false,\n error: false,\n fullWidth: false,\n margin: 'none',\n required: false,\n variant: 'standard'\n};\n\nvar _default = (0, _withStyles.default)(styles, {\n name: 'MuiFormControl'\n})(FormControl);\n\nexports.default = _default;\n\n//# sourceURL=webpack:///./node_modules/@material-ui/core/FormControl/FormControl.js?"); - - /***/ }), - - /***/ "./node_modules/@material-ui/core/FormControl/FormControlContext.js": - /*!**************************************************************************!*\ - !*** ./node_modules/@material-ui/core/FormControl/FormControlContext.js ***! - \**************************************************************************/ - /*! no static exports found */ - /***/ (function(module, exports, __webpack_require__) { - - "use strict"; - eval("\n\nvar _interopRequireDefault = __webpack_require__(/*! @babel/runtime/helpers/interopRequireDefault */ \"./node_modules/@babel/runtime/helpers/interopRequireDefault.js\");\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.default = void 0;\n\nvar _react = _interopRequireDefault(__webpack_require__(/*! react */ \"./node_modules/react/index.js\"));\n\n/**\n * @ignore - internal component.\n */\nvar FormControlContext = _react.default.createContext();\n\nvar _default = FormControlContext;\nexports.default = _default;\n\n//# sourceURL=webpack:///./node_modules/@material-ui/core/FormControl/FormControlContext.js?"); - - /***/ }), - - /***/ "./node_modules/@material-ui/core/FormControl/formControlState.js": - /*!************************************************************************!*\ - !*** ./node_modules/@material-ui/core/FormControl/formControlState.js ***! - \************************************************************************/ - /*! no static exports found */ - /***/ (function(module, exports, __webpack_require__) { - - "use strict"; - eval("\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.default = formControlState;\n\nfunction formControlState(_ref) {\n var props = _ref.props,\n states = _ref.states,\n muiFormControl = _ref.muiFormControl;\n return states.reduce(function (acc, state) {\n acc[state] = props[state];\n\n if (muiFormControl) {\n if (typeof props[state] === 'undefined') {\n acc[state] = muiFormControl[state];\n }\n }\n\n return acc;\n }, {});\n}\n\n//# sourceURL=webpack:///./node_modules/@material-ui/core/FormControl/formControlState.js?"); - - /***/ }), - - /***/ "./node_modules/@material-ui/core/FormControl/index.js": - /*!*************************************************************!*\ - !*** ./node_modules/@material-ui/core/FormControl/index.js ***! - \*************************************************************/ - /*! no static exports found */ - /***/ (function(module, exports, __webpack_require__) { - - "use strict"; - eval("\n\nvar _interopRequireDefault = __webpack_require__(/*! @babel/runtime/helpers/interopRequireDefault */ \"./node_modules/@babel/runtime/helpers/interopRequireDefault.js\");\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nObject.defineProperty(exports, \"default\", {\n enumerable: true,\n get: function get() {\n return _FormControl.default;\n }\n});\n\nvar _FormControl = _interopRequireDefault(__webpack_require__(/*! ./FormControl */ \"./node_modules/@material-ui/core/FormControl/FormControl.js\"));\n\n//# sourceURL=webpack:///./node_modules/@material-ui/core/FormControl/index.js?"); - - /***/ }), - - /***/ "./node_modules/@material-ui/core/FormControl/withFormControlContext.js": - /*!******************************************************************************!*\ - !*** ./node_modules/@material-ui/core/FormControl/withFormControlContext.js ***! - \******************************************************************************/ - /*! no static exports found */ - /***/ (function(module, exports, __webpack_require__) { - - "use strict"; - eval("\n\nvar _interopRequireDefault = __webpack_require__(/*! @babel/runtime/helpers/interopRequireDefault */ \"./node_modules/@babel/runtime/helpers/interopRequireDefault.js\");\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.default = withFormControlContext;\n\nvar _extends2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/extends */ \"./node_modules/@babel/runtime/helpers/extends.js\"));\n\nvar _react = _interopRequireDefault(__webpack_require__(/*! react */ \"./node_modules/react/index.js\"));\n\nvar _hoistNonReactStatics = _interopRequireDefault(__webpack_require__(/*! hoist-non-react-statics */ \"./node_modules/hoist-non-react-statics/dist/hoist-non-react-statics.cjs.js\"));\n\nvar _FormControlContext = _interopRequireDefault(__webpack_require__(/*! ./FormControlContext */ \"./node_modules/@material-ui/core/FormControl/FormControlContext.js\"));\n\nvar _utils = __webpack_require__(/*! @material-ui/utils */ \"./node_modules/@material-ui/utils/index.es.js\");\n\nfunction withFormControlContext(Component) {\n var EnhancedComponent = function EnhancedComponent(props) {\n return _react.default.createElement(_FormControlContext.default.Consumer, null, function (context) {\n return _react.default.createElement(Component, (0, _extends2.default)({\n muiFormControl: context\n }, props));\n });\n };\n\n if (true) {\n EnhancedComponent.displayName = \"WithFormControlContext(\".concat((0, _utils.getDisplayName)(Component), \")\");\n }\n\n (0, _hoistNonReactStatics.default)(EnhancedComponent, Component);\n return EnhancedComponent;\n}\n\n//# sourceURL=webpack:///./node_modules/@material-ui/core/FormControl/withFormControlContext.js?"); - - /***/ }), - - /***/ "./node_modules/@material-ui/core/FormControlLabel/FormControlLabel.js": - /*!*****************************************************************************!*\ - !*** ./node_modules/@material-ui/core/FormControlLabel/FormControlLabel.js ***! - \*****************************************************************************/ - /*! no static exports found */ - /***/ (function(module, exports, __webpack_require__) { - - "use strict"; - eval("\n\nvar _interopRequireDefault = __webpack_require__(/*! @babel/runtime/helpers/interopRequireDefault */ \"./node_modules/@babel/runtime/helpers/interopRequireDefault.js\");\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.default = exports.styles = void 0;\n\nvar _extends2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/extends */ \"./node_modules/@babel/runtime/helpers/extends.js\"));\n\nvar _defineProperty2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/defineProperty */ \"./node_modules/@babel/runtime/helpers/defineProperty.js\"));\n\nvar _objectWithoutProperties2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/objectWithoutProperties */ \"./node_modules/@babel/runtime/helpers/objectWithoutProperties.js\"));\n\nvar _react = _interopRequireDefault(__webpack_require__(/*! react */ \"./node_modules/react/index.js\"));\n\nvar _propTypes = _interopRequireDefault(__webpack_require__(/*! prop-types */ \"./node_modules/prop-types/index.js\"));\n\nvar _classnames = _interopRequireDefault(__webpack_require__(/*! classnames */ \"./node_modules/classnames/index.js\"));\n\nvar _withFormControlContext = _interopRequireDefault(__webpack_require__(/*! ../FormControl/withFormControlContext */ \"./node_modules/@material-ui/core/FormControl/withFormControlContext.js\"));\n\nvar _withStyles = _interopRequireDefault(__webpack_require__(/*! ../styles/withStyles */ \"./node_modules/@material-ui/core/styles/withStyles.js\"));\n\nvar _Typography = _interopRequireDefault(__webpack_require__(/*! ../Typography */ \"./node_modules/@material-ui/core/Typography/index.js\"));\n\nvar _helpers = __webpack_require__(/*! ../utils/helpers */ \"./node_modules/@material-ui/core/utils/helpers.js\");\n\nvar styles = function styles(theme) {\n return {\n /* Styles applied to the root element. */\n root: {\n display: 'inline-flex',\n alignItems: 'center',\n cursor: 'pointer',\n // For correct alignment with the text.\n verticalAlign: 'middle',\n // Remove grey highlight\n WebkitTapHighlightColor: 'transparent',\n marginLeft: -14,\n marginRight: 16,\n // used for row presentation of radio/checkbox\n '&$disabled': {\n cursor: 'default'\n }\n },\n\n /* Styles applied to the root element if `labelPlacement=\"start\"`. */\n labelPlacementStart: {\n flexDirection: 'row-reverse',\n marginLeft: 16,\n // used for row presentation of radio/checkbox\n marginRight: -14\n },\n\n /* Styles applied to the root element if `labelPlacement=\"top\"`. */\n labelPlacementTop: {\n flexDirection: 'column-reverse',\n marginLeft: 16\n },\n\n /* Styles applied to the root element if `labelPlacement=\"bottom\"`. */\n labelPlacementBottom: {\n flexDirection: 'column',\n marginLeft: 16\n },\n\n /* Styles applied to the root element if `disabled={true}`. */\n disabled: {},\n\n /* Styles applied to the label's Typography component. */\n label: {\n '&$disabled': {\n color: theme.palette.text.disabled\n }\n }\n };\n};\n/**\n * Drop in replacement of the `Radio`, `Switch` and `Checkbox` component.\n * Use this component if you want to display an extra label.\n */\n\n\nexports.styles = styles;\n\nfunction FormControlLabel(props) {\n var _classNames;\n\n var checked = props.checked,\n classes = props.classes,\n classNameProp = props.className,\n control = props.control,\n disabledProp = props.disabled,\n inputRef = props.inputRef,\n label = props.label,\n labelPlacement = props.labelPlacement,\n muiFormControl = props.muiFormControl,\n name = props.name,\n onChange = props.onChange,\n value = props.value,\n other = (0, _objectWithoutProperties2.default)(props, [\"checked\", \"classes\", \"className\", \"control\", \"disabled\", \"inputRef\", \"label\", \"labelPlacement\", \"muiFormControl\", \"name\", \"onChange\", \"value\"]);\n var disabled = disabledProp;\n\n if (typeof disabled === 'undefined' && typeof control.props.disabled !== 'undefined') {\n disabled = control.props.disabled;\n }\n\n if (typeof disabled === 'undefined' && muiFormControl) {\n disabled = muiFormControl.disabled;\n }\n\n var controlProps = {\n disabled: disabled\n };\n ['checked', 'name', 'onChange', 'value', 'inputRef'].forEach(function (key) {\n if (typeof control.props[key] === 'undefined' && typeof props[key] !== 'undefined') {\n controlProps[key] = props[key];\n }\n });\n return _react.default.createElement(\"label\", (0, _extends2.default)({\n className: (0, _classnames.default)(classes.root, (_classNames = {}, (0, _defineProperty2.default)(_classNames, classes[\"labelPlacement\".concat((0, _helpers.capitalize)(labelPlacement))], labelPlacement !== 'end'), (0, _defineProperty2.default)(_classNames, classes.disabled, disabled), _classNames), classNameProp)\n }, other), _react.default.cloneElement(control, controlProps), _react.default.createElement(_Typography.default, {\n component: \"span\",\n className: (0, _classnames.default)(classes.label, (0, _defineProperty2.default)({}, classes.disabled, disabled))\n }, label));\n}\n\n true ? FormControlLabel.propTypes = {\n /**\n * If `true`, the component appears selected.\n */\n checked: _propTypes.default.oneOfType([_propTypes.default.bool, _propTypes.default.string]),\n\n /**\n * Override or extend the styles applied to the component.\n * See [CSS API](#css-api) below for more details.\n */\n classes: _propTypes.default.object.isRequired,\n\n /**\n * @ignore\n */\n className: _propTypes.default.string,\n\n /**\n * A control element. For instance, it can be be a `Radio`, a `Switch` or a `Checkbox`.\n */\n control: _propTypes.default.element,\n\n /**\n * If `true`, the control will be disabled.\n */\n disabled: _propTypes.default.bool,\n\n /**\n * Use that property to pass a ref callback to the native input component.\n */\n inputRef: _propTypes.default.oneOfType([_propTypes.default.func, _propTypes.default.object]),\n\n /**\n * The text to be used in an enclosing label element.\n */\n label: _propTypes.default.node,\n\n /**\n * The position of the label.\n */\n labelPlacement: _propTypes.default.oneOf(['end', 'start', 'top', 'bottom']),\n\n /**\n * @ignore\n */\n muiFormControl: _propTypes.default.object,\n\n /*\n * @ignore\n */\n name: _propTypes.default.string,\n\n /**\n * Callback fired when the state is changed.\n *\n * @param {object} event The event source of the callback.\n * You can pull out the new value by accessing `event.target.checked`.\n * @param {boolean} checked The `checked` value of the switch\n */\n onChange: _propTypes.default.func,\n\n /**\n * The value of the component.\n */\n value: _propTypes.default.string\n} : undefined;\nFormControlLabel.defaultProps = {\n labelPlacement: 'end'\n};\n\nvar _default = (0, _withStyles.default)(styles, {\n name: 'MuiFormControlLabel'\n})((0, _withFormControlContext.default)(FormControlLabel));\n\nexports.default = _default;\n\n//# sourceURL=webpack:///./node_modules/@material-ui/core/FormControlLabel/FormControlLabel.js?"); - - /***/ }), - - /***/ "./node_modules/@material-ui/core/FormControlLabel/index.js": - /*!******************************************************************!*\ - !*** ./node_modules/@material-ui/core/FormControlLabel/index.js ***! - \******************************************************************/ - /*! no static exports found */ - /***/ (function(module, exports, __webpack_require__) { - - "use strict"; - eval("\n\nvar _interopRequireDefault = __webpack_require__(/*! @babel/runtime/helpers/interopRequireDefault */ \"./node_modules/@babel/runtime/helpers/interopRequireDefault.js\");\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nObject.defineProperty(exports, \"default\", {\n enumerable: true,\n get: function get() {\n return _FormControlLabel.default;\n }\n});\n\nvar _FormControlLabel = _interopRequireDefault(__webpack_require__(/*! ./FormControlLabel */ \"./node_modules/@material-ui/core/FormControlLabel/FormControlLabel.js\"));\n\n//# sourceURL=webpack:///./node_modules/@material-ui/core/FormControlLabel/index.js?"); - - /***/ }), - - /***/ "./node_modules/@material-ui/core/FormGroup/FormGroup.js": - /*!***************************************************************!*\ - !*** ./node_modules/@material-ui/core/FormGroup/FormGroup.js ***! - \***************************************************************/ - /*! no static exports found */ - /***/ (function(module, exports, __webpack_require__) { - - "use strict"; - eval("\n\nvar _interopRequireDefault = __webpack_require__(/*! @babel/runtime/helpers/interopRequireDefault */ \"./node_modules/@babel/runtime/helpers/interopRequireDefault.js\");\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.default = exports.styles = void 0;\n\nvar _extends2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/extends */ \"./node_modules/@babel/runtime/helpers/extends.js\"));\n\nvar _defineProperty2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/defineProperty */ \"./node_modules/@babel/runtime/helpers/defineProperty.js\"));\n\nvar _objectWithoutProperties2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/objectWithoutProperties */ \"./node_modules/@babel/runtime/helpers/objectWithoutProperties.js\"));\n\nvar _react = _interopRequireDefault(__webpack_require__(/*! react */ \"./node_modules/react/index.js\"));\n\nvar _propTypes = _interopRequireDefault(__webpack_require__(/*! prop-types */ \"./node_modules/prop-types/index.js\"));\n\nvar _classnames = _interopRequireDefault(__webpack_require__(/*! classnames */ \"./node_modules/classnames/index.js\"));\n\nvar _withStyles = _interopRequireDefault(__webpack_require__(/*! ../styles/withStyles */ \"./node_modules/@material-ui/core/styles/withStyles.js\"));\n\nvar styles = {\n /* Styles applied to the root element. */\n root: {\n display: 'flex',\n flexDirection: 'column',\n flexWrap: 'wrap'\n },\n\n /* Styles applied to the root element if `row={true}`. */\n row: {\n flexDirection: 'row'\n }\n};\n/**\n * `FormGroup` wraps controls such as `Checkbox` and `Switch`.\n * It provides compact row layout.\n * For the `Radio`, you should be using the `RadioGroup` component instead of this one.\n */\n\nexports.styles = styles;\n\nfunction FormGroup(props) {\n var classes = props.classes,\n className = props.className,\n children = props.children,\n row = props.row,\n other = (0, _objectWithoutProperties2.default)(props, [\"classes\", \"className\", \"children\", \"row\"]);\n return _react.default.createElement(\"div\", (0, _extends2.default)({\n className: (0, _classnames.default)(classes.root, (0, _defineProperty2.default)({}, classes.row, row), className)\n }, other), children);\n}\n\n true ? FormGroup.propTypes = {\n /**\n * The content of the component.\n */\n children: _propTypes.default.node,\n\n /**\n * Override or extend the styles applied to the component.\n * See [CSS API](#css-api) below for more details.\n */\n classes: _propTypes.default.object.isRequired,\n\n /**\n * @ignore\n */\n className: _propTypes.default.string,\n\n /**\n * Display group of elements in a compact row.\n */\n row: _propTypes.default.bool\n} : undefined;\nFormGroup.defaultProps = {\n row: false\n};\n\nvar _default = (0, _withStyles.default)(styles, {\n name: 'MuiFormGroup'\n})(FormGroup);\n\nexports.default = _default;\n\n//# sourceURL=webpack:///./node_modules/@material-ui/core/FormGroup/FormGroup.js?"); - - /***/ }), - - /***/ "./node_modules/@material-ui/core/FormGroup/index.js": - /*!***********************************************************!*\ - !*** ./node_modules/@material-ui/core/FormGroup/index.js ***! - \***********************************************************/ - /*! no static exports found */ - /***/ (function(module, exports, __webpack_require__) { - - "use strict"; - eval("\n\nvar _interopRequireDefault = __webpack_require__(/*! @babel/runtime/helpers/interopRequireDefault */ \"./node_modules/@babel/runtime/helpers/interopRequireDefault.js\");\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nObject.defineProperty(exports, \"default\", {\n enumerable: true,\n get: function get() {\n return _FormGroup.default;\n }\n});\n\nvar _FormGroup = _interopRequireDefault(__webpack_require__(/*! ./FormGroup */ \"./node_modules/@material-ui/core/FormGroup/FormGroup.js\"));\n\n//# sourceURL=webpack:///./node_modules/@material-ui/core/FormGroup/index.js?"); - - /***/ }), - - /***/ "./node_modules/@material-ui/core/FormHelperText/FormHelperText.js": - /*!*************************************************************************!*\ - !*** ./node_modules/@material-ui/core/FormHelperText/FormHelperText.js ***! - \*************************************************************************/ - /*! no static exports found */ - /***/ (function(module, exports, __webpack_require__) { - - "use strict"; - eval("\n\nvar _interopRequireDefault = __webpack_require__(/*! @babel/runtime/helpers/interopRequireDefault */ \"./node_modules/@babel/runtime/helpers/interopRequireDefault.js\");\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.default = exports.styles = void 0;\n\nvar _extends2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/extends */ \"./node_modules/@babel/runtime/helpers/extends.js\"));\n\nvar _defineProperty2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/defineProperty */ \"./node_modules/@babel/runtime/helpers/defineProperty.js\"));\n\nvar _objectWithoutProperties2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/objectWithoutProperties */ \"./node_modules/@babel/runtime/helpers/objectWithoutProperties.js\"));\n\nvar _react = _interopRequireDefault(__webpack_require__(/*! react */ \"./node_modules/react/index.js\"));\n\nvar _propTypes = _interopRequireDefault(__webpack_require__(/*! prop-types */ \"./node_modules/prop-types/index.js\"));\n\nvar _classnames = _interopRequireDefault(__webpack_require__(/*! classnames */ \"./node_modules/classnames/index.js\"));\n\nvar _utils = __webpack_require__(/*! @material-ui/utils */ \"./node_modules/@material-ui/utils/index.es.js\");\n\nvar _formControlState = _interopRequireDefault(__webpack_require__(/*! ../FormControl/formControlState */ \"./node_modules/@material-ui/core/FormControl/formControlState.js\"));\n\nvar _withFormControlContext = _interopRequireDefault(__webpack_require__(/*! ../FormControl/withFormControlContext */ \"./node_modules/@material-ui/core/FormControl/withFormControlContext.js\"));\n\nvar _withStyles = _interopRequireDefault(__webpack_require__(/*! ../styles/withStyles */ \"./node_modules/@material-ui/core/styles/withStyles.js\"));\n\nvar styles = function styles(theme) {\n return {\n /* Styles applied to the root element. */\n root: {\n color: theme.palette.text.secondary,\n fontFamily: theme.typography.fontFamily,\n fontSize: theme.typography.pxToRem(12),\n textAlign: 'left',\n marginTop: 8,\n lineHeight: '1em',\n minHeight: '1em',\n margin: 0,\n '&$disabled': {\n color: theme.palette.text.disabled\n },\n '&$error': {\n color: theme.palette.error.main\n }\n },\n\n /* Styles applied to the root element if `error={true}`. */\n error: {},\n\n /* Styles applied to the root element if `disabled={true}`. */\n disabled: {},\n\n /* Styles applied to the root element if `margin=\"dense\"`. */\n marginDense: {\n marginTop: 4\n },\n\n /* Styles applied to the root element if `variant=\"filled\"` or `variant=\"outlined\"`. */\n contained: {\n margin: '8px 12px 0'\n },\n\n /* Styles applied to the root element if `focused={true}`. */\n focused: {},\n\n /* Styles applied to the root element if `filled={true}`. */\n filled: {},\n\n /* Styles applied to the root element if `required={true}`. */\n required: {}\n };\n};\n\nexports.styles = styles;\n\nfunction FormHelperText(props) {\n var _classNames;\n\n var classes = props.classes,\n classNameProp = props.className,\n Component = props.component,\n disabled = props.disabled,\n error = props.error,\n filled = props.filled,\n focused = props.focused,\n margin = props.margin,\n muiFormControl = props.muiFormControl,\n required = props.required,\n variant = props.variant,\n other = (0, _objectWithoutProperties2.default)(props, [\"classes\", \"className\", \"component\", \"disabled\", \"error\", \"filled\", \"focused\", \"margin\", \"muiFormControl\", \"required\", \"variant\"]);\n var fcs = (0, _formControlState.default)({\n props: props,\n muiFormControl: muiFormControl,\n states: ['variant', 'margin', 'disabled', 'error', 'filled', 'focused', 'required']\n });\n return _react.default.createElement(Component, (0, _extends2.default)({\n className: (0, _classnames.default)(classes.root, (_classNames = {}, (0, _defineProperty2.default)(_classNames, classes.contained, fcs.variant === 'filled' || fcs.variant === 'outlined'), (0, _defineProperty2.default)(_classNames, classes.marginDense, fcs.margin === 'dense'), (0, _defineProperty2.default)(_classNames, classes.disabled, fcs.disabled), (0, _defineProperty2.default)(_classNames, classes.error, fcs.error), (0, _defineProperty2.default)(_classNames, classes.filled, fcs.filled), (0, _defineProperty2.default)(_classNames, classes.focused, fcs.focused), (0, _defineProperty2.default)(_classNames, classes.required, fcs.required), _classNames), classNameProp)\n }, other));\n}\n\n true ? FormHelperText.propTypes = {\n /**\n * The content of the component.\n */\n children: _propTypes.default.node,\n\n /**\n * Override or extend the styles applied to the component.\n * See [CSS API](#css-api) below for more details.\n */\n classes: _propTypes.default.object.isRequired,\n\n /**\n * @ignore\n */\n className: _propTypes.default.string,\n\n /**\n * The component used for the root node.\n * Either a string to use a DOM element or a component.\n */\n component: _utils.componentPropType,\n\n /**\n * If `true`, the helper text should be displayed in a disabled state.\n */\n disabled: _propTypes.default.bool,\n\n /**\n * If `true`, helper text should be displayed in an error state.\n */\n error: _propTypes.default.bool,\n\n /**\n * If `true`, the helper text should use filled classes key.\n */\n filled: _propTypes.default.bool,\n\n /**\n * If `true`, the helper text should use focused classes key.\n */\n focused: _propTypes.default.bool,\n\n /**\n * If `dense`, will adjust vertical spacing. This is normally obtained via context from\n * FormControl.\n */\n margin: _propTypes.default.oneOf(['dense']),\n\n /**\n * @ignore\n */\n muiFormControl: _propTypes.default.object,\n\n /**\n * If `true`, the helper text should use required classes key.\n */\n required: _propTypes.default.bool,\n\n /**\n * The variant to use.\n */\n variant: _propTypes.default.oneOf(['standard', 'outlined', 'filled'])\n} : undefined;\nFormHelperText.defaultProps = {\n component: 'p'\n};\n\nvar _default = (0, _withStyles.default)(styles, {\n name: 'MuiFormHelperText'\n})((0, _withFormControlContext.default)(FormHelperText));\n\nexports.default = _default;\n\n//# sourceURL=webpack:///./node_modules/@material-ui/core/FormHelperText/FormHelperText.js?"); - - /***/ }), - - /***/ "./node_modules/@material-ui/core/FormHelperText/index.js": - /*!****************************************************************!*\ - !*** ./node_modules/@material-ui/core/FormHelperText/index.js ***! - \****************************************************************/ - /*! no static exports found */ - /***/ (function(module, exports, __webpack_require__) { - - "use strict"; - eval("\n\nvar _interopRequireDefault = __webpack_require__(/*! @babel/runtime/helpers/interopRequireDefault */ \"./node_modules/@babel/runtime/helpers/interopRequireDefault.js\");\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nObject.defineProperty(exports, \"default\", {\n enumerable: true,\n get: function get() {\n return _FormHelperText.default;\n }\n});\n\nvar _FormHelperText = _interopRequireDefault(__webpack_require__(/*! ./FormHelperText */ \"./node_modules/@material-ui/core/FormHelperText/FormHelperText.js\"));\n\n//# sourceURL=webpack:///./node_modules/@material-ui/core/FormHelperText/index.js?"); - - /***/ }), - - /***/ "./node_modules/@material-ui/core/FormLabel/FormLabel.js": - /*!***************************************************************!*\ - !*** ./node_modules/@material-ui/core/FormLabel/FormLabel.js ***! - \***************************************************************/ - /*! no static exports found */ - /***/ (function(module, exports, __webpack_require__) { - - "use strict"; - eval("\n\nvar _interopRequireDefault = __webpack_require__(/*! @babel/runtime/helpers/interopRequireDefault */ \"./node_modules/@babel/runtime/helpers/interopRequireDefault.js\");\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.default = exports.styles = void 0;\n\nvar _extends2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/extends */ \"./node_modules/@babel/runtime/helpers/extends.js\"));\n\nvar _defineProperty2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/defineProperty */ \"./node_modules/@babel/runtime/helpers/defineProperty.js\"));\n\nvar _objectWithoutProperties2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/objectWithoutProperties */ \"./node_modules/@babel/runtime/helpers/objectWithoutProperties.js\"));\n\nvar _react = _interopRequireDefault(__webpack_require__(/*! react */ \"./node_modules/react/index.js\"));\n\nvar _propTypes = _interopRequireDefault(__webpack_require__(/*! prop-types */ \"./node_modules/prop-types/index.js\"));\n\nvar _classnames = _interopRequireDefault(__webpack_require__(/*! classnames */ \"./node_modules/classnames/index.js\"));\n\nvar _utils = __webpack_require__(/*! @material-ui/utils */ \"./node_modules/@material-ui/utils/index.es.js\");\n\nvar _formControlState = _interopRequireDefault(__webpack_require__(/*! ../FormControl/formControlState */ \"./node_modules/@material-ui/core/FormControl/formControlState.js\"));\n\nvar _withFormControlContext = _interopRequireDefault(__webpack_require__(/*! ../FormControl/withFormControlContext */ \"./node_modules/@material-ui/core/FormControl/withFormControlContext.js\"));\n\nvar _withStyles = _interopRequireDefault(__webpack_require__(/*! ../styles/withStyles */ \"./node_modules/@material-ui/core/styles/withStyles.js\"));\n\nvar styles = function styles(theme) {\n return {\n /* Styles applied to the root element. */\n root: {\n fontFamily: theme.typography.fontFamily,\n color: theme.palette.text.secondary,\n fontSize: theme.typography.pxToRem(16),\n lineHeight: 1,\n padding: 0,\n '&$focused': {\n color: theme.palette.primary[theme.palette.type === 'light' ? 'dark' : 'light']\n },\n '&$disabled': {\n color: theme.palette.text.disabled\n },\n '&$error': {\n color: theme.palette.error.main\n }\n },\n\n /* Styles applied to the root element if `focused={true}`. */\n focused: {},\n\n /* Styles applied to the root element if `disabled={true}`. */\n disabled: {},\n\n /* Styles applied to the root element if `error={true}`. */\n error: {},\n\n /* Styles applied to the root element if `filled={true}`. */\n filled: {},\n\n /* Styles applied to the root element if `required={true}`. */\n required: {},\n asterisk: {\n '&$error': {\n color: theme.palette.error.main\n }\n }\n };\n};\n\nexports.styles = styles;\n\nfunction FormLabel(props) {\n var _classNames;\n\n var children = props.children,\n classes = props.classes,\n classNameProp = props.className,\n Component = props.component,\n disabled = props.disabled,\n error = props.error,\n filled = props.filled,\n focused = props.focused,\n muiFormControl = props.muiFormControl,\n required = props.required,\n other = (0, _objectWithoutProperties2.default)(props, [\"children\", \"classes\", \"className\", \"component\", \"disabled\", \"error\", \"filled\", \"focused\", \"muiFormControl\", \"required\"]);\n var fcs = (0, _formControlState.default)({\n props: props,\n muiFormControl: muiFormControl,\n states: ['required', 'focused', 'disabled', 'error', 'filled']\n });\n return _react.default.createElement(Component, (0, _extends2.default)({\n className: (0, _classnames.default)(classes.root, (_classNames = {}, (0, _defineProperty2.default)(_classNames, classes.disabled, fcs.disabled), (0, _defineProperty2.default)(_classNames, classes.error, fcs.error), (0, _defineProperty2.default)(_classNames, classes.filled, fcs.filled), (0, _defineProperty2.default)(_classNames, classes.focused, fcs.focused), (0, _defineProperty2.default)(_classNames, classes.required, fcs.required), _classNames), classNameProp)\n }, other), children, fcs.required && _react.default.createElement(\"span\", {\n className: (0, _classnames.default)(classes.asterisk, (0, _defineProperty2.default)({}, classes.error, fcs.error))\n }, \"\\u2009*\"));\n}\n\n true ? FormLabel.propTypes = {\n /**\n * The content of the component.\n */\n children: _propTypes.default.node,\n\n /**\n * Override or extend the styles applied to the component.\n * See [CSS API](#css-api) below for more details.\n */\n classes: _propTypes.default.object.isRequired,\n\n /**\n * @ignore\n */\n className: _propTypes.default.string,\n\n /**\n * The component used for the root node.\n * Either a string to use a DOM element or a component.\n */\n component: _utils.componentPropType,\n\n /**\n * If `true`, the label should be displayed in a disabled state.\n */\n disabled: _propTypes.default.bool,\n\n /**\n * If `true`, the label should be displayed in an error state.\n */\n error: _propTypes.default.bool,\n\n /**\n * If `true`, the label should use filled classes key.\n */\n filled: _propTypes.default.bool,\n\n /**\n * If `true`, the input of this label is focused (used by `FormGroup` components).\n */\n focused: _propTypes.default.bool,\n\n /**\n * @ignore\n */\n muiFormControl: _propTypes.default.object,\n\n /**\n * If `true`, the label will indicate that the input is required.\n */\n required: _propTypes.default.bool\n} : undefined;\nFormLabel.defaultProps = {\n component: 'label'\n};\n\nvar _default = (0, _withStyles.default)(styles, {\n name: 'MuiFormLabel'\n})((0, _withFormControlContext.default)(FormLabel));\n\nexports.default = _default;\n\n//# sourceURL=webpack:///./node_modules/@material-ui/core/FormLabel/FormLabel.js?"); - - /***/ }), - - /***/ "./node_modules/@material-ui/core/FormLabel/index.js": - /*!***********************************************************!*\ - !*** ./node_modules/@material-ui/core/FormLabel/index.js ***! - \***********************************************************/ - /*! no static exports found */ - /***/ (function(module, exports, __webpack_require__) { - - "use strict"; - eval("\n\nvar _interopRequireDefault = __webpack_require__(/*! @babel/runtime/helpers/interopRequireDefault */ \"./node_modules/@babel/runtime/helpers/interopRequireDefault.js\");\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nObject.defineProperty(exports, \"default\", {\n enumerable: true,\n get: function get() {\n return _FormLabel.default;\n }\n});\n\nvar _FormLabel = _interopRequireDefault(__webpack_require__(/*! ./FormLabel */ \"./node_modules/@material-ui/core/FormLabel/FormLabel.js\"));\n\n//# sourceURL=webpack:///./node_modules/@material-ui/core/FormLabel/index.js?"); - - /***/ }), - - /***/ "./node_modules/@material-ui/core/Grid/Grid.js": - /*!*****************************************************!*\ - !*** ./node_modules/@material-ui/core/Grid/Grid.js ***! - \*****************************************************/ - /*! no static exports found */ - /***/ (function(module, exports, __webpack_require__) { - - "use strict"; - eval("\n\nvar _interopRequireDefault = __webpack_require__(/*! @babel/runtime/helpers/interopRequireDefault */ \"./node_modules/@babel/runtime/helpers/interopRequireDefault.js\");\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.default = exports.styles = void 0;\n\nvar _defineProperty2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/defineProperty */ \"./node_modules/@babel/runtime/helpers/defineProperty.js\"));\n\nvar _objectWithoutProperties2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/objectWithoutProperties */ \"./node_modules/@babel/runtime/helpers/objectWithoutProperties.js\"));\n\nvar _extends2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/extends */ \"./node_modules/@babel/runtime/helpers/extends.js\"));\n\nvar _react = _interopRequireDefault(__webpack_require__(/*! react */ \"./node_modules/react/index.js\"));\n\nvar _propTypes = _interopRequireDefault(__webpack_require__(/*! prop-types */ \"./node_modules/prop-types/index.js\"));\n\nvar _classnames = _interopRequireDefault(__webpack_require__(/*! classnames */ \"./node_modules/classnames/index.js\"));\n\nvar _utils = __webpack_require__(/*! @material-ui/utils */ \"./node_modules/@material-ui/utils/index.es.js\");\n\nvar _withStyles = _interopRequireDefault(__webpack_require__(/*! ../styles/withStyles */ \"./node_modules/@material-ui/core/styles/withStyles.js\"));\n\nvar _createBreakpoints = __webpack_require__(/*! ../styles/createBreakpoints */ \"./node_modules/@material-ui/core/styles/createBreakpoints.js\");\n\nvar _requirePropFactory = _interopRequireDefault(__webpack_require__(/*! ../utils/requirePropFactory */ \"./node_modules/@material-ui/core/utils/requirePropFactory.js\"));\n\n// A grid component using the following libs as inspiration.\n//\n// For the implementation:\n// - http://v4-alpha.getbootstrap.com/layout/flexbox-grid/\n// - https://github.com/kristoferjoseph/flexboxgrid/blob/master/src/css/flexboxgrid.css\n// - https://github.com/roylee0704/react-flexbox-grid\n// - https://material.angularjs.org/latest/layout/introduction\n//\n// Follow this flexbox Guide to better understand the underlying model:\n// - https://css-tricks.com/snippets/css/a-guide-to-flexbox/\nvar GUTTERS = [0, 8, 16, 24, 32, 40];\nvar GRID_SIZES = ['auto', true, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];\n\nfunction generateGrid(globalStyles, theme, breakpoint) {\n var styles = {};\n GRID_SIZES.forEach(function (size) {\n var key = \"grid-\".concat(breakpoint, \"-\").concat(size);\n\n if (size === true) {\n // For the auto layouting\n styles[key] = {\n flexBasis: 0,\n flexGrow: 1,\n maxWidth: '100%'\n };\n return;\n }\n\n if (size === 'auto') {\n styles[key] = {\n flexBasis: 'auto',\n flexGrow: 0,\n maxWidth: 'none'\n };\n return;\n } // Keep 7 significant numbers.\n\n\n var width = \"\".concat(Math.round(size / 12 * 10e7) / 10e5, \"%\"); // Close to the bootstrap implementation:\n // https://github.com/twbs/bootstrap/blob/8fccaa2439e97ec72a4b7dc42ccc1f649790adb0/scss/mixins/_grid.scss#L41\n\n styles[key] = {\n flexBasis: width,\n flexGrow: 0,\n maxWidth: width\n };\n }); // No need for a media query for the first size.\n\n if (breakpoint === 'xs') {\n (0, _extends2.default)(globalStyles, styles);\n } else {\n globalStyles[theme.breakpoints.up(breakpoint)] = styles;\n }\n}\n\nfunction generateGutter(theme, breakpoint) {\n var styles = {};\n GUTTERS.forEach(function (spacing, index) {\n if (index === 0) {\n // Skip the default style.\n return;\n }\n\n styles[\"spacing-\".concat(breakpoint, \"-\").concat(spacing)] = {\n margin: -spacing / 2,\n width: \"calc(100% + \".concat(spacing, \"px)\"),\n '& > $item': {\n padding: spacing / 2\n }\n };\n });\n return styles;\n} // Default CSS values\n// flex: '0 1 auto',\n// flexDirection: 'row',\n// alignItems: 'flex-start',\n// flexWrap: 'nowrap',\n// justifyContent: 'flex-start',\n\n\nvar styles = function styles(theme) {\n return (0, _extends2.default)({\n /* Styles applied to the root element if `container={true}`. */\n container: {\n boxSizing: 'border-box',\n display: 'flex',\n flexWrap: 'wrap',\n width: '100%'\n },\n\n /* Styles applied to the root element if `item={true}`. */\n item: {\n boxSizing: 'border-box',\n margin: '0' // For instance, it's useful when used with a `figure` element.\n\n },\n\n /* Styles applied to the root element if `zeroMinWidth={true}`. */\n zeroMinWidth: {\n minWidth: 0\n },\n\n /* Styles applied to the root element if `direction=\"column\"`. */\n 'direction-xs-column': {\n flexDirection: 'column'\n },\n\n /* Styles applied to the root element if `direction=\"column-reverse\"`. */\n 'direction-xs-column-reverse': {\n flexDirection: 'column-reverse'\n },\n\n /* Styles applied to the root element if `direction=\"rwo-reverse\"`. */\n 'direction-xs-row-reverse': {\n flexDirection: 'row-reverse'\n },\n\n /* Styles applied to the root element if `wrap=\"nowrap\"`. */\n 'wrap-xs-nowrap': {\n flexWrap: 'nowrap'\n },\n\n /* Styles applied to the root element if `wrap=\"reverse\"`. */\n 'wrap-xs-wrap-reverse': {\n flexWrap: 'wrap-reverse'\n },\n\n /* Styles applied to the root element if `alignItems=\"center\"`. */\n 'align-items-xs-center': {\n alignItems: 'center'\n },\n\n /* Styles applied to the root element if `alignItems=\"flex-start\"`. */\n 'align-items-xs-flex-start': {\n alignItems: 'flex-start'\n },\n\n /* Styles applied to the root element if `alignItems=\"flex-end\"`. */\n 'align-items-xs-flex-end': {\n alignItems: 'flex-end'\n },\n\n /* Styles applied to the root element if `alignItems=\"baseline\"`. */\n 'align-items-xs-baseline': {\n alignItems: 'baseline'\n },\n\n /* Styles applied to the root element if `alignContent=\"center\"`. */\n 'align-content-xs-center': {\n alignContent: 'center'\n },\n\n /* Styles applied to the root element if `alignContent=\"flex-start\"`. */\n 'align-content-xs-flex-start': {\n alignContent: 'flex-start'\n },\n\n /* Styles applied to the root element if `alignContent=\"flex-end\"`. */\n 'align-content-xs-flex-end': {\n alignContent: 'flex-end'\n },\n\n /* Styles applied to the root element if `alignContent=\"space-between\"`. */\n 'align-content-xs-space-between': {\n alignContent: 'space-between'\n },\n\n /* Styles applied to the root element if `alignContent=\"space-around\"`. */\n 'align-content-xs-space-around': {\n alignContent: 'space-around'\n },\n\n /* Styles applied to the root element if `justify=\"center\"`. */\n 'justify-xs-center': {\n justifyContent: 'center'\n },\n\n /* Styles applied to the root element if `justify=\"flex-end\"`. */\n 'justify-xs-flex-end': {\n justifyContent: 'flex-end'\n },\n\n /* Styles applied to the root element if `justify=\"space-between\"`. */\n 'justify-xs-space-between': {\n justifyContent: 'space-between'\n },\n\n /* Styles applied to the root element if `justify=\"space-around\"`. */\n 'justify-xs-space-around': {\n justifyContent: 'space-around'\n },\n\n /* Styles applied to the root element if `justify=\"space-evenly\"`. */\n 'justify-xs-space-evenly': {\n justifyContent: 'space-evenly'\n }\n }, generateGutter(theme, 'xs'), _createBreakpoints.keys.reduce(function (accumulator, key) {\n // Use side effect over immutability for better performance.\n generateGrid(accumulator, theme, key);\n return accumulator;\n }, {}));\n};\n\nexports.styles = styles;\n\nfunction Grid(props) {\n var _classNames;\n\n var alignContent = props.alignContent,\n alignItems = props.alignItems,\n classes = props.classes,\n classNameProp = props.className,\n Component = props.component,\n container = props.container,\n direction = props.direction,\n item = props.item,\n justify = props.justify,\n lg = props.lg,\n md = props.md,\n sm = props.sm,\n spacing = props.spacing,\n wrap = props.wrap,\n xl = props.xl,\n xs = props.xs,\n zeroMinWidth = props.zeroMinWidth,\n other = (0, _objectWithoutProperties2.default)(props, [\"alignContent\", \"alignItems\", \"classes\", \"className\", \"component\", \"container\", \"direction\", \"item\", \"justify\", \"lg\", \"md\", \"sm\", \"spacing\", \"wrap\", \"xl\", \"xs\", \"zeroMinWidth\"]);\n var className = (0, _classnames.default)((_classNames = {}, (0, _defineProperty2.default)(_classNames, classes.container, container), (0, _defineProperty2.default)(_classNames, classes.item, item), (0, _defineProperty2.default)(_classNames, classes.zeroMinWidth, zeroMinWidth), (0, _defineProperty2.default)(_classNames, classes[\"spacing-xs-\".concat(String(spacing))], container && spacing !== 0), (0, _defineProperty2.default)(_classNames, classes[\"direction-xs-\".concat(String(direction))], direction !== Grid.defaultProps.direction), (0, _defineProperty2.default)(_classNames, classes[\"wrap-xs-\".concat(String(wrap))], wrap !== Grid.defaultProps.wrap), (0, _defineProperty2.default)(_classNames, classes[\"align-items-xs-\".concat(String(alignItems))], alignItems !== Grid.defaultProps.alignItems), (0, _defineProperty2.default)(_classNames, classes[\"align-content-xs-\".concat(String(alignContent))], alignContent !== Grid.defaultProps.alignContent), (0, _defineProperty2.default)(_classNames, classes[\"justify-xs-\".concat(String(justify))], justify !== Grid.defaultProps.justify), (0, _defineProperty2.default)(_classNames, classes[\"grid-xs-\".concat(String(xs))], xs !== false), (0, _defineProperty2.default)(_classNames, classes[\"grid-sm-\".concat(String(sm))], sm !== false), (0, _defineProperty2.default)(_classNames, classes[\"grid-md-\".concat(String(md))], md !== false), (0, _defineProperty2.default)(_classNames, classes[\"grid-lg-\".concat(String(lg))], lg !== false), (0, _defineProperty2.default)(_classNames, classes[\"grid-xl-\".concat(String(xl))], xl !== false), _classNames), classNameProp);\n return _react.default.createElement(Component, (0, _extends2.default)({\n className: className\n }, other));\n}\n\n true ? Grid.propTypes = {\n /**\n * Defines the `align-content` style property.\n * It's applied for all screen sizes.\n */\n alignContent: _propTypes.default.oneOf(['stretch', 'center', 'flex-start', 'flex-end', 'space-between', 'space-around']),\n\n /**\n * Defines the `align-items` style property.\n * It's applied for all screen sizes.\n */\n alignItems: _propTypes.default.oneOf(['flex-start', 'center', 'flex-end', 'stretch', 'baseline']),\n\n /**\n * The content of the component.\n */\n children: _propTypes.default.node,\n\n /**\n * Override or extend the styles applied to the component.\n * See [CSS API](#css-api) below for more details.\n */\n classes: _propTypes.default.object.isRequired,\n\n /**\n * @ignore\n */\n className: _propTypes.default.string,\n\n /**\n * The component used for the root node.\n * Either a string to use a DOM element or a component.\n */\n component: _utils.componentPropType,\n\n /**\n * If `true`, the component will have the flex *container* behavior.\n * You should be wrapping *items* with a *container*.\n */\n container: _propTypes.default.bool,\n\n /**\n * Defines the `flex-direction` style property.\n * It is applied for all screen sizes.\n */\n direction: _propTypes.default.oneOf(['row', 'row-reverse', 'column', 'column-reverse']),\n\n /**\n * If `true`, the component will have the flex *item* behavior.\n * You should be wrapping *items* with a *container*.\n */\n item: _propTypes.default.bool,\n\n /**\n * Defines the `justify-content` style property.\n * It is applied for all screen sizes.\n */\n justify: _propTypes.default.oneOf(['flex-start', 'center', 'flex-end', 'space-between', 'space-around', 'space-evenly']),\n\n /**\n * Defines the number of grids the component is going to use.\n * It's applied for the `lg` breakpoint and wider screens if not overridden.\n */\n lg: _propTypes.default.oneOf([false, 'auto', true, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]),\n\n /**\n * Defines the number of grids the component is going to use.\n * It's applied for the `md` breakpoint and wider screens if not overridden.\n */\n md: _propTypes.default.oneOf([false, 'auto', true, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]),\n\n /**\n * Defines the number of grids the component is going to use.\n * It's applied for the `sm` breakpoint and wider screens if not overridden.\n */\n sm: _propTypes.default.oneOf([false, 'auto', true, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]),\n\n /**\n * Defines the space between the type `item` component.\n * It can only be used on a type `container` component.\n */\n spacing: _propTypes.default.oneOf(GUTTERS),\n\n /**\n * Defines the `flex-wrap` style property.\n * It's applied for all screen sizes.\n */\n wrap: _propTypes.default.oneOf(['nowrap', 'wrap', 'wrap-reverse']),\n\n /**\n * Defines the number of grids the component is going to use.\n * It's applied for the `xl` breakpoint and wider screens.\n */\n xl: _propTypes.default.oneOf([false, 'auto', true, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]),\n\n /**\n * Defines the number of grids the component is going to use.\n * It's applied for all the screen sizes with the lowest priority.\n */\n xs: _propTypes.default.oneOf([false, 'auto', true, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]),\n\n /**\n * If `true`, it sets `min-width: 0` on the item.\n * Refer to the limitations section of the documentation to better understand the use case.\n */\n zeroMinWidth: _propTypes.default.bool\n} : undefined;\nGrid.defaultProps = {\n alignContent: 'stretch',\n alignItems: 'stretch',\n component: 'div',\n container: false,\n direction: 'row',\n item: false,\n justify: 'flex-start',\n lg: false,\n md: false,\n sm: false,\n spacing: 0,\n wrap: 'wrap',\n xl: false,\n xs: false,\n zeroMinWidth: false\n};\nvar StyledGrid = (0, _withStyles.default)(styles, {\n name: 'MuiGrid'\n})(Grid);\n\nif (true) {\n var requireProp = (0, _requirePropFactory.default)('Grid');\n StyledGrid.propTypes = (0, _extends2.default)({}, StyledGrid.propTypes, {\n alignContent: requireProp('container'),\n alignItems: requireProp('container'),\n direction: requireProp('container'),\n justify: requireProp('container'),\n lg: requireProp('item'),\n md: requireProp('item'),\n sm: requireProp('item'),\n spacing: requireProp('container'),\n wrap: requireProp('container'),\n xs: requireProp('item'),\n zeroMinWidth: requireProp('zeroMinWidth')\n });\n}\n\nvar _default = StyledGrid;\nexports.default = _default;\n\n//# sourceURL=webpack:///./node_modules/@material-ui/core/Grid/Grid.js?"); - - /***/ }), - - /***/ "./node_modules/@material-ui/core/Grid/index.js": - /*!******************************************************!*\ - !*** ./node_modules/@material-ui/core/Grid/index.js ***! - \******************************************************/ - /*! no static exports found */ - /***/ (function(module, exports, __webpack_require__) { - - "use strict"; - eval("\n\nvar _interopRequireDefault = __webpack_require__(/*! @babel/runtime/helpers/interopRequireDefault */ \"./node_modules/@babel/runtime/helpers/interopRequireDefault.js\");\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nObject.defineProperty(exports, \"default\", {\n enumerable: true,\n get: function get() {\n return _Grid.default;\n }\n});\n\nvar _Grid = _interopRequireDefault(__webpack_require__(/*! ./Grid */ \"./node_modules/@material-ui/core/Grid/Grid.js\"));\n\n//# sourceURL=webpack:///./node_modules/@material-ui/core/Grid/index.js?"); - - /***/ }), - - /***/ "./node_modules/@material-ui/core/Grow/Grow.js": - /*!*****************************************************!*\ - !*** ./node_modules/@material-ui/core/Grow/Grow.js ***! - \*****************************************************/ - /*! no static exports found */ - /***/ (function(module, exports, __webpack_require__) { - - "use strict"; - eval("\n\nvar _interopRequireDefault = __webpack_require__(/*! @babel/runtime/helpers/interopRequireDefault */ \"./node_modules/@babel/runtime/helpers/interopRequireDefault.js\");\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.default = void 0;\n\nvar _extends2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/extends */ \"./node_modules/@babel/runtime/helpers/extends.js\"));\n\nvar _objectWithoutProperties2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/objectWithoutProperties */ \"./node_modules/@babel/runtime/helpers/objectWithoutProperties.js\"));\n\nvar _classCallCheck2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/classCallCheck */ \"./node_modules/@babel/runtime/helpers/classCallCheck.js\"));\n\nvar _createClass2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/createClass */ \"./node_modules/@babel/runtime/helpers/createClass.js\"));\n\nvar _possibleConstructorReturn2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/possibleConstructorReturn */ \"./node_modules/@babel/runtime/helpers/possibleConstructorReturn.js\"));\n\nvar _getPrototypeOf3 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/getPrototypeOf */ \"./node_modules/@babel/runtime/helpers/getPrototypeOf.js\"));\n\nvar _inherits2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/inherits */ \"./node_modules/@babel/runtime/helpers/inherits.js\"));\n\nvar _react = _interopRequireDefault(__webpack_require__(/*! react */ \"./node_modules/react/index.js\"));\n\nvar _propTypes = _interopRequireDefault(__webpack_require__(/*! prop-types */ \"./node_modules/prop-types/index.js\"));\n\nvar _Transition = _interopRequireDefault(__webpack_require__(/*! react-transition-group/Transition */ \"./node_modules/react-transition-group/Transition.js\"));\n\nvar _withTheme = _interopRequireDefault(__webpack_require__(/*! ../styles/withTheme */ \"./node_modules/@material-ui/core/styles/withTheme.js\"));\n\nvar _utils = __webpack_require__(/*! ../transitions/utils */ \"./node_modules/@material-ui/core/transitions/utils.js\");\n\n// @inheritedComponent Transition\nfunction getScale(value) {\n return \"scale(\".concat(value, \", \").concat(Math.pow(value, 2), \")\");\n}\n\nvar styles = {\n entering: {\n opacity: 1,\n transform: getScale(1)\n },\n entered: {\n opacity: 1,\n // Use translateZ to scrolling issue on Chrome.\n transform: \"\".concat(getScale(1), \" translateZ(0)\")\n }\n};\n/**\n * The Grow transition is used by the [Tooltip](/demos/tooltips/) and\n * [Popover](/utils/popover/) components.\n * It uses [react-transition-group](https://github.com/reactjs/react-transition-group) internally.\n */\n\nvar Grow =\n/*#__PURE__*/\nfunction (_React$Component) {\n (0, _inherits2.default)(Grow, _React$Component);\n\n function Grow() {\n var _getPrototypeOf2;\n\n var _this;\n\n (0, _classCallCheck2.default)(this, Grow);\n\n for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {\n args[_key] = arguments[_key];\n }\n\n _this = (0, _possibleConstructorReturn2.default)(this, (_getPrototypeOf2 = (0, _getPrototypeOf3.default)(Grow)).call.apply(_getPrototypeOf2, [this].concat(args)));\n\n _this.handleEnter = function (node) {\n var _this$props = _this.props,\n theme = _this$props.theme,\n timeout = _this$props.timeout;\n (0, _utils.reflow)(node); // So the animation always start from the start.\n\n var _getTransitionProps = (0, _utils.getTransitionProps)(_this.props, {\n mode: 'enter'\n }),\n transitionDuration = _getTransitionProps.duration,\n delay = _getTransitionProps.delay;\n\n var duration = 0;\n\n if (timeout === 'auto') {\n duration = theme.transitions.getAutoHeightDuration(node.clientHeight);\n _this.autoTimeout = duration;\n } else {\n duration = transitionDuration;\n }\n\n node.style.transition = [theme.transitions.create('opacity', {\n duration: duration,\n delay: delay\n }), theme.transitions.create('transform', {\n duration: duration * 0.666,\n delay: delay\n })].join(',');\n\n if (_this.props.onEnter) {\n _this.props.onEnter(node);\n }\n };\n\n _this.handleExit = function (node) {\n var _this$props2 = _this.props,\n theme = _this$props2.theme,\n timeout = _this$props2.timeout;\n var duration = 0;\n\n var _getTransitionProps2 = (0, _utils.getTransitionProps)(_this.props, {\n mode: 'exit'\n }),\n transitionDuration = _getTransitionProps2.duration,\n delay = _getTransitionProps2.delay;\n\n if (timeout === 'auto') {\n duration = theme.transitions.getAutoHeightDuration(node.clientHeight);\n _this.autoTimeout = duration;\n } else {\n duration = transitionDuration;\n }\n\n node.style.transition = [theme.transitions.create('opacity', {\n duration: duration,\n delay: delay\n }), theme.transitions.create('transform', {\n duration: duration * 0.666,\n delay: delay || duration * 0.333\n })].join(',');\n node.style.opacity = '0';\n node.style.transform = getScale(0.75);\n\n if (_this.props.onExit) {\n _this.props.onExit(node);\n }\n };\n\n _this.addEndListener = function (_, next) {\n if (_this.props.timeout === 'auto') {\n _this.timer = setTimeout(next, _this.autoTimeout || 0);\n }\n };\n\n return _this;\n }\n\n (0, _createClass2.default)(Grow, [{\n key: \"componentWillUnmount\",\n value: function componentWillUnmount() {\n clearTimeout(this.timer);\n }\n }, {\n key: \"render\",\n value: function render() {\n var _this$props3 = this.props,\n children = _this$props3.children,\n onEnter = _this$props3.onEnter,\n onExit = _this$props3.onExit,\n styleProp = _this$props3.style,\n theme = _this$props3.theme,\n timeout = _this$props3.timeout,\n other = (0, _objectWithoutProperties2.default)(_this$props3, [\"children\", \"onEnter\", \"onExit\", \"style\", \"theme\", \"timeout\"]);\n var style = (0, _extends2.default)({}, styleProp, _react.default.isValidElement(children) ? children.props.style : {});\n return _react.default.createElement(_Transition.default, (0, _extends2.default)({\n appear: true,\n onEnter: this.handleEnter,\n onExit: this.handleExit,\n addEndListener: this.addEndListener,\n timeout: timeout === 'auto' ? null : timeout\n }, other), function (state, childProps) {\n return _react.default.cloneElement(children, (0, _extends2.default)({\n style: (0, _extends2.default)({\n opacity: 0,\n transform: getScale(0.75)\n }, styles[state], style)\n }, childProps));\n });\n }\n }]);\n return Grow;\n}(_react.default.Component);\n\n true ? Grow.propTypes = {\n /**\n * A single child content element.\n */\n children: _propTypes.default.oneOfType([_propTypes.default.element, _propTypes.default.func]),\n\n /**\n * If `true`, show the component; triggers the enter or exit animation.\n */\n in: _propTypes.default.bool,\n\n /**\n * @ignore\n */\n onEnter: _propTypes.default.func,\n\n /**\n * @ignore\n */\n onExit: _propTypes.default.func,\n\n /**\n * @ignore\n */\n style: _propTypes.default.object,\n\n /**\n * @ignore\n */\n theme: _propTypes.default.object.isRequired,\n\n /**\n * The duration for the transition, in milliseconds.\n * You may specify a single timeout for all transitions, or individually with an object.\n *\n * Set to 'auto' to automatically calculate transition time based on height.\n */\n timeout: _propTypes.default.oneOfType([_propTypes.default.number, _propTypes.default.shape({\n enter: _propTypes.default.number,\n exit: _propTypes.default.number\n }), _propTypes.default.oneOf(['auto'])])\n} : undefined;\nGrow.defaultProps = {\n timeout: 'auto'\n};\nGrow.muiSupportAuto = true;\n\nvar _default = (0, _withTheme.default)()(Grow);\n\nexports.default = _default;\n\n//# sourceURL=webpack:///./node_modules/@material-ui/core/Grow/Grow.js?"); - - /***/ }), - - /***/ "./node_modules/@material-ui/core/Grow/index.js": - /*!******************************************************!*\ - !*** ./node_modules/@material-ui/core/Grow/index.js ***! - \******************************************************/ - /*! no static exports found */ - /***/ (function(module, exports, __webpack_require__) { - - "use strict"; - eval("\n\nvar _interopRequireDefault = __webpack_require__(/*! @babel/runtime/helpers/interopRequireDefault */ \"./node_modules/@babel/runtime/helpers/interopRequireDefault.js\");\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nObject.defineProperty(exports, \"default\", {\n enumerable: true,\n get: function get() {\n return _Grow.default;\n }\n});\n\nvar _Grow = _interopRequireDefault(__webpack_require__(/*! ./Grow */ \"./node_modules/@material-ui/core/Grow/Grow.js\"));\n\n//# sourceURL=webpack:///./node_modules/@material-ui/core/Grow/index.js?"); - - /***/ }), - - /***/ "./node_modules/@material-ui/core/IconButton/IconButton.js": - /*!*****************************************************************!*\ - !*** ./node_modules/@material-ui/core/IconButton/IconButton.js ***! - \*****************************************************************/ - /*! no static exports found */ - /***/ (function(module, exports, __webpack_require__) { - - "use strict"; - eval("\n\nvar _interopRequireDefault = __webpack_require__(/*! @babel/runtime/helpers/interopRequireDefault */ \"./node_modules/@babel/runtime/helpers/interopRequireDefault.js\");\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.default = exports.styles = void 0;\n\nvar _extends2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/extends */ \"./node_modules/@babel/runtime/helpers/extends.js\"));\n\nvar _defineProperty2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/defineProperty */ \"./node_modules/@babel/runtime/helpers/defineProperty.js\"));\n\nvar _objectWithoutProperties2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/objectWithoutProperties */ \"./node_modules/@babel/runtime/helpers/objectWithoutProperties.js\"));\n\nvar _react = _interopRequireDefault(__webpack_require__(/*! react */ \"./node_modules/react/index.js\"));\n\nvar _propTypes = _interopRequireDefault(__webpack_require__(/*! prop-types */ \"./node_modules/prop-types/index.js\"));\n\nvar _classnames = _interopRequireDefault(__webpack_require__(/*! classnames */ \"./node_modules/classnames/index.js\"));\n\nvar _utils = __webpack_require__(/*! @material-ui/utils */ \"./node_modules/@material-ui/utils/index.es.js\");\n\nvar _withStyles = _interopRequireDefault(__webpack_require__(/*! ../styles/withStyles */ \"./node_modules/@material-ui/core/styles/withStyles.js\"));\n\nvar _colorManipulator = __webpack_require__(/*! ../styles/colorManipulator */ \"./node_modules/@material-ui/core/styles/colorManipulator.js\");\n\nvar _ButtonBase = _interopRequireDefault(__webpack_require__(/*! ../ButtonBase */ \"./node_modules/@material-ui/core/ButtonBase/index.js\"));\n\nvar _helpers = __webpack_require__(/*! ../utils/helpers */ \"./node_modules/@material-ui/core/utils/helpers.js\");\n\n// @inheritedComponent ButtonBase\nvar styles = function styles(theme) {\n return {\n /* Styles applied to the root element. */\n root: {\n textAlign: 'center',\n flex: '0 0 auto',\n fontSize: theme.typography.pxToRem(24),\n padding: 12,\n borderRadius: '50%',\n overflow: 'visible',\n // Explicitly set the default value to solve a bug on IE 11.\n color: theme.palette.action.active,\n transition: theme.transitions.create('background-color', {\n duration: theme.transitions.duration.shortest\n }),\n '&:hover': {\n backgroundColor: (0, _colorManipulator.fade)(theme.palette.action.active, theme.palette.action.hoverOpacity),\n // Reset on touch devices, it doesn't add specificity\n '@media (hover: none)': {\n backgroundColor: 'transparent'\n },\n '&$disabled': {\n backgroundColor: 'transparent'\n }\n },\n '&$disabled': {\n color: theme.palette.action.disabled\n }\n },\n\n /* Styles applied to the root element if `color=\"inherit\"`. */\n colorInherit: {\n color: 'inherit'\n },\n\n /* Styles applied to the root element if `color=\"primary\"`. */\n colorPrimary: {\n color: theme.palette.primary.main,\n '&:hover': {\n backgroundColor: (0, _colorManipulator.fade)(theme.palette.primary.main, theme.palette.action.hoverOpacity),\n // Reset on touch devices, it doesn't add specificity\n '@media (hover: none)': {\n backgroundColor: 'transparent'\n }\n }\n },\n\n /* Styles applied to the root element if `color=\"secondary\"`. */\n colorSecondary: {\n color: theme.palette.secondary.main,\n '&:hover': {\n backgroundColor: (0, _colorManipulator.fade)(theme.palette.secondary.main, theme.palette.action.hoverOpacity),\n // Reset on touch devices, it doesn't add specificity\n '@media (hover: none)': {\n backgroundColor: 'transparent'\n }\n }\n },\n\n /* Styles applied to the root element if `disabled={true}`. */\n disabled: {},\n\n /* Styles applied to the children container element. */\n label: {\n width: '100%',\n display: 'flex',\n alignItems: 'inherit',\n justifyContent: 'inherit'\n }\n };\n};\n/**\n * Refer to the [Icons](/style/icons/) section of the documentation\n * regarding the available icon options.\n */\n\n\nexports.styles = styles;\n\nfunction IconButton(props) {\n var _classNames;\n\n var children = props.children,\n classes = props.classes,\n className = props.className,\n color = props.color,\n disabled = props.disabled,\n other = (0, _objectWithoutProperties2.default)(props, [\"children\", \"classes\", \"className\", \"color\", \"disabled\"]);\n return _react.default.createElement(_ButtonBase.default, (0, _extends2.default)({\n className: (0, _classnames.default)(classes.root, (_classNames = {}, (0, _defineProperty2.default)(_classNames, classes[\"color\".concat((0, _helpers.capitalize)(color))], color !== 'default'), (0, _defineProperty2.default)(_classNames, classes.disabled, disabled), _classNames), className),\n centerRipple: true,\n focusRipple: true,\n disabled: disabled\n }, other), _react.default.createElement(\"span\", {\n className: classes.label\n }, children));\n}\n\n true ? IconButton.propTypes = {\n /**\n * The icon element.\n */\n children: (0, _utils.chainPropTypes)(_propTypes.default.node, function (props) {\n var found = _react.default.Children.toArray(props.children).some(function (child) {\n return _react.default.isValidElement(child) && child.props.onClick;\n });\n\n if (found) {\n return new Error(['Material-UI: you are providing an onClick event listener ' + 'to a child of a button element.', 'Firefox will never trigger the event.', 'You should move the onClick listener to the parent button element.', 'https://github.com/mui-org/material-ui/issues/13957', // Change error message slightly on every check to prevent caching when testing\n // which would not trigger console errors on subsequent fails\n false ? undefined : ''].join('\\n'));\n }\n\n return null;\n }),\n\n /**\n * Override or extend the styles applied to the component.\n * See [CSS API](#css-api) below for more details.\n */\n classes: _propTypes.default.object.isRequired,\n\n /**\n * @ignore\n */\n className: _propTypes.default.string,\n\n /**\n * The color of the component. It supports those theme colors that make sense for this component.\n */\n color: _propTypes.default.oneOf(['default', 'inherit', 'primary', 'secondary']),\n\n /**\n * If `true`, the button will be disabled.\n */\n disabled: _propTypes.default.bool,\n\n /**\n * If `true`, the ripple will be disabled.\n */\n disableRipple: _propTypes.default.bool\n} : undefined;\nIconButton.defaultProps = {\n color: 'default',\n disabled: false\n};\n\nvar _default = (0, _withStyles.default)(styles, {\n name: 'MuiIconButton'\n})(IconButton);\n\nexports.default = _default;\n\n//# sourceURL=webpack:///./node_modules/@material-ui/core/IconButton/IconButton.js?"); - - /***/ }), - - /***/ "./node_modules/@material-ui/core/IconButton/index.js": - /*!************************************************************!*\ - !*** ./node_modules/@material-ui/core/IconButton/index.js ***! - \************************************************************/ - /*! no static exports found */ - /***/ (function(module, exports, __webpack_require__) { - - "use strict"; - eval("\n\nvar _interopRequireDefault = __webpack_require__(/*! @babel/runtime/helpers/interopRequireDefault */ \"./node_modules/@babel/runtime/helpers/interopRequireDefault.js\");\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nObject.defineProperty(exports, \"default\", {\n enumerable: true,\n get: function get() {\n return _IconButton.default;\n }\n});\n\nvar _IconButton = _interopRequireDefault(__webpack_require__(/*! ./IconButton */ \"./node_modules/@material-ui/core/IconButton/IconButton.js\"));\n\n//# sourceURL=webpack:///./node_modules/@material-ui/core/IconButton/index.js?"); - - /***/ }), - - /***/ "./node_modules/@material-ui/core/Input/Input.js": - /*!*******************************************************!*\ - !*** ./node_modules/@material-ui/core/Input/Input.js ***! - \*******************************************************/ - /*! no static exports found */ - /***/ (function(module, exports, __webpack_require__) { - - "use strict"; - eval("\n\nvar _interopRequireDefault = __webpack_require__(/*! @babel/runtime/helpers/interopRequireDefault */ \"./node_modules/@babel/runtime/helpers/interopRequireDefault.js\");\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.default = exports.styles = void 0;\n\nvar _defineProperty2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/defineProperty */ \"./node_modules/@babel/runtime/helpers/defineProperty.js\"));\n\nvar _extends2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/extends */ \"./node_modules/@babel/runtime/helpers/extends.js\"));\n\nvar _objectWithoutProperties2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/objectWithoutProperties */ \"./node_modules/@babel/runtime/helpers/objectWithoutProperties.js\"));\n\nvar _react = _interopRequireDefault(__webpack_require__(/*! react */ \"./node_modules/react/index.js\"));\n\nvar _propTypes = _interopRequireDefault(__webpack_require__(/*! prop-types */ \"./node_modules/prop-types/index.js\"));\n\nvar _classnames = _interopRequireDefault(__webpack_require__(/*! classnames */ \"./node_modules/classnames/index.js\"));\n\nvar _utils = __webpack_require__(/*! @material-ui/utils */ \"./node_modules/@material-ui/utils/index.es.js\");\n\nvar _InputBase = _interopRequireDefault(__webpack_require__(/*! ../InputBase */ \"./node_modules/@material-ui/core/InputBase/index.js\"));\n\nvar _withStyles = _interopRequireDefault(__webpack_require__(/*! ../styles/withStyles */ \"./node_modules/@material-ui/core/styles/withStyles.js\"));\n\n// @inheritedComponent InputBase\nvar styles = function styles(theme) {\n var light = theme.palette.type === 'light';\n var bottomLineColor = light ? 'rgba(0, 0, 0, 0.42)' : 'rgba(255, 255, 255, 0.7)';\n return {\n /* Styles applied to the root element. */\n root: {\n position: 'relative'\n },\n\n /* Styles applied to the root element if the component is a descendant of `FormControl`. */\n formControl: {\n 'label + &': {\n marginTop: 16\n }\n },\n\n /* Styles applied to the root element if the component is focused. */\n focused: {},\n\n /* Styles applied to the root element if `disabled={true}`. */\n disabled: {},\n\n /* Styles applied to the root element if `disableUnderline={false}`. */\n underline: {\n '&:after': {\n borderBottom: \"2px solid \".concat(theme.palette.primary[light ? 'dark' : 'light']),\n left: 0,\n bottom: 0,\n // Doing the other way around crash on IE 11 \"''\" https://github.com/cssinjs/jss/issues/242\n content: '\"\"',\n position: 'absolute',\n right: 0,\n transform: 'scaleX(0)',\n transition: theme.transitions.create('transform', {\n duration: theme.transitions.duration.shorter,\n easing: theme.transitions.easing.easeOut\n }),\n pointerEvents: 'none' // Transparent to the hover style.\n\n },\n '&$focused:after': {\n transform: 'scaleX(1)'\n },\n '&$error:after': {\n borderBottomColor: theme.palette.error.main,\n transform: 'scaleX(1)' // error is always underlined in red\n\n },\n '&:before': {\n borderBottom: \"1px solid \".concat(bottomLineColor),\n left: 0,\n bottom: 0,\n // Doing the other way around crash on IE 11 \"''\" https://github.com/cssinjs/jss/issues/242\n content: '\"\\\\00a0\"',\n position: 'absolute',\n right: 0,\n transition: theme.transitions.create('border-bottom-color', {\n duration: theme.transitions.duration.shorter\n }),\n pointerEvents: 'none' // Transparent to the hover style.\n\n },\n '&:hover:not($disabled):not($focused):not($error):before': {\n borderBottom: \"2px solid \".concat(theme.palette.text.primary),\n // Reset on touch devices, it doesn't add specificity\n '@media (hover: none)': {\n borderBottom: \"1px solid \".concat(bottomLineColor)\n }\n },\n '&$disabled:before': {\n borderBottomStyle: 'dotted'\n }\n },\n\n /* Styles applied to the root element if `error={true}`. */\n error: {},\n\n /* Styles applied to the root element if `multiline={true}`. */\n multiline: {},\n\n /* Styles applied to the root element if `fullWidth={true}`. */\n fullWidth: {},\n\n /* Styles applied to the `input` element. */\n input: {},\n\n /* Styles applied to the `input` element if `margin=\"dense\"`. */\n inputMarginDense: {},\n\n /* Styles applied to the `input` element if `multiline={true}`. */\n inputMultiline: {},\n\n /* Styles applied to the `input` element if `type` is not \"text\"`. */\n inputType: {},\n\n /* Styles applied to the `input` element if `type=\"search\"`. */\n inputTypeSearch: {}\n };\n};\n\nexports.styles = styles;\n\nfunction Input(props) {\n var disableUnderline = props.disableUnderline,\n classes = props.classes,\n other = (0, _objectWithoutProperties2.default)(props, [\"disableUnderline\", \"classes\"]);\n return _react.default.createElement(_InputBase.default, (0, _extends2.default)({\n classes: (0, _extends2.default)({}, classes, {\n root: (0, _classnames.default)(classes.root, (0, _defineProperty2.default)({}, classes.underline, !disableUnderline)),\n underline: null\n })\n }, other));\n}\n\n true ? Input.propTypes = {\n /**\n * This property helps users to fill forms faster, especially on mobile devices.\n * The name can be confusing, as it's more like an autofill.\n * You can learn more about it here:\n * https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofill\n */\n autoComplete: _propTypes.default.string,\n\n /**\n * If `true`, the input will be focused during the first mount.\n */\n autoFocus: _propTypes.default.bool,\n\n /**\n * Override or extend the styles applied to the component.\n * See [CSS API](#css-api) below for more details.\n */\n classes: _propTypes.default.object.isRequired,\n\n /**\n * The CSS class name of the wrapper element.\n */\n className: _propTypes.default.string,\n\n /**\n * The default input value, useful when not controlling the component.\n */\n defaultValue: _propTypes.default.oneOfType([_propTypes.default.string, _propTypes.default.number, _propTypes.default.bool, _propTypes.default.object, _propTypes.default.arrayOf(_propTypes.default.oneOfType([_propTypes.default.string, _propTypes.default.number, _propTypes.default.bool, _propTypes.default.object]))]),\n\n /**\n * If `true`, the input will be disabled.\n */\n disabled: _propTypes.default.bool,\n\n /**\n * If `true`, the input will not have an underline.\n */\n disableUnderline: _propTypes.default.bool,\n\n /**\n * End `InputAdornment` for this component.\n */\n endAdornment: _propTypes.default.node,\n\n /**\n * If `true`, the input will indicate an error. This is normally obtained via context from\n * FormControl.\n */\n error: _propTypes.default.bool,\n\n /**\n * If `true`, the input will take up the full width of its container.\n */\n fullWidth: _propTypes.default.bool,\n\n /**\n * The id of the `input` element.\n */\n id: _propTypes.default.string,\n\n /**\n * The component used for the native input.\n * Either a string to use a DOM element or a component.\n */\n inputComponent: _utils.componentPropType,\n\n /**\n * Attributes applied to the `input` element.\n */\n inputProps: _propTypes.default.object,\n\n /**\n * Use that property to pass a ref callback to the native input component.\n */\n inputRef: _propTypes.default.oneOfType([_propTypes.default.func, _propTypes.default.object]),\n\n /**\n * If `dense`, will adjust vertical spacing. This is normally obtained via context from\n * FormControl.\n */\n margin: _propTypes.default.oneOf(['dense', 'none']),\n\n /**\n * If `true`, a textarea element will be rendered.\n */\n multiline: _propTypes.default.bool,\n\n /**\n * Name attribute of the `input` element.\n */\n name: _propTypes.default.string,\n\n /**\n * Callback fired when the value is changed.\n *\n * @param {object} event The event source of the callback.\n * You can pull out the new value by accessing `event.target.value`.\n */\n onChange: _propTypes.default.func,\n\n /**\n * The short hint displayed in the input before the user enters a value.\n */\n placeholder: _propTypes.default.string,\n\n /**\n * It prevents the user from changing the value of the field\n * (not from interacting with the field).\n */\n readOnly: _propTypes.default.bool,\n\n /**\n * If `true`, the input will be required.\n */\n required: _propTypes.default.bool,\n\n /**\n * Number of rows to display when multiline option is set to true.\n */\n rows: _propTypes.default.oneOfType([_propTypes.default.string, _propTypes.default.number]),\n\n /**\n * Maximum number of rows to display when multiline option is set to true.\n */\n rowsMax: _propTypes.default.oneOfType([_propTypes.default.string, _propTypes.default.number]),\n\n /**\n * Start `InputAdornment` for this component.\n */\n startAdornment: _propTypes.default.node,\n\n /**\n * Type of the input element. It should be a valid HTML5 input type.\n */\n type: _propTypes.default.string,\n\n /**\n * The input value, required for a controlled component.\n */\n value: _propTypes.default.oneOfType([_propTypes.default.string, _propTypes.default.number, _propTypes.default.bool, _propTypes.default.object, _propTypes.default.arrayOf(_propTypes.default.oneOfType([_propTypes.default.string, _propTypes.default.number, _propTypes.default.bool, _propTypes.default.object]))])\n} : undefined;\n_InputBase.default.defaultProps = {\n fullWidth: false,\n inputComponent: 'input',\n multiline: false,\n type: 'text'\n};\nInput.muiName = 'Input';\n\nvar _default = (0, _withStyles.default)(styles, {\n name: 'MuiInput'\n})(Input);\n\nexports.default = _default;\n\n//# sourceURL=webpack:///./node_modules/@material-ui/core/Input/Input.js?"); - - /***/ }), - - /***/ "./node_modules/@material-ui/core/Input/index.js": - /*!*******************************************************!*\ - !*** ./node_modules/@material-ui/core/Input/index.js ***! - \*******************************************************/ - /*! no static exports found */ - /***/ (function(module, exports, __webpack_require__) { - - "use strict"; - eval("\n\nvar _interopRequireDefault = __webpack_require__(/*! @babel/runtime/helpers/interopRequireDefault */ \"./node_modules/@babel/runtime/helpers/interopRequireDefault.js\");\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nObject.defineProperty(exports, \"default\", {\n enumerable: true,\n get: function get() {\n return _Input.default;\n }\n});\n\nvar _Input = _interopRequireDefault(__webpack_require__(/*! ./Input */ \"./node_modules/@material-ui/core/Input/Input.js\"));\n\n//# sourceURL=webpack:///./node_modules/@material-ui/core/Input/index.js?"); - - /***/ }), - - /***/ "./node_modules/@material-ui/core/InputBase/InputBase.js": - /*!***************************************************************!*\ - !*** ./node_modules/@material-ui/core/InputBase/InputBase.js ***! - \***************************************************************/ - /*! no static exports found */ - /***/ (function(module, exports, __webpack_require__) { - - "use strict"; - eval("\n\nvar _interopRequireDefault = __webpack_require__(/*! @babel/runtime/helpers/interopRequireDefault */ \"./node_modules/@babel/runtime/helpers/interopRequireDefault.js\");\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.default = exports.styles = void 0;\n\nvar _extends2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/extends */ \"./node_modules/@babel/runtime/helpers/extends.js\"));\n\nvar _defineProperty2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/defineProperty */ \"./node_modules/@babel/runtime/helpers/defineProperty.js\"));\n\nvar _objectWithoutProperties2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/objectWithoutProperties */ \"./node_modules/@babel/runtime/helpers/objectWithoutProperties.js\"));\n\nvar _classCallCheck2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/classCallCheck */ \"./node_modules/@babel/runtime/helpers/classCallCheck.js\"));\n\nvar _possibleConstructorReturn2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/possibleConstructorReturn */ \"./node_modules/@babel/runtime/helpers/possibleConstructorReturn.js\"));\n\nvar _getPrototypeOf2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/getPrototypeOf */ \"./node_modules/@babel/runtime/helpers/getPrototypeOf.js\"));\n\nvar _createClass2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/createClass */ \"./node_modules/@babel/runtime/helpers/createClass.js\"));\n\nvar _inherits2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/inherits */ \"./node_modules/@babel/runtime/helpers/inherits.js\"));\n\nvar _react = _interopRequireDefault(__webpack_require__(/*! react */ \"./node_modules/react/index.js\"));\n\nvar _propTypes = _interopRequireDefault(__webpack_require__(/*! prop-types */ \"./node_modules/prop-types/index.js\"));\n\nvar _warning = _interopRequireDefault(__webpack_require__(/*! warning */ \"./node_modules/warning/warning.js\"));\n\nvar _classnames = _interopRequireDefault(__webpack_require__(/*! classnames */ \"./node_modules/classnames/index.js\"));\n\nvar _utils = __webpack_require__(/*! @material-ui/utils */ \"./node_modules/@material-ui/utils/index.es.js\");\n\nvar _formControlState = _interopRequireDefault(__webpack_require__(/*! ../FormControl/formControlState */ \"./node_modules/@material-ui/core/FormControl/formControlState.js\"));\n\nvar _FormControlContext = _interopRequireDefault(__webpack_require__(/*! ../FormControl/FormControlContext */ \"./node_modules/@material-ui/core/FormControl/FormControlContext.js\"));\n\nvar _withFormControlContext = _interopRequireDefault(__webpack_require__(/*! ../FormControl/withFormControlContext */ \"./node_modules/@material-ui/core/FormControl/withFormControlContext.js\"));\n\nvar _withStyles = _interopRequireDefault(__webpack_require__(/*! ../styles/withStyles */ \"./node_modules/@material-ui/core/styles/withStyles.js\"));\n\nvar _reactHelpers = __webpack_require__(/*! ../utils/reactHelpers */ \"./node_modules/@material-ui/core/utils/reactHelpers.js\");\n\nvar _Textarea = _interopRequireDefault(__webpack_require__(/*! ./Textarea */ \"./node_modules/@material-ui/core/InputBase/Textarea.js\"));\n\nvar _utils2 = __webpack_require__(/*! ./utils */ \"./node_modules/@material-ui/core/InputBase/utils.js\");\n\n/* eslint-disable jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */\nvar styles = function styles(theme) {\n var light = theme.palette.type === 'light';\n var placeholder = {\n color: 'currentColor',\n opacity: light ? 0.42 : 0.5,\n transition: theme.transitions.create('opacity', {\n duration: theme.transitions.duration.shorter\n })\n };\n var placeholderHidden = {\n opacity: 0\n };\n var placeholderVisible = {\n opacity: light ? 0.42 : 0.5\n };\n return {\n /* Styles applied to the root element. */\n root: {\n // Mimics the default input display property used by browsers for an input.\n fontFamily: theme.typography.fontFamily,\n color: theme.palette.text.primary,\n fontSize: theme.typography.pxToRem(16),\n lineHeight: '1.1875em',\n // Reset (19px), match the native input line-height\n cursor: 'text',\n display: 'inline-flex',\n alignItems: 'center',\n '&$disabled': {\n color: theme.palette.text.disabled,\n cursor: 'default'\n }\n },\n\n /* Styles applied to the root element if the component is a descendant of `FormControl`. */\n formControl: {},\n\n /* Styles applied to the root element if the component is focused. */\n focused: {},\n\n /* Styles applied to the root element if `disabled={true}`. */\n disabled: {},\n\n /* Styles applied to the root element if `startAdornment` is provided. */\n adornedStart: {},\n\n /* Styles applied to the root element if `endAdornment` is provided. */\n adornedEnd: {},\n\n /* Styles applied to the root element if `error={true}`. */\n error: {},\n\n /* Styles applied to the `input` element if `margin=\"dense\"`. */\n marginDense: {},\n\n /* Styles applied to the root element if `multiline={true}`. */\n multiline: {\n padding: \"\".concat(8 - 2, \"px 0 \").concat(8 - 1, \"px\")\n },\n\n /* Styles applied to the root element if `fullWidth={true}`. */\n fullWidth: {\n width: '100%'\n },\n\n /* Styles applied to the `input` element. */\n input: {\n font: 'inherit',\n color: 'currentColor',\n padding: \"\".concat(8 - 2, \"px 0 \").concat(8 - 1, \"px\"),\n border: 0,\n boxSizing: 'content-box',\n background: 'none',\n margin: 0,\n // Reset for Safari\n // Remove grey highlight\n WebkitTapHighlightColor: 'transparent',\n display: 'block',\n // Make the flex item shrink with Firefox\n minWidth: 0,\n width: '100%',\n // Fix IE 11 width issue\n '&::-webkit-input-placeholder': placeholder,\n '&::-moz-placeholder': placeholder,\n // Firefox 19+\n '&:-ms-input-placeholder': placeholder,\n // IE 11\n '&::-ms-input-placeholder': placeholder,\n // Edge\n '&:focus': {\n outline: 0\n },\n // Reset Firefox invalid required input style\n '&:invalid': {\n boxShadow: 'none'\n },\n '&::-webkit-search-decoration': {\n // Remove the padding when type=search.\n '-webkit-appearance': 'none'\n },\n // Show and hide the placeholder logic\n 'label[data-shrink=false] + $formControl &': {\n '&::-webkit-input-placeholder': placeholderHidden,\n '&::-moz-placeholder': placeholderHidden,\n // Firefox 19+\n '&:-ms-input-placeholder': placeholderHidden,\n // IE 11\n '&::-ms-input-placeholder': placeholderHidden,\n // Edge\n '&:focus::-webkit-input-placeholder': placeholderVisible,\n '&:focus::-moz-placeholder': placeholderVisible,\n // Firefox 19+\n '&:focus:-ms-input-placeholder': placeholderVisible,\n // IE 11\n '&:focus::-ms-input-placeholder': placeholderVisible // Edge\n\n },\n '&$disabled': {\n opacity: 1 // Reset iOS opacity\n\n }\n },\n\n /* Styles applied to the `input` element if `margin=\"dense\"`. */\n inputMarginDense: {\n paddingTop: 4 - 1\n },\n\n /* Styles applied to the `input` element if `multiline={true}`. */\n inputMultiline: {\n resize: 'none',\n padding: 0\n },\n\n /* Styles applied to the `input` element if `type` is not \"text\"`. */\n inputType: {\n // type=\"date\" or type=\"time\", etc. have specific styles we need to reset.\n height: '1.1875em' // Reset (19px), match the native input line-height\n\n },\n\n /* Styles applied to the `input` element if `type=\"search\"`. */\n inputTypeSearch: {\n // Improve type search style.\n '-moz-appearance': 'textfield',\n '-webkit-appearance': 'textfield'\n },\n\n /* Styles applied to the `input` element if `startAdornment` is provided. */\n inputAdornedStart: {},\n\n /* Styles applied to the `input` element if `endAdornment` is provided. */\n inputAdornedEnd: {}\n };\n};\n/**\n * `InputBase` contains as few styles as possible.\n * It aims to be a simple building block for creating an input.\n * It contains a load of style reset and some state logic.\n */\n\n\nexports.styles = styles;\n\nvar InputBase =\n/*#__PURE__*/\nfunction (_React$Component) {\n (0, _inherits2.default)(InputBase, _React$Component);\n (0, _createClass2.default)(InputBase, null, [{\n key: \"getDerivedStateFromProps\",\n value: function getDerivedStateFromProps(props, state) {\n // The blur won't fire when the disabled state is set on a focused input.\n // We need to book keep the focused state manually.\n if (props.disabled && state.focused) {\n return {\n focused: false\n };\n }\n\n return null;\n }\n }]);\n\n function InputBase(props) {\n var _this;\n\n (0, _classCallCheck2.default)(this, InputBase);\n _this = (0, _possibleConstructorReturn2.default)(this, (0, _getPrototypeOf2.default)(InputBase).call(this, props));\n _this.state = {\n focused: false\n };\n\n _this.handleFocus = function (event) {\n var muiFormControl = _this.props.muiFormControl; // Fix a bug with IE 11 where the focus/blur events are triggered\n // while the input is disabled.\n\n if ((0, _formControlState.default)({\n props: _this.props,\n muiFormControl: muiFormControl,\n states: ['disabled']\n }).disabled) {\n event.stopPropagation();\n return;\n }\n\n _this.setState({\n focused: true\n });\n\n if (_this.props.onFocus) {\n _this.props.onFocus(event);\n }\n\n if (muiFormControl && muiFormControl.onFocus) {\n muiFormControl.onFocus(event);\n }\n };\n\n _this.handleBlur = function (event) {\n _this.setState({\n focused: false\n });\n\n if (_this.props.onBlur) {\n _this.props.onBlur(event);\n }\n\n var muiFormControl = _this.props.muiFormControl;\n\n if (muiFormControl && muiFormControl.onBlur) {\n muiFormControl.onBlur(event);\n }\n };\n\n _this.handleChange = function () {\n if (!_this.isControlled) {\n _this.checkDirty(_this.inputRef);\n } // Perform in the willUpdate\n\n\n if (_this.props.onChange) {\n var _this$props;\n\n (_this$props = _this.props).onChange.apply(_this$props, arguments);\n }\n };\n\n _this.handleRefInput = function (ref) {\n _this.inputRef = ref;\n true ? (0, _warning.default)(!ref || ref instanceof HTMLInputElement || ref.focus, ['Material-UI: you have provided a `inputComponent` to the input component', 'that does not correctly handle the `inputRef` property.', 'Make sure the `inputRef` property is called with a HTMLInputElement.'].join('\\n')) : undefined;\n var refProp;\n\n if (_this.props.inputRef) {\n refProp = _this.props.inputRef;\n } else if (_this.props.inputProps && _this.props.inputProps.ref) {\n refProp = _this.props.inputProps.ref;\n }\n\n (0, _reactHelpers.setRef)(refProp, ref);\n };\n\n _this.handleClick = function (event) {\n if (_this.inputRef && event.currentTarget === event.target) {\n _this.inputRef.focus();\n }\n\n if (_this.props.onClick) {\n _this.props.onClick(event);\n }\n };\n\n _this.isControlled = props.value != null;\n\n if (_this.isControlled) {\n _this.checkDirty(props);\n }\n\n return _this;\n }\n\n (0, _createClass2.default)(InputBase, [{\n key: \"componentDidMount\",\n value: function componentDidMount() {\n if (!this.isControlled) {\n this.checkDirty(this.inputRef);\n }\n }\n }, {\n key: \"componentDidUpdate\",\n value: function componentDidUpdate(prevProps) {\n // Book keep the focused state.\n if (!prevProps.disabled && this.props.disabled) {\n var muiFormControl = this.props.muiFormControl;\n\n if (muiFormControl && muiFormControl.onBlur) {\n muiFormControl.onBlur();\n }\n }\n\n if (this.isControlled) {\n this.checkDirty(this.props);\n } // else performed in the onChange\n\n }\n }, {\n key: \"checkDirty\",\n value: function checkDirty(obj) {\n var muiFormControl = this.props.muiFormControl;\n\n if ((0, _utils2.isFilled)(obj)) {\n if (muiFormControl && muiFormControl.onFilled) {\n muiFormControl.onFilled();\n }\n\n if (this.props.onFilled) {\n this.props.onFilled();\n }\n\n return;\n }\n\n if (muiFormControl && muiFormControl.onEmpty) {\n muiFormControl.onEmpty();\n }\n\n if (this.props.onEmpty) {\n this.props.onEmpty();\n }\n }\n }, {\n key: \"render\",\n value: function render() {\n var _classNames, _classNames2;\n\n var _this$props2 = this.props,\n autoComplete = _this$props2.autoComplete,\n autoFocus = _this$props2.autoFocus,\n classes = _this$props2.classes,\n classNameProp = _this$props2.className,\n defaultValue = _this$props2.defaultValue,\n disabled = _this$props2.disabled,\n endAdornment = _this$props2.endAdornment,\n error = _this$props2.error,\n fullWidth = _this$props2.fullWidth,\n id = _this$props2.id,\n inputComponent = _this$props2.inputComponent,\n _this$props2$inputPro = _this$props2.inputProps;\n _this$props2$inputPro = _this$props2$inputPro === void 0 ? {} : _this$props2$inputPro;\n var inputPropsClassName = _this$props2$inputPro.className,\n inputPropsProp = (0, _objectWithoutProperties2.default)(_this$props2$inputPro, [\"className\"]),\n inputRef = _this$props2.inputRef,\n margin = _this$props2.margin,\n muiFormControl = _this$props2.muiFormControl,\n multiline = _this$props2.multiline,\n name = _this$props2.name,\n onBlur = _this$props2.onBlur,\n onChange = _this$props2.onChange,\n onClick = _this$props2.onClick,\n onEmpty = _this$props2.onEmpty,\n onFilled = _this$props2.onFilled,\n onFocus = _this$props2.onFocus,\n onKeyDown = _this$props2.onKeyDown,\n onKeyUp = _this$props2.onKeyUp,\n placeholder = _this$props2.placeholder,\n readOnly = _this$props2.readOnly,\n renderPrefix = _this$props2.renderPrefix,\n rows = _this$props2.rows,\n rowsMax = _this$props2.rowsMax,\n startAdornment = _this$props2.startAdornment,\n type = _this$props2.type,\n value = _this$props2.value,\n other = (0, _objectWithoutProperties2.default)(_this$props2, [\"autoComplete\", \"autoFocus\", \"classes\", \"className\", \"defaultValue\", \"disabled\", \"endAdornment\", \"error\", \"fullWidth\", \"id\", \"inputComponent\", \"inputProps\", \"inputRef\", \"margin\", \"muiFormControl\", \"multiline\", \"name\", \"onBlur\", \"onChange\", \"onClick\", \"onEmpty\", \"onFilled\", \"onFocus\", \"onKeyDown\", \"onKeyUp\", \"placeholder\", \"readOnly\", \"renderPrefix\", \"rows\", \"rowsMax\", \"startAdornment\", \"type\", \"value\"]);\n var ariaDescribedby = other['aria-describedby'];\n delete other['aria-describedby'];\n var fcs = (0, _formControlState.default)({\n props: this.props,\n muiFormControl: muiFormControl,\n states: ['disabled', 'error', 'margin', 'required', 'filled']\n });\n var focused = muiFormControl ? muiFormControl.focused : this.state.focused;\n var className = (0, _classnames.default)(classes.root, (_classNames = {}, (0, _defineProperty2.default)(_classNames, classes.disabled, fcs.disabled), (0, _defineProperty2.default)(_classNames, classes.error, fcs.error), (0, _defineProperty2.default)(_classNames, classes.fullWidth, fullWidth), (0, _defineProperty2.default)(_classNames, classes.focused, focused), (0, _defineProperty2.default)(_classNames, classes.formControl, muiFormControl), (0, _defineProperty2.default)(_classNames, classes.marginDense, fcs.margin === 'dense'), (0, _defineProperty2.default)(_classNames, classes.multiline, multiline), (0, _defineProperty2.default)(_classNames, classes.adornedStart, startAdornment), (0, _defineProperty2.default)(_classNames, classes.adornedEnd, endAdornment), _classNames), classNameProp);\n var inputClassName = (0, _classnames.default)(classes.input, (_classNames2 = {}, (0, _defineProperty2.default)(_classNames2, classes.disabled, fcs.disabled), (0, _defineProperty2.default)(_classNames2, classes.inputType, type !== 'text'), (0, _defineProperty2.default)(_classNames2, classes.inputTypeSearch, type === 'search'), (0, _defineProperty2.default)(_classNames2, classes.inputMultiline, multiline), (0, _defineProperty2.default)(_classNames2, classes.inputMarginDense, fcs.margin === 'dense'), (0, _defineProperty2.default)(_classNames2, classes.inputAdornedStart, startAdornment), (0, _defineProperty2.default)(_classNames2, classes.inputAdornedEnd, endAdornment), _classNames2), inputPropsClassName);\n var InputComponent = inputComponent;\n var inputProps = (0, _extends2.default)({}, inputPropsProp, {\n ref: this.handleRefInput\n });\n\n if (typeof InputComponent !== 'string') {\n inputProps = (0, _extends2.default)({\n // Rename ref to inputRef as we don't know the\n // provided `inputComponent` structure.\n inputRef: this.handleRefInput,\n type: type\n }, inputProps, {\n ref: null\n });\n } else if (multiline) {\n if (rows && !rowsMax) {\n InputComponent = 'textarea';\n } else {\n inputProps = (0, _extends2.default)({\n rowsMax: rowsMax,\n textareaRef: this.handleRefInput\n }, inputProps, {\n ref: null\n });\n InputComponent = _Textarea.default;\n }\n } else {\n inputProps = (0, _extends2.default)({\n type: type\n }, inputProps);\n }\n\n return _react.default.createElement(\"div\", (0, _extends2.default)({\n className: className,\n onClick: this.handleClick\n }, other), renderPrefix ? renderPrefix((0, _extends2.default)({}, fcs, {\n startAdornment: startAdornment,\n focused: focused\n })) : null, startAdornment, _react.default.createElement(_FormControlContext.default.Provider, {\n value: null\n }, _react.default.createElement(InputComponent, (0, _extends2.default)({\n \"aria-invalid\": fcs.error,\n \"aria-describedby\": ariaDescribedby,\n autoComplete: autoComplete,\n autoFocus: autoFocus,\n className: inputClassName,\n defaultValue: defaultValue,\n disabled: fcs.disabled,\n id: id,\n name: name,\n onBlur: this.handleBlur,\n onChange: this.handleChange,\n onFocus: this.handleFocus,\n onKeyDown: onKeyDown,\n onKeyUp: onKeyUp,\n placeholder: placeholder,\n readOnly: readOnly,\n required: fcs.required,\n rows: rows,\n value: value\n }, inputProps))), endAdornment);\n }\n }]);\n return InputBase;\n}(_react.default.Component);\n\n true ? InputBase.propTypes = {\n /**\n * This property helps users to fill forms faster, especially on mobile devices.\n * The name can be confusing, as it's more like an autofill.\n * You can learn more about it here:\n * https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofill\n */\n autoComplete: _propTypes.default.string,\n\n /**\n * If `true`, the input will be focused during the first mount.\n */\n autoFocus: _propTypes.default.bool,\n\n /**\n * Override or extend the styles applied to the component.\n * See [CSS API](#css-api) below for more details.\n */\n classes: _propTypes.default.object.isRequired,\n\n /**\n * The CSS class name of the wrapper element.\n */\n className: _propTypes.default.string,\n\n /**\n * The default input value, useful when not controlling the component.\n */\n defaultValue: _propTypes.default.oneOfType([_propTypes.default.string, _propTypes.default.number, _propTypes.default.bool, _propTypes.default.object, _propTypes.default.arrayOf(_propTypes.default.oneOfType([_propTypes.default.string, _propTypes.default.number, _propTypes.default.bool, _propTypes.default.object]))]),\n\n /**\n * If `true`, the input will be disabled.\n */\n disabled: _propTypes.default.bool,\n\n /**\n * End `InputAdornment` for this component.\n */\n endAdornment: _propTypes.default.node,\n\n /**\n * If `true`, the input will indicate an error. This is normally obtained via context from\n * FormControl.\n */\n error: _propTypes.default.bool,\n\n /**\n * If `true`, the input will take up the full width of its container.\n */\n fullWidth: _propTypes.default.bool,\n\n /**\n * The id of the `input` element.\n */\n id: _propTypes.default.string,\n\n /**\n * The component used for the native input.\n * Either a string to use a DOM element or a component.\n */\n inputComponent: _utils.componentPropType,\n\n /**\n * Attributes applied to the `input` element.\n */\n inputProps: _propTypes.default.object,\n\n /**\n * Use that property to pass a ref callback to the native input component.\n */\n inputRef: _propTypes.default.oneOfType([_propTypes.default.func, _propTypes.default.object]),\n\n /**\n * If `dense`, will adjust vertical spacing. This is normally obtained via context from\n * FormControl.\n */\n margin: _propTypes.default.oneOf(['dense', 'none']),\n\n /**\n * @ignore\n */\n muiFormControl: _propTypes.default.object,\n\n /**\n * If `true`, a textarea element will be rendered.\n */\n multiline: _propTypes.default.bool,\n\n /**\n * Name attribute of the `input` element.\n */\n name: _propTypes.default.string,\n\n /**\n * @ignore\n */\n onBlur: _propTypes.default.func,\n\n /**\n * Callback fired when the value is changed.\n *\n * @param {object} event The event source of the callback.\n * You can pull out the new value by accessing `event.target.value`.\n */\n onChange: _propTypes.default.func,\n\n /**\n * @ignore\n */\n onClick: _propTypes.default.func,\n\n /**\n * @ignore\n */\n onEmpty: _propTypes.default.func,\n\n /**\n * @ignore\n */\n onFilled: _propTypes.default.func,\n\n /**\n * @ignore\n */\n onFocus: _propTypes.default.func,\n\n /**\n * @ignore\n */\n onKeyDown: _propTypes.default.func,\n\n /**\n * @ignore\n */\n onKeyUp: _propTypes.default.func,\n\n /**\n * The short hint displayed in the input before the user enters a value.\n */\n placeholder: _propTypes.default.string,\n\n /**\n * It prevents the user from changing the value of the field\n * (not from interacting with the field).\n */\n readOnly: _propTypes.default.bool,\n\n /**\n * @ignore\n */\n renderPrefix: _propTypes.default.func,\n\n /**\n * If `true`, the input will be required.\n */\n required: _propTypes.default.bool,\n\n /**\n * Number of rows to display when multiline option is set to true.\n */\n rows: _propTypes.default.oneOfType([_propTypes.default.string, _propTypes.default.number]),\n\n /**\n * Maximum number of rows to display when multiline option is set to true.\n */\n rowsMax: _propTypes.default.oneOfType([_propTypes.default.string, _propTypes.default.number]),\n\n /**\n * Start `InputAdornment` for this component.\n */\n startAdornment: _propTypes.default.node,\n\n /**\n * Type of the input element. It should be a valid HTML5 input type.\n */\n type: _propTypes.default.string,\n\n /**\n * The input value, required for a controlled component.\n */\n value: _propTypes.default.oneOfType([_propTypes.default.string, _propTypes.default.number, _propTypes.default.bool, _propTypes.default.object, _propTypes.default.arrayOf(_propTypes.default.oneOfType([_propTypes.default.string, _propTypes.default.number, _propTypes.default.bool, _propTypes.default.object]))])\n} : undefined;\nInputBase.defaultProps = {\n fullWidth: false,\n inputComponent: 'input',\n multiline: false,\n type: 'text'\n};\n\nvar _default = (0, _withStyles.default)(styles, {\n name: 'MuiInputBase'\n})((0, _withFormControlContext.default)(InputBase));\n\nexports.default = _default;\n\n//# sourceURL=webpack:///./node_modules/@material-ui/core/InputBase/InputBase.js?"); - - /***/ }), - - /***/ "./node_modules/@material-ui/core/InputBase/Textarea.js": - /*!**************************************************************!*\ - !*** ./node_modules/@material-ui/core/InputBase/Textarea.js ***! - \**************************************************************/ - /*! no static exports found */ - /***/ (function(module, exports, __webpack_require__) { - - "use strict"; - eval("\n\nvar _interopRequireDefault = __webpack_require__(/*! @babel/runtime/helpers/interopRequireDefault */ \"./node_modules/@babel/runtime/helpers/interopRequireDefault.js\");\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.default = exports.styles = void 0;\n\nvar _extends2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/extends */ \"./node_modules/@babel/runtime/helpers/extends.js\"));\n\nvar _objectWithoutProperties2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/objectWithoutProperties */ \"./node_modules/@babel/runtime/helpers/objectWithoutProperties.js\"));\n\nvar _classCallCheck2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/classCallCheck */ \"./node_modules/@babel/runtime/helpers/classCallCheck.js\"));\n\nvar _createClass2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/createClass */ \"./node_modules/@babel/runtime/helpers/createClass.js\"));\n\nvar _possibleConstructorReturn2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/possibleConstructorReturn */ \"./node_modules/@babel/runtime/helpers/possibleConstructorReturn.js\"));\n\nvar _getPrototypeOf2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/getPrototypeOf */ \"./node_modules/@babel/runtime/helpers/getPrototypeOf.js\"));\n\nvar _inherits2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/inherits */ \"./node_modules/@babel/runtime/helpers/inherits.js\"));\n\nvar _react = _interopRequireDefault(__webpack_require__(/*! react */ \"./node_modules/react/index.js\"));\n\nvar _propTypes = _interopRequireDefault(__webpack_require__(/*! prop-types */ \"./node_modules/prop-types/index.js\"));\n\nvar _classnames = _interopRequireDefault(__webpack_require__(/*! classnames */ \"./node_modules/classnames/index.js\"));\n\nvar _debounce = _interopRequireDefault(__webpack_require__(/*! debounce */ \"./node_modules/debounce/index.js\"));\n\nvar _reactEventListener = _interopRequireDefault(__webpack_require__(/*! react-event-listener */ \"./node_modules/react-event-listener/dist/react-event-listener.cjs.js\"));\n\nvar _withStyles = _interopRequireDefault(__webpack_require__(/*! ../styles/withStyles */ \"./node_modules/@material-ui/core/styles/withStyles.js\"));\n\nvar _reactHelpers = __webpack_require__(/*! ../utils/reactHelpers */ \"./node_modules/@material-ui/core/utils/reactHelpers.js\");\n\n// < 1kb payload overhead when lodash/debounce is > 3kb.\nvar ROWS_HEIGHT = 19;\nvar styles = {\n /* Styles applied to the root element. */\n root: {\n position: 'relative',\n // because the shadow has position: 'absolute',\n width: '100%'\n },\n textarea: {\n width: '100%',\n height: '100%',\n resize: 'none',\n font: 'inherit',\n padding: 0,\n cursor: 'inherit',\n boxSizing: 'border-box',\n lineHeight: 'inherit',\n border: 'none',\n outline: 'none',\n background: 'transparent'\n },\n shadow: {\n // Overflow also needed to here to remove the extra row\n // added to textareas in Firefox.\n overflow: 'hidden',\n // Visibility needed to hide the extra text area on iPads\n visibility: 'hidden',\n position: 'absolute',\n height: 'auto',\n whiteSpace: 'pre-wrap'\n }\n};\n/**\n * @ignore - internal component.\n */\n\nexports.styles = styles;\n\nvar Textarea =\n/*#__PURE__*/\nfunction (_React$Component) {\n (0, _inherits2.default)(Textarea, _React$Component);\n\n function Textarea(props) {\n var _this;\n\n (0, _classCallCheck2.default)(this, Textarea);\n _this = (0, _possibleConstructorReturn2.default)(this, (0, _getPrototypeOf2.default)(Textarea).call(this));\n\n _this.handleRefInput = function (ref) {\n _this.inputRef = ref;\n (0, _reactHelpers.setRef)(_this.props.textareaRef, ref);\n };\n\n _this.handleRefSinglelineShadow = function (ref) {\n _this.singlelineShadowRef = ref;\n };\n\n _this.handleRefShadow = function (ref) {\n _this.shadowRef = ref;\n };\n\n _this.handleChange = function (event) {\n _this.value = event.target.value;\n\n if (!_this.isControlled) {\n // The component is not controlled, we need to update the shallow value.\n _this.shadowRef.value = _this.value;\n\n _this.syncHeightWithShadow();\n }\n\n if (_this.props.onChange) {\n _this.props.onChange(event);\n }\n };\n\n _this.isControlled = props.value != null; // expects the components it renders to respond to 'value'\n // so that it can check whether they are filled.\n\n _this.value = props.value || props.defaultValue || '';\n _this.state = {\n height: Number(props.rows) * ROWS_HEIGHT\n };\n\n if (typeof window !== 'undefined') {\n _this.handleResize = (0, _debounce.default)(function () {\n _this.syncHeightWithShadow();\n }, 166); // Corresponds to 10 frames at 60 Hz.\n }\n\n return _this;\n }\n\n (0, _createClass2.default)(Textarea, [{\n key: \"componentDidMount\",\n value: function componentDidMount() {\n this.syncHeightWithShadow();\n }\n }, {\n key: \"componentDidUpdate\",\n value: function componentDidUpdate() {\n this.syncHeightWithShadow();\n }\n }, {\n key: \"componentWillUnmount\",\n value: function componentWillUnmount() {\n this.handleResize.clear();\n }\n }, {\n key: \"syncHeightWithShadow\",\n value: function syncHeightWithShadow() {\n var props = this.props; // Guarding for **broken** shallow rendering method that call componentDidMount\n // but doesn't handle refs correctly.\n // To remove once the shallow rendering has been fixed.\n\n if (!this.shadowRef) {\n return;\n }\n\n if (this.isControlled) {\n // The component is controlled, we need to update the shallow value.\n this.shadowRef.value = props.value == null ? '' : String(props.value);\n }\n\n var lineHeight = this.singlelineShadowRef.scrollHeight; // The Textarea might not be visible (p.ex: display: none).\n // In this case, the layout values read from the DOM will be 0.\n\n lineHeight = lineHeight === 0 ? ROWS_HEIGHT : lineHeight;\n var newHeight = this.shadowRef.scrollHeight; // Guarding for jsdom, where scrollHeight isn't present.\n // See https://github.com/tmpvar/jsdom/issues/1013\n\n if (newHeight === undefined) {\n return;\n }\n\n if (Number(props.rowsMax) >= Number(props.rows)) {\n newHeight = Math.min(Number(props.rowsMax) * lineHeight, newHeight);\n }\n\n newHeight = Math.max(newHeight, lineHeight); // Need a large enough different to update the height.\n // This prevents infinite rendering loop.\n\n if (Math.abs(this.state.height - newHeight) > 1) {\n this.setState({\n height: newHeight\n });\n }\n }\n }, {\n key: \"render\",\n value: function render() {\n var _this$props = this.props,\n classes = _this$props.classes,\n className = _this$props.className,\n defaultValue = _this$props.defaultValue,\n onChange = _this$props.onChange,\n rows = _this$props.rows,\n rowsMax = _this$props.rowsMax,\n style = _this$props.style,\n textareaRef = _this$props.textareaRef,\n value = _this$props.value,\n other = (0, _objectWithoutProperties2.default)(_this$props, [\"classes\", \"className\", \"defaultValue\", \"onChange\", \"rows\", \"rowsMax\", \"style\", \"textareaRef\", \"value\"]);\n return _react.default.createElement(\"div\", {\n className: classes.root\n }, _react.default.createElement(_reactEventListener.default, {\n target: \"window\",\n onResize: this.handleResize\n }), _react.default.createElement(\"textarea\", {\n \"aria-hidden\": \"true\",\n className: (0, _classnames.default)(classes.textarea, classes.shadow),\n readOnly: true,\n ref: this.handleRefSinglelineShadow,\n rows: \"1\",\n tabIndex: -1,\n value: \"\"\n }), _react.default.createElement(\"textarea\", {\n \"aria-hidden\": \"true\",\n className: (0, _classnames.default)(classes.textarea, classes.shadow),\n defaultValue: defaultValue,\n readOnly: true,\n ref: this.handleRefShadow,\n rows: rows,\n tabIndex: -1,\n value: value\n }), _react.default.createElement(\"textarea\", (0, _extends2.default)({\n rows: rows,\n className: (0, _classnames.default)(classes.textarea, className),\n defaultValue: defaultValue,\n value: value,\n onChange: this.handleChange,\n ref: this.handleRefInput,\n style: (0, _extends2.default)({\n height: this.state.height\n }, style)\n }, other)));\n }\n }]);\n return Textarea;\n}(_react.default.Component);\n\n true ? Textarea.propTypes = {\n /**\n * Override or extend the styles applied to the component.\n * See [CSS API](#css-api) below for more details.\n */\n classes: _propTypes.default.object.isRequired,\n\n /**\n * @ignore\n */\n className: _propTypes.default.string,\n\n /**\n * @ignore\n */\n defaultValue: _propTypes.default.oneOfType([_propTypes.default.string, _propTypes.default.number]),\n\n /**\n * @ignore\n */\n disabled: _propTypes.default.bool,\n\n /**\n * @ignore\n */\n onChange: _propTypes.default.func,\n\n /**\n * Number of rows to display when multiline option is set to true.\n */\n rows: _propTypes.default.oneOfType([_propTypes.default.string, _propTypes.default.number]),\n\n /**\n * Maximum number of rows to display when multiline option is set to true.\n */\n rowsMax: _propTypes.default.oneOfType([_propTypes.default.string, _propTypes.default.number]),\n\n /**\n * @ignore\n */\n style: _propTypes.default.object,\n\n /**\n * Use that property to pass a ref callback to the native textarea element.\n */\n textareaRef: _propTypes.default.oneOfType([_propTypes.default.func, _propTypes.default.object]),\n\n /**\n * @ignore\n */\n value: _propTypes.default.oneOfType([_propTypes.default.string, _propTypes.default.number])\n} : undefined;\nTextarea.defaultProps = {\n rows: 1\n};\n\nvar _default = (0, _withStyles.default)(styles, {\n name: 'MuiPrivateTextarea'\n})(Textarea);\n\nexports.default = _default;\n\n//# sourceURL=webpack:///./node_modules/@material-ui/core/InputBase/Textarea.js?"); - - /***/ }), - - /***/ "./node_modules/@material-ui/core/InputBase/index.js": - /*!***********************************************************!*\ - !*** ./node_modules/@material-ui/core/InputBase/index.js ***! - \***********************************************************/ - /*! no static exports found */ - /***/ (function(module, exports, __webpack_require__) { - - "use strict"; - eval("\n\nvar _interopRequireDefault = __webpack_require__(/*! @babel/runtime/helpers/interopRequireDefault */ \"./node_modules/@babel/runtime/helpers/interopRequireDefault.js\");\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nObject.defineProperty(exports, \"default\", {\n enumerable: true,\n get: function get() {\n return _InputBase.default;\n }\n});\n\nvar _InputBase = _interopRequireDefault(__webpack_require__(/*! ./InputBase */ \"./node_modules/@material-ui/core/InputBase/InputBase.js\"));\n\n//# sourceURL=webpack:///./node_modules/@material-ui/core/InputBase/index.js?"); - - /***/ }), - - /***/ "./node_modules/@material-ui/core/InputBase/utils.js": - /*!***********************************************************!*\ - !*** ./node_modules/@material-ui/core/InputBase/utils.js ***! - \***********************************************************/ - /*! no static exports found */ - /***/ (function(module, exports, __webpack_require__) { - - "use strict"; - eval("\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.hasValue = hasValue;\nexports.isFilled = isFilled;\nexports.isAdornedStart = isAdornedStart;\n\n// Supports determination of isControlled().\n// Controlled input accepts its current value as a prop.\n//\n// @see https://facebook.github.io/react/docs/forms.html#controlled-components\n// @param value\n// @returns {boolean} true if string (including '') or number (including zero)\nfunction hasValue(value) {\n return value != null && !(Array.isArray(value) && value.length === 0);\n} // Determine if field is empty or filled.\n// Response determines if label is presented above field or as placeholder.\n//\n// @param obj\n// @param SSR\n// @returns {boolean} False when not present or empty string.\n// True when any number or string with length.\n\n\nfunction isFilled(obj) {\n var SSR = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;\n return obj && (hasValue(obj.value) && obj.value !== '' || SSR && hasValue(obj.defaultValue) && obj.defaultValue !== '');\n} // Determine if an Input is adorned on start.\n// It's corresponding to the left with LTR.\n//\n// @param obj\n// @returns {boolean} False when no adornments.\n// True when adorned at the start.\n\n\nfunction isAdornedStart(obj) {\n return obj.startAdornment;\n}\n\n//# sourceURL=webpack:///./node_modules/@material-ui/core/InputBase/utils.js?"); - - /***/ }), - - /***/ "./node_modules/@material-ui/core/InputLabel/InputLabel.js": - /*!*****************************************************************!*\ - !*** ./node_modules/@material-ui/core/InputLabel/InputLabel.js ***! - \*****************************************************************/ - /*! no static exports found */ - /***/ (function(module, exports, __webpack_require__) { - - "use strict"; - eval("\n\nvar _interopRequireDefault = __webpack_require__(/*! @babel/runtime/helpers/interopRequireDefault */ \"./node_modules/@babel/runtime/helpers/interopRequireDefault.js\");\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.default = exports.styles = void 0;\n\nvar _extends2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/extends */ \"./node_modules/@babel/runtime/helpers/extends.js\"));\n\nvar _defineProperty2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/defineProperty */ \"./node_modules/@babel/runtime/helpers/defineProperty.js\"));\n\nvar _objectWithoutProperties2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/objectWithoutProperties */ \"./node_modules/@babel/runtime/helpers/objectWithoutProperties.js\"));\n\nvar _react = _interopRequireDefault(__webpack_require__(/*! react */ \"./node_modules/react/index.js\"));\n\nvar _propTypes = _interopRequireDefault(__webpack_require__(/*! prop-types */ \"./node_modules/prop-types/index.js\"));\n\nvar _classnames = _interopRequireDefault(__webpack_require__(/*! classnames */ \"./node_modules/classnames/index.js\"));\n\nvar _formControlState = _interopRequireDefault(__webpack_require__(/*! ../FormControl/formControlState */ \"./node_modules/@material-ui/core/FormControl/formControlState.js\"));\n\nvar _withFormControlContext = _interopRequireDefault(__webpack_require__(/*! ../FormControl/withFormControlContext */ \"./node_modules/@material-ui/core/FormControl/withFormControlContext.js\"));\n\nvar _withStyles = _interopRequireDefault(__webpack_require__(/*! ../styles/withStyles */ \"./node_modules/@material-ui/core/styles/withStyles.js\"));\n\nvar _FormLabel = _interopRequireDefault(__webpack_require__(/*! ../FormLabel */ \"./node_modules/@material-ui/core/FormLabel/index.js\"));\n\n// @inheritedComponent FormLabel\nvar styles = function styles(theme) {\n return {\n /* Styles applied to the root element. */\n root: {\n transformOrigin: 'top left'\n },\n\n /* Styles applied to the root element if `focused={true}`. */\n focused: {},\n\n /* Styles applied to the root element if `disabled={true}`. */\n disabled: {},\n\n /* Styles applied to the root element if `error={true}`. */\n error: {},\n\n /* Styles applied to the root element if `required={true}`. */\n required: {},\n\n /* Styles applied to the root element if the component is a descendant of `FormControl`. */\n formControl: {\n position: 'absolute',\n left: 0,\n top: 0,\n // slight alteration to spec spacing to match visual spec result\n transform: 'translate(0, 24px) scale(1)'\n },\n\n /* Styles applied to the root element if `margin=\"dense\"`. */\n marginDense: {\n // Compensation for the `Input.inputDense` style.\n transform: 'translate(0, 21px) scale(1)'\n },\n\n /* Styles applied to the `input` element if `shrink={true}`. */\n shrink: {\n transform: 'translate(0, 1.5px) scale(0.75)',\n transformOrigin: 'top left'\n },\n\n /* Styles applied to the `input` element if `disableAnimation={false}`. */\n animated: {\n transition: theme.transitions.create(['color', 'transform'], {\n duration: theme.transitions.duration.shorter,\n easing: theme.transitions.easing.easeOut\n })\n },\n\n /* Styles applied to the root element if `variant=\"filled\"`. */\n filled: {\n // Chrome's autofill feature gives the input field a yellow background.\n // Since the input field is behind the label in the HTML tree,\n // the input field is drawn last and hides the label with an opaque background color.\n // zIndex: 1 will raise the label above opaque background-colors of input.\n zIndex: 1,\n pointerEvents: 'none',\n transform: 'translate(12px, 20px) scale(1)',\n '&$marginDense': {\n transform: 'translate(12px, 17px) scale(1)'\n },\n '&$shrink': {\n transform: 'translate(12px, 10px) scale(0.75)',\n '&$marginDense': {\n transform: 'translate(12px, 7px) scale(0.75)'\n }\n }\n },\n\n /* Styles applied to the root element if `variant=\"outlined\"`. */\n outlined: {\n // see comment above on filled.zIndex\n zIndex: 1,\n pointerEvents: 'none',\n transform: 'translate(14px, 20px) scale(1)',\n '&$marginDense': {\n transform: 'translate(14px, 17px) scale(1)'\n },\n '&$shrink': {\n transform: 'translate(14px, -6px) scale(0.75)'\n }\n }\n };\n};\n\nexports.styles = styles;\n\nfunction InputLabel(props) {\n var _classNames;\n\n var children = props.children,\n classes = props.classes,\n classNameProp = props.className,\n disableAnimation = props.disableAnimation,\n FormLabelClasses = props.FormLabelClasses,\n margin = props.margin,\n muiFormControl = props.muiFormControl,\n shrinkProp = props.shrink,\n variant = props.variant,\n other = (0, _objectWithoutProperties2.default)(props, [\"children\", \"classes\", \"className\", \"disableAnimation\", \"FormLabelClasses\", \"margin\", \"muiFormControl\", \"shrink\", \"variant\"]);\n var shrink = shrinkProp;\n\n if (typeof shrink === 'undefined' && muiFormControl) {\n shrink = muiFormControl.filled || muiFormControl.focused || muiFormControl.adornedStart;\n }\n\n var fcs = (0, _formControlState.default)({\n props: props,\n muiFormControl: muiFormControl,\n states: ['margin', 'variant']\n });\n var className = (0, _classnames.default)(classes.root, (_classNames = {}, (0, _defineProperty2.default)(_classNames, classes.formControl, muiFormControl), (0, _defineProperty2.default)(_classNames, classes.animated, !disableAnimation), (0, _defineProperty2.default)(_classNames, classes.shrink, shrink), (0, _defineProperty2.default)(_classNames, classes.marginDense, fcs.margin === 'dense'), (0, _defineProperty2.default)(_classNames, classes.filled, fcs.variant === 'filled'), (0, _defineProperty2.default)(_classNames, classes.outlined, fcs.variant === 'outlined'), _classNames), classNameProp);\n return _react.default.createElement(_FormLabel.default, (0, _extends2.default)({\n \"data-shrink\": shrink,\n className: className,\n classes: (0, _extends2.default)({\n focused: classes.focused,\n disabled: classes.disabled,\n error: classes.error,\n required: classes.required\n }, FormLabelClasses)\n }, other), children);\n}\n\n true ? InputLabel.propTypes = {\n /**\n * The contents of the `InputLabel`.\n */\n children: _propTypes.default.node,\n\n /**\n * Override or extend the styles applied to the component.\n * See [CSS API](#css-api) below for more details.\n */\n classes: _propTypes.default.object.isRequired,\n\n /**\n * @ignore\n */\n className: _propTypes.default.string,\n\n /**\n * If `true`, the transition animation is disabled.\n */\n disableAnimation: _propTypes.default.bool,\n\n /**\n * If `true`, apply disabled class.\n */\n disabled: _propTypes.default.bool,\n\n /**\n * If `true`, the label will be displayed in an error state.\n */\n error: _propTypes.default.bool,\n\n /**\n * If `true`, the input of this label is focused.\n */\n focused: _propTypes.default.bool,\n\n /**\n * `classes` property applied to the [`FormLabel`](/api/form-label/) element.\n */\n FormLabelClasses: _propTypes.default.object,\n\n /**\n * If `dense`, will adjust vertical spacing. This is normally obtained via context from\n * FormControl.\n */\n margin: _propTypes.default.oneOf(['dense']),\n\n /**\n * @ignore\n */\n muiFormControl: _propTypes.default.object,\n\n /**\n * if `true`, the label will indicate that the input is required.\n */\n required: _propTypes.default.bool,\n\n /**\n * If `true`, the label is shrunk.\n */\n shrink: _propTypes.default.bool,\n\n /**\n * The variant to use.\n */\n variant: _propTypes.default.oneOf(['standard', 'outlined', 'filled'])\n} : undefined;\nInputLabel.defaultProps = {\n disableAnimation: false\n};\n\nvar _default = (0, _withStyles.default)(styles, {\n name: 'MuiInputLabel'\n})((0, _withFormControlContext.default)(InputLabel));\n\nexports.default = _default;\n\n//# sourceURL=webpack:///./node_modules/@material-ui/core/InputLabel/InputLabel.js?"); - - /***/ }), - - /***/ "./node_modules/@material-ui/core/InputLabel/index.js": - /*!************************************************************!*\ - !*** ./node_modules/@material-ui/core/InputLabel/index.js ***! - \************************************************************/ - /*! no static exports found */ - /***/ (function(module, exports, __webpack_require__) { - - "use strict"; - eval("\n\nvar _interopRequireDefault = __webpack_require__(/*! @babel/runtime/helpers/interopRequireDefault */ \"./node_modules/@babel/runtime/helpers/interopRequireDefault.js\");\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nObject.defineProperty(exports, \"default\", {\n enumerable: true,\n get: function get() {\n return _InputLabel.default;\n }\n});\n\nvar _InputLabel = _interopRequireDefault(__webpack_require__(/*! ./InputLabel */ \"./node_modules/@material-ui/core/InputLabel/InputLabel.js\"));\n\n//# sourceURL=webpack:///./node_modules/@material-ui/core/InputLabel/index.js?"); - - /***/ }), - - /***/ "./node_modules/@material-ui/core/List/List.js": - /*!*****************************************************!*\ - !*** ./node_modules/@material-ui/core/List/List.js ***! - \*****************************************************/ - /*! no static exports found */ - /***/ (function(module, exports, __webpack_require__) { - - "use strict"; - eval("\n\nvar _interopRequireDefault = __webpack_require__(/*! @babel/runtime/helpers/interopRequireDefault */ \"./node_modules/@babel/runtime/helpers/interopRequireDefault.js\");\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.default = exports.styles = void 0;\n\nvar _extends2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/extends */ \"./node_modules/@babel/runtime/helpers/extends.js\"));\n\nvar _defineProperty2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/defineProperty */ \"./node_modules/@babel/runtime/helpers/defineProperty.js\"));\n\nvar _objectWithoutProperties2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/objectWithoutProperties */ \"./node_modules/@babel/runtime/helpers/objectWithoutProperties.js\"));\n\nvar _react = _interopRequireDefault(__webpack_require__(/*! react */ \"./node_modules/react/index.js\"));\n\nvar _propTypes = _interopRequireDefault(__webpack_require__(/*! prop-types */ \"./node_modules/prop-types/index.js\"));\n\nvar _classnames = _interopRequireDefault(__webpack_require__(/*! classnames */ \"./node_modules/classnames/index.js\"));\n\nvar _utils = __webpack_require__(/*! @material-ui/utils */ \"./node_modules/@material-ui/utils/index.es.js\");\n\nvar _withStyles = _interopRequireDefault(__webpack_require__(/*! ../styles/withStyles */ \"./node_modules/@material-ui/core/styles/withStyles.js\"));\n\nvar _ListContext = _interopRequireDefault(__webpack_require__(/*! ./ListContext */ \"./node_modules/@material-ui/core/List/ListContext.js\"));\n\nvar styles = {\n /* Styles applied to the root element. */\n root: {\n listStyle: 'none',\n margin: 0,\n padding: 0,\n position: 'relative'\n },\n\n /* Styles applied to the root element if `disablePadding={false}`. */\n padding: {\n paddingTop: 8,\n paddingBottom: 8\n },\n\n /* Styles applied to the root element if `dense={true}` & `disablePadding={false}`. */\n dense: {\n paddingTop: 4,\n paddingBottom: 4\n },\n\n /* Styles applied to the root element if a `subheader` is provided. */\n subheader: {\n paddingTop: 0\n }\n};\nexports.styles = styles;\n\nfunction List(props) {\n var _classNames;\n\n var children = props.children,\n classes = props.classes,\n className = props.className,\n Component = props.component,\n dense = props.dense,\n disablePadding = props.disablePadding,\n subheader = props.subheader,\n other = (0, _objectWithoutProperties2.default)(props, [\"children\", \"classes\", \"className\", \"component\", \"dense\", \"disablePadding\", \"subheader\"]);\n return _react.default.createElement(Component, (0, _extends2.default)({\n className: (0, _classnames.default)(classes.root, (_classNames = {}, (0, _defineProperty2.default)(_classNames, classes.dense, dense && !disablePadding), (0, _defineProperty2.default)(_classNames, classes.padding, !disablePadding), (0, _defineProperty2.default)(_classNames, classes.subheader, subheader), _classNames), className)\n }, other), _react.default.createElement(_ListContext.default.Provider, {\n value: {\n dense: dense\n }\n }, subheader, children));\n}\n\n true ? List.propTypes = {\n /**\n * The content of the component.\n */\n children: _propTypes.default.node,\n\n /**\n * Override or extend the styles applied to the component.\n * See [CSS API](#css-api) below for more details.\n */\n classes: _propTypes.default.object.isRequired,\n\n /**\n * @ignore\n */\n className: _propTypes.default.string,\n\n /**\n * The component used for the root node.\n * Either a string to use a DOM element or a component.\n */\n component: _utils.componentPropType,\n\n /**\n * If `true`, compact vertical padding designed for keyboard and mouse input will be used for\n * the list and list items. The property is available to descendant components as the\n * `dense` context.\n */\n dense: _propTypes.default.bool,\n\n /**\n * If `true`, vertical padding will be removed from the list.\n */\n disablePadding: _propTypes.default.bool,\n\n /**\n * The content of the subheader, normally `ListSubheader`.\n */\n subheader: _propTypes.default.node\n} : undefined;\nList.defaultProps = {\n component: 'ul',\n dense: false,\n disablePadding: false\n};\n\nvar _default = (0, _withStyles.default)(styles, {\n name: 'MuiList'\n})(List);\n\nexports.default = _default;\n\n//# sourceURL=webpack:///./node_modules/@material-ui/core/List/List.js?"); - - /***/ }), - - /***/ "./node_modules/@material-ui/core/List/ListContext.js": - /*!************************************************************!*\ - !*** ./node_modules/@material-ui/core/List/ListContext.js ***! - \************************************************************/ - /*! no static exports found */ - /***/ (function(module, exports, __webpack_require__) { - - "use strict"; - eval("\n\nvar _interopRequireDefault = __webpack_require__(/*! @babel/runtime/helpers/interopRequireDefault */ \"./node_modules/@babel/runtime/helpers/interopRequireDefault.js\");\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.default = void 0;\n\nvar _react = _interopRequireDefault(__webpack_require__(/*! react */ \"./node_modules/react/index.js\"));\n\n/**\n * @ignore - internal component.\n */\nvar ListContext = _react.default.createContext({});\n\nvar _default = ListContext;\nexports.default = _default;\n\n//# sourceURL=webpack:///./node_modules/@material-ui/core/List/ListContext.js?"); - - /***/ }), - - /***/ "./node_modules/@material-ui/core/List/index.js": - /*!******************************************************!*\ - !*** ./node_modules/@material-ui/core/List/index.js ***! - \******************************************************/ - /*! no static exports found */ - /***/ (function(module, exports, __webpack_require__) { - - "use strict"; - eval("\n\nvar _interopRequireDefault = __webpack_require__(/*! @babel/runtime/helpers/interopRequireDefault */ \"./node_modules/@babel/runtime/helpers/interopRequireDefault.js\");\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nObject.defineProperty(exports, \"default\", {\n enumerable: true,\n get: function get() {\n return _List.default;\n }\n});\n\nvar _List = _interopRequireDefault(__webpack_require__(/*! ./List */ \"./node_modules/@material-ui/core/List/List.js\"));\n\n//# sourceURL=webpack:///./node_modules/@material-ui/core/List/index.js?"); - - /***/ }), - - /***/ "./node_modules/@material-ui/core/Menu/Menu.js": - /*!*****************************************************!*\ - !*** ./node_modules/@material-ui/core/Menu/Menu.js ***! - \*****************************************************/ - /*! no static exports found */ - /***/ (function(module, exports, __webpack_require__) { - - "use strict"; - eval("\n\nvar _interopRequireDefault = __webpack_require__(/*! @babel/runtime/helpers/interopRequireDefault */ \"./node_modules/@babel/runtime/helpers/interopRequireDefault.js\");\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.default = exports.styles = void 0;\n\nvar _extends2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/extends */ \"./node_modules/@babel/runtime/helpers/extends.js\"));\n\nvar _objectWithoutProperties2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/objectWithoutProperties */ \"./node_modules/@babel/runtime/helpers/objectWithoutProperties.js\"));\n\nvar _classCallCheck2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/classCallCheck */ \"./node_modules/@babel/runtime/helpers/classCallCheck.js\"));\n\nvar _createClass2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/createClass */ \"./node_modules/@babel/runtime/helpers/createClass.js\"));\n\nvar _possibleConstructorReturn2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/possibleConstructorReturn */ \"./node_modules/@babel/runtime/helpers/possibleConstructorReturn.js\"));\n\nvar _getPrototypeOf3 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/getPrototypeOf */ \"./node_modules/@babel/runtime/helpers/getPrototypeOf.js\"));\n\nvar _inherits2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/inherits */ \"./node_modules/@babel/runtime/helpers/inherits.js\"));\n\nvar _react = _interopRequireDefault(__webpack_require__(/*! react */ \"./node_modules/react/index.js\"));\n\nvar _propTypes = _interopRequireDefault(__webpack_require__(/*! prop-types */ \"./node_modules/prop-types/index.js\"));\n\nvar _reactDom = _interopRequireDefault(__webpack_require__(/*! react-dom */ \"./node_modules/react-dom/index.js\"));\n\nvar _scrollbarSize = _interopRequireDefault(__webpack_require__(/*! dom-helpers/util/scrollbarSize */ \"./node_modules/dom-helpers/util/scrollbarSize.js\"));\n\nvar _withStyles = _interopRequireDefault(__webpack_require__(/*! ../styles/withStyles */ \"./node_modules/@material-ui/core/styles/withStyles.js\"));\n\nvar _Popover = _interopRequireDefault(__webpack_require__(/*! ../Popover */ \"./node_modules/@material-ui/core/Popover/index.js\"));\n\nvar _MenuList = _interopRequireDefault(__webpack_require__(/*! ../MenuList */ \"./node_modules/@material-ui/core/MenuList/index.js\"));\n\n// @inheritedComponent Popover\nvar RTL_ORIGIN = {\n vertical: 'top',\n horizontal: 'right'\n};\nvar LTR_ORIGIN = {\n vertical: 'top',\n horizontal: 'left'\n};\nvar styles = {\n /* Styles applied to the `Paper` component. */\n paper: {\n // specZ: The maximum height of a simple menu should be one or more rows less than the view\n // height. This ensures a tapable area outside of the simple menu with which to dismiss\n // the menu.\n maxHeight: 'calc(100% - 96px)',\n // Add iOS momentum scrolling.\n WebkitOverflowScrolling: 'touch'\n }\n};\nexports.styles = styles;\n\nvar Menu =\n/*#__PURE__*/\nfunction (_React$Component) {\n (0, _inherits2.default)(Menu, _React$Component);\n\n function Menu() {\n var _getPrototypeOf2;\n\n var _this;\n\n (0, _classCallCheck2.default)(this, Menu);\n\n for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {\n args[_key] = arguments[_key];\n }\n\n _this = (0, _possibleConstructorReturn2.default)(this, (_getPrototypeOf2 = (0, _getPrototypeOf3.default)(Menu)).call.apply(_getPrototypeOf2, [this].concat(args)));\n\n _this.getContentAnchorEl = function () {\n if (_this.menuListRef.selectedItemRef) {\n return _reactDom.default.findDOMNode(_this.menuListRef.selectedItemRef);\n }\n\n return _reactDom.default.findDOMNode(_this.menuListRef).firstChild;\n };\n\n _this.focus = function () {\n if (_this.menuListRef && _this.menuListRef.selectedItemRef) {\n _reactDom.default.findDOMNode(_this.menuListRef.selectedItemRef).focus();\n\n return;\n }\n\n var menuList = _reactDom.default.findDOMNode(_this.menuListRef);\n\n if (menuList && menuList.firstChild) {\n menuList.firstChild.focus();\n }\n };\n\n _this.handleMenuListRef = function (ref) {\n _this.menuListRef = ref;\n };\n\n _this.handleEntering = function (element) {\n var _this$props = _this.props,\n disableAutoFocusItem = _this$props.disableAutoFocusItem,\n theme = _this$props.theme;\n\n var menuList = _reactDom.default.findDOMNode(_this.menuListRef); // Focus so the scroll computation of the Popover works as expected.\n\n\n if (disableAutoFocusItem !== true) {\n _this.focus();\n } // Let's ignore that piece of logic if users are already overriding the width\n // of the menu.\n\n\n if (menuList && element.clientHeight < menuList.clientHeight && !menuList.style.width) {\n var size = \"\".concat((0, _scrollbarSize.default)(), \"px\");\n menuList.style[theme.direction === 'rtl' ? 'paddingLeft' : 'paddingRight'] = size;\n menuList.style.width = \"calc(100% + \".concat(size, \")\");\n }\n\n if (_this.props.onEntering) {\n _this.props.onEntering(element);\n }\n };\n\n _this.handleListKeyDown = function (event) {\n if (event.key === 'Tab') {\n event.preventDefault();\n\n if (_this.props.onClose) {\n _this.props.onClose(event, 'tabKeyDown');\n }\n }\n };\n\n return _this;\n }\n\n (0, _createClass2.default)(Menu, [{\n key: \"componentDidMount\",\n value: function componentDidMount() {\n if (this.props.open && this.props.disableAutoFocusItem !== true) {\n this.focus();\n }\n }\n }, {\n key: \"render\",\n value: function render() {\n var _this$props2 = this.props,\n children = _this$props2.children,\n classes = _this$props2.classes,\n disableAutoFocusItem = _this$props2.disableAutoFocusItem,\n MenuListProps = _this$props2.MenuListProps,\n onEntering = _this$props2.onEntering,\n _this$props2$PaperPro = _this$props2.PaperProps,\n PaperProps = _this$props2$PaperPro === void 0 ? {} : _this$props2$PaperPro,\n PopoverClasses = _this$props2.PopoverClasses,\n theme = _this$props2.theme,\n other = (0, _objectWithoutProperties2.default)(_this$props2, [\"children\", \"classes\", \"disableAutoFocusItem\", \"MenuListProps\", \"onEntering\", \"PaperProps\", \"PopoverClasses\", \"theme\"]);\n return _react.default.createElement(_Popover.default, (0, _extends2.default)({\n getContentAnchorEl: this.getContentAnchorEl,\n classes: PopoverClasses,\n onEntering: this.handleEntering,\n anchorOrigin: theme.direction === 'rtl' ? RTL_ORIGIN : LTR_ORIGIN,\n transformOrigin: theme.direction === 'rtl' ? RTL_ORIGIN : LTR_ORIGIN,\n PaperProps: (0, _extends2.default)({}, PaperProps, {\n classes: (0, _extends2.default)({}, PaperProps.classes, {\n root: classes.paper\n })\n })\n }, other), _react.default.createElement(_MenuList.default, (0, _extends2.default)({\n onKeyDown: this.handleListKeyDown\n }, MenuListProps, {\n ref: this.handleMenuListRef\n }), children));\n }\n }]);\n return Menu;\n}(_react.default.Component);\n\n true ? Menu.propTypes = {\n /**\n * The DOM element used to set the position of the menu.\n */\n anchorEl: _propTypes.default.oneOfType([_propTypes.default.object, _propTypes.default.func]),\n\n /**\n * Menu contents, normally `MenuItem`s.\n */\n children: _propTypes.default.node,\n\n /**\n * Override or extend the styles applied to the component.\n * See [CSS API](#css-api) below for more details.\n */\n classes: _propTypes.default.object.isRequired,\n\n /**\n * If `true`, the selected / first menu item will not be auto focused.\n */\n disableAutoFocusItem: _propTypes.default.bool,\n\n /**\n * Properties applied to the [`MenuList`](/api/menu-list/) element.\n */\n MenuListProps: _propTypes.default.object,\n\n /**\n * Callback fired when the component requests to be closed.\n *\n * @param {object} event The event source of the callback\n * @param {string} reason Can be:`\"escapeKeyDown\"`, `\"backdropClick\"`, `\"tabKeyDown\"`\n */\n onClose: _propTypes.default.func,\n\n /**\n * Callback fired before the Menu enters.\n */\n onEnter: _propTypes.default.func,\n\n /**\n * Callback fired when the Menu has entered.\n */\n onEntered: _propTypes.default.func,\n\n /**\n * Callback fired when the Menu is entering.\n */\n onEntering: _propTypes.default.func,\n\n /**\n * Callback fired before the Menu exits.\n */\n onExit: _propTypes.default.func,\n\n /**\n * Callback fired when the Menu has exited.\n */\n onExited: _propTypes.default.func,\n\n /**\n * Callback fired when the Menu is exiting.\n */\n onExiting: _propTypes.default.func,\n\n /**\n * If `true`, the menu is visible.\n */\n open: _propTypes.default.bool.isRequired,\n\n /**\n * @ignore\n */\n PaperProps: _propTypes.default.object,\n\n /**\n * `classes` property applied to the [`Popover`](/api/popover/) element.\n */\n PopoverClasses: _propTypes.default.object,\n\n /**\n * @ignore\n */\n theme: _propTypes.default.object.isRequired,\n\n /**\n * The length of the transition in `ms`, or 'auto'\n */\n transitionDuration: _propTypes.default.oneOfType([_propTypes.default.number, _propTypes.default.shape({\n enter: _propTypes.default.number,\n exit: _propTypes.default.number\n }), _propTypes.default.oneOf(['auto'])])\n} : undefined;\nMenu.defaultProps = {\n disableAutoFocusItem: false,\n transitionDuration: 'auto'\n};\n\nvar _default = (0, _withStyles.default)(styles, {\n name: 'MuiMenu',\n withTheme: true\n})(Menu);\n\nexports.default = _default;\n\n//# sourceURL=webpack:///./node_modules/@material-ui/core/Menu/Menu.js?"); - - /***/ }), - - /***/ "./node_modules/@material-ui/core/MenuList/MenuList.js": - /*!*************************************************************!*\ - !*** ./node_modules/@material-ui/core/MenuList/MenuList.js ***! - \*************************************************************/ - /*! no static exports found */ - /***/ (function(module, exports, __webpack_require__) { - - "use strict"; - eval("\n\nvar _interopRequireDefault = __webpack_require__(/*! @babel/runtime/helpers/interopRequireDefault */ \"./node_modules/@babel/runtime/helpers/interopRequireDefault.js\");\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.default = void 0;\n\nvar _extends2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/extends */ \"./node_modules/@babel/runtime/helpers/extends.js\"));\n\nvar _objectWithoutProperties2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/objectWithoutProperties */ \"./node_modules/@babel/runtime/helpers/objectWithoutProperties.js\"));\n\nvar _classCallCheck2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/classCallCheck */ \"./node_modules/@babel/runtime/helpers/classCallCheck.js\"));\n\nvar _createClass2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/createClass */ \"./node_modules/@babel/runtime/helpers/createClass.js\"));\n\nvar _possibleConstructorReturn2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/possibleConstructorReturn */ \"./node_modules/@babel/runtime/helpers/possibleConstructorReturn.js\"));\n\nvar _getPrototypeOf3 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/getPrototypeOf */ \"./node_modules/@babel/runtime/helpers/getPrototypeOf.js\"));\n\nvar _inherits2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/inherits */ \"./node_modules/@babel/runtime/helpers/inherits.js\"));\n\nvar _react = _interopRequireDefault(__webpack_require__(/*! react */ \"./node_modules/react/index.js\"));\n\nvar _propTypes = _interopRequireDefault(__webpack_require__(/*! prop-types */ \"./node_modules/prop-types/index.js\"));\n\nvar _reactDom = _interopRequireDefault(__webpack_require__(/*! react-dom */ \"./node_modules/react-dom/index.js\"));\n\nvar _warning = _interopRequireDefault(__webpack_require__(/*! warning */ \"./node_modules/warning/warning.js\"));\n\nvar _ownerDocument = _interopRequireDefault(__webpack_require__(/*! ../utils/ownerDocument */ \"./node_modules/@material-ui/core/utils/ownerDocument.js\"));\n\nvar _List = _interopRequireDefault(__webpack_require__(/*! ../List */ \"./node_modules/@material-ui/core/List/index.js\"));\n\n// @inheritedComponent List\nvar MenuList =\n/*#__PURE__*/\nfunction (_React$Component) {\n (0, _inherits2.default)(MenuList, _React$Component);\n\n function MenuList() {\n var _getPrototypeOf2;\n\n var _this;\n\n (0, _classCallCheck2.default)(this, MenuList);\n\n for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {\n args[_key] = arguments[_key];\n }\n\n _this = (0, _possibleConstructorReturn2.default)(this, (_getPrototypeOf2 = (0, _getPrototypeOf3.default)(MenuList)).call.apply(_getPrototypeOf2, [this].concat(args)));\n _this.state = {\n currentTabIndex: null\n };\n\n _this.handleBlur = function (event) {\n _this.blurTimer = setTimeout(function () {\n if (_this.listRef) {\n var list = _this.listRef;\n var currentFocus = (0, _ownerDocument.default)(list).activeElement;\n\n if (!list.contains(currentFocus)) {\n _this.resetTabIndex();\n }\n }\n }, 30);\n\n if (_this.props.onBlur) {\n _this.props.onBlur(event);\n }\n };\n\n _this.handleKeyDown = function (event) {\n var list = _this.listRef;\n var key = event.key;\n var currentFocus = (0, _ownerDocument.default)(list).activeElement;\n\n if ((key === 'ArrowUp' || key === 'ArrowDown') && (!currentFocus || currentFocus && !list.contains(currentFocus))) {\n if (_this.selectedItemRef) {\n _this.selectedItemRef.focus();\n } else {\n list.firstChild.focus();\n }\n } else if (key === 'ArrowDown') {\n event.preventDefault();\n\n if (currentFocus.nextElementSibling) {\n currentFocus.nextElementSibling.focus();\n } else if (!_this.props.disableListWrap) {\n list.firstChild.focus();\n }\n } else if (key === 'ArrowUp') {\n event.preventDefault();\n\n if (currentFocus.previousElementSibling) {\n currentFocus.previousElementSibling.focus();\n } else if (!_this.props.disableListWrap) {\n list.lastChild.focus();\n }\n } else if (key === 'Home') {\n event.preventDefault();\n list.firstChild.focus();\n } else if (key === 'End') {\n event.preventDefault();\n list.lastChild.focus();\n }\n\n if (_this.props.onKeyDown) {\n _this.props.onKeyDown(event);\n }\n };\n\n _this.handleItemFocus = function (event) {\n var list = _this.listRef;\n\n if (list) {\n for (var i = 0; i < list.children.length; i += 1) {\n if (list.children[i] === event.currentTarget) {\n _this.setTabIndex(i);\n\n break;\n }\n }\n }\n };\n\n return _this;\n }\n\n (0, _createClass2.default)(MenuList, [{\n key: \"componentDidMount\",\n value: function componentDidMount() {\n this.resetTabIndex();\n }\n }, {\n key: \"componentWillUnmount\",\n value: function componentWillUnmount() {\n clearTimeout(this.blurTimer);\n }\n }, {\n key: \"setTabIndex\",\n value: function setTabIndex(index) {\n this.setState({\n currentTabIndex: index\n });\n }\n }, {\n key: \"focus\",\n value: function focus() {\n var currentTabIndex = this.state.currentTabIndex;\n var list = this.listRef;\n\n if (!list || !list.children || !list.firstChild) {\n return;\n }\n\n if (currentTabIndex && currentTabIndex >= 0) {\n list.children[currentTabIndex].focus();\n } else {\n list.firstChild.focus();\n }\n }\n }, {\n key: \"resetTabIndex\",\n value: function resetTabIndex() {\n var list = this.listRef;\n var currentFocus = (0, _ownerDocument.default)(list).activeElement;\n var items = [];\n\n for (var i = 0; i < list.children.length; i += 1) {\n items.push(list.children[i]);\n }\n\n var currentFocusIndex = items.indexOf(currentFocus);\n\n if (currentFocusIndex !== -1) {\n return this.setTabIndex(currentFocusIndex);\n }\n\n if (this.selectedItemRef) {\n return this.setTabIndex(items.indexOf(this.selectedItemRef));\n }\n\n return this.setTabIndex(0);\n }\n }, {\n key: \"render\",\n value: function render() {\n var _this2 = this;\n\n var _this$props = this.props,\n children = _this$props.children,\n className = _this$props.className,\n onBlur = _this$props.onBlur,\n onKeyDown = _this$props.onKeyDown,\n disableListWrap = _this$props.disableListWrap,\n other = (0, _objectWithoutProperties2.default)(_this$props, [\"children\", \"className\", \"onBlur\", \"onKeyDown\", \"disableListWrap\"]);\n return _react.default.createElement(_List.default, (0, _extends2.default)({\n role: \"menu\",\n ref: function ref(_ref) {\n _this2.listRef = _reactDom.default.findDOMNode(_ref);\n },\n className: className,\n onKeyDown: this.handleKeyDown,\n onBlur: this.handleBlur\n }, other), _react.default.Children.map(children, function (child, index) {\n if (!_react.default.isValidElement(child)) {\n return null;\n }\n\n true ? (0, _warning.default)(child.type !== _react.default.Fragment, [\"Material-UI: the MenuList component doesn't accept a Fragment as a child.\", 'Consider providing an array instead.'].join('\\n')) : undefined;\n return _react.default.cloneElement(child, {\n tabIndex: index === _this2.state.currentTabIndex ? 0 : -1,\n ref: child.props.selected ? function (ref) {\n _this2.selectedItemRef = _reactDom.default.findDOMNode(ref);\n } : undefined,\n onFocus: _this2.handleItemFocus\n });\n }));\n }\n }]);\n return MenuList;\n}(_react.default.Component);\n\n true ? MenuList.propTypes = {\n /**\n * MenuList contents, normally `MenuItem`s.\n */\n children: _propTypes.default.node,\n\n /**\n * @ignore\n */\n className: _propTypes.default.string,\n\n /**\n * If `true`, the menu items will not wrap focus.\n */\n disableListWrap: _propTypes.default.bool,\n\n /**\n * @ignore\n */\n onBlur: _propTypes.default.func,\n\n /**\n * @ignore\n */\n onKeyDown: _propTypes.default.func\n} : undefined;\nMenuList.defaultProps = {\n disableListWrap: false\n};\nvar _default = MenuList;\nexports.default = _default;\n\n//# sourceURL=webpack:///./node_modules/@material-ui/core/MenuList/MenuList.js?"); - - /***/ }), - - /***/ "./node_modules/@material-ui/core/MenuList/index.js": - /*!**********************************************************!*\ - !*** ./node_modules/@material-ui/core/MenuList/index.js ***! - \**********************************************************/ - /*! no static exports found */ - /***/ (function(module, exports, __webpack_require__) { - - "use strict"; - eval("\n\nvar _interopRequireDefault = __webpack_require__(/*! @babel/runtime/helpers/interopRequireDefault */ \"./node_modules/@babel/runtime/helpers/interopRequireDefault.js\");\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nObject.defineProperty(exports, \"default\", {\n enumerable: true,\n get: function get() {\n return _MenuList.default;\n }\n});\n\nvar _MenuList = _interopRequireDefault(__webpack_require__(/*! ./MenuList */ \"./node_modules/@material-ui/core/MenuList/MenuList.js\"));\n\n//# sourceURL=webpack:///./node_modules/@material-ui/core/MenuList/index.js?"); - - /***/ }), - - /***/ "./node_modules/@material-ui/core/Modal/Modal.js": - /*!*******************************************************!*\ - !*** ./node_modules/@material-ui/core/Modal/Modal.js ***! - \*******************************************************/ - /*! no static exports found */ - /***/ (function(module, exports, __webpack_require__) { - - "use strict"; - eval("\n\nvar _interopRequireDefault = __webpack_require__(/*! @babel/runtime/helpers/interopRequireDefault */ \"./node_modules/@babel/runtime/helpers/interopRequireDefault.js\");\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.default = exports.styles = void 0;\n\nvar _extends2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/extends */ \"./node_modules/@babel/runtime/helpers/extends.js\"));\n\nvar _defineProperty2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/defineProperty */ \"./node_modules/@babel/runtime/helpers/defineProperty.js\"));\n\nvar _objectWithoutProperties2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/objectWithoutProperties */ \"./node_modules/@babel/runtime/helpers/objectWithoutProperties.js\"));\n\nvar _classCallCheck2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/classCallCheck */ \"./node_modules/@babel/runtime/helpers/classCallCheck.js\"));\n\nvar _createClass2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/createClass */ \"./node_modules/@babel/runtime/helpers/createClass.js\"));\n\nvar _possibleConstructorReturn2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/possibleConstructorReturn */ \"./node_modules/@babel/runtime/helpers/possibleConstructorReturn.js\"));\n\nvar _getPrototypeOf2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/getPrototypeOf */ \"./node_modules/@babel/runtime/helpers/getPrototypeOf.js\"));\n\nvar _inherits2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/inherits */ \"./node_modules/@babel/runtime/helpers/inherits.js\"));\n\nvar _assertThisInitialized2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/assertThisInitialized */ \"./node_modules/@babel/runtime/helpers/assertThisInitialized.js\"));\n\nvar _react = _interopRequireDefault(__webpack_require__(/*! react */ \"./node_modules/react/index.js\"));\n\nvar _reactDom = _interopRequireDefault(__webpack_require__(/*! react-dom */ \"./node_modules/react-dom/index.js\"));\n\nvar _propTypes = _interopRequireDefault(__webpack_require__(/*! prop-types */ \"./node_modules/prop-types/index.js\"));\n\nvar _classnames = _interopRequireDefault(__webpack_require__(/*! classnames */ \"./node_modules/classnames/index.js\"));\n\nvar _warning = _interopRequireDefault(__webpack_require__(/*! warning */ \"./node_modules/warning/warning.js\"));\n\nvar _utils = __webpack_require__(/*! @material-ui/utils */ \"./node_modules/@material-ui/utils/index.es.js\");\n\nvar _ownerDocument = _interopRequireDefault(__webpack_require__(/*! ../utils/ownerDocument */ \"./node_modules/@material-ui/core/utils/ownerDocument.js\"));\n\nvar _RootRef = _interopRequireDefault(__webpack_require__(/*! ../RootRef */ \"./node_modules/@material-ui/core/RootRef/index.js\"));\n\nvar _Portal = _interopRequireDefault(__webpack_require__(/*! ../Portal */ \"./node_modules/@material-ui/core/Portal/index.js\"));\n\nvar _helpers = __webpack_require__(/*! ../utils/helpers */ \"./node_modules/@material-ui/core/utils/helpers.js\");\n\nvar _withStyles = _interopRequireDefault(__webpack_require__(/*! ../styles/withStyles */ \"./node_modules/@material-ui/core/styles/withStyles.js\"));\n\nvar _ModalManager = _interopRequireDefault(__webpack_require__(/*! ./ModalManager */ \"./node_modules/@material-ui/core/Modal/ModalManager.js\"));\n\nvar _Backdrop = _interopRequireDefault(__webpack_require__(/*! ../Backdrop */ \"./node_modules/@material-ui/core/Backdrop/index.js\"));\n\nvar _manageAriaHidden = __webpack_require__(/*! ./manageAriaHidden */ \"./node_modules/@material-ui/core/Modal/manageAriaHidden.js\");\n\nfunction getContainer(container, defaultContainer) {\n container = typeof container === 'function' ? container() : container;\n return _reactDom.default.findDOMNode(container) || defaultContainer;\n}\n\nfunction getHasTransition(props) {\n return props.children ? props.children.props.hasOwnProperty('in') : false;\n}\n\nvar styles = function styles(theme) {\n return {\n /* Styles applied to the root element. */\n root: {\n position: 'fixed',\n zIndex: theme.zIndex.modal,\n right: 0,\n bottom: 0,\n top: 0,\n left: 0\n },\n\n /* Styles applied to the root element if the `Modal` has exited. */\n hidden: {\n visibility: 'hidden'\n }\n };\n};\n/* istanbul ignore if */\n\n\nexports.styles = styles;\n\nif ( true && !_react.default.createContext) {\n throw new Error('Material-UI: react@16.3.0 or greater is required.');\n}\n/**\n * Modal is a lower-level construct that is leveraged by the following components:\n *\n * - [Dialog](/api/dialog/)\n * - [Drawer](/api/drawer/)\n * - [Menu](/api/menu/)\n * - [Popover](/api/popover/)\n *\n * If you are creating a modal dialog, you probably want to use the [Dialog](/api/dialog/) component\n * rather than directly using Modal.\n *\n * This component shares many concepts with [react-overlays](https://react-bootstrap.github.io/react-overlays/#modals).\n */\n\n\nvar Modal =\n/*#__PURE__*/\nfunction (_React$Component) {\n (0, _inherits2.default)(Modal, _React$Component);\n\n function Modal(props) {\n var _this;\n\n (0, _classCallCheck2.default)(this, Modal);\n _this = (0, _possibleConstructorReturn2.default)(this, (0, _getPrototypeOf2.default)(Modal).call(this));\n _this.mounted = false;\n\n _this.handleOpen = function () {\n var doc = (0, _ownerDocument.default)(_this.mountNode);\n var container = getContainer(_this.props.container, doc.body);\n\n _this.props.manager.add((0, _assertThisInitialized2.default)((0, _assertThisInitialized2.default)(_this)), container);\n\n doc.addEventListener('focus', _this.enforceFocus, true);\n\n if (_this.dialogRef) {\n _this.handleOpened();\n }\n };\n\n _this.handleRendered = function () {\n if (_this.props.onRendered) {\n _this.props.onRendered();\n }\n\n if (_this.props.open) {\n _this.handleOpened();\n } else {\n (0, _manageAriaHidden.ariaHidden)(_this.modalRef, true);\n }\n };\n\n _this.handleOpened = function () {\n _this.autoFocus();\n\n _this.props.manager.mount((0, _assertThisInitialized2.default)((0, _assertThisInitialized2.default)(_this))); // Fix a bug on Chrome where the scroll isn't initially 0.\n\n\n _this.modalRef.scrollTop = 0;\n };\n\n _this.handleClose = function (reason) {\n var hasTransition = getHasTransition(_this.props);\n /* If the component does not have a transition or is unmounting remove the Modal\n otherwise let the transition handle removing the style, this prevents elements\n moving around when the Modal is closed. */\n\n if (!(hasTransition && _this.props.closeAfterTransition) || reason === 'unmount') {\n _this.props.manager.remove((0, _assertThisInitialized2.default)((0, _assertThisInitialized2.default)(_this)));\n }\n\n var doc = (0, _ownerDocument.default)(_this.mountNode);\n doc.removeEventListener('focus', _this.enforceFocus, true);\n\n _this.restoreLastFocus();\n };\n\n _this.handleExited = function () {\n if (_this.props.closeAfterTransition) {\n _this.props.manager.remove((0, _assertThisInitialized2.default)((0, _assertThisInitialized2.default)(_this)));\n }\n\n _this.setState({\n exited: true\n });\n };\n\n _this.handleBackdropClick = function (event) {\n if (event.target !== event.currentTarget) {\n return;\n }\n\n if (_this.props.onBackdropClick) {\n _this.props.onBackdropClick(event);\n }\n\n if (!_this.props.disableBackdropClick && _this.props.onClose) {\n _this.props.onClose(event, 'backdropClick');\n }\n };\n\n _this.handleKeyDown = function (event) {\n // event.defaultPrevented:\n //\n // Ignore events that have been `event.preventDefault()` marked.\n // preventDefault() is meant to stop default behaviours like\n // clicking a checkbox to check it, hitting a button to submit a form,\n // and hitting left arrow to move the cursor in a text input etc.\n // Only special HTML elements have these default bahaviours.\n //\n // To remove in v4.\n if (event.key !== 'Escape' || !_this.isTopModal() || event.defaultPrevented) {\n return;\n } // Swallow the event, in case someone is listening for the escape key on the body.\n\n\n event.stopPropagation();\n\n if (_this.props.onEscapeKeyDown) {\n _this.props.onEscapeKeyDown(event);\n }\n\n if (!_this.props.disableEscapeKeyDown && _this.props.onClose) {\n _this.props.onClose(event, 'escapeKeyDown');\n }\n };\n\n _this.enforceFocus = function () {\n // The Modal might not already be mounted.\n if (!_this.isTopModal() || _this.props.disableEnforceFocus || !_this.mounted || !_this.dialogRef) {\n return;\n }\n\n var currentActiveElement = (0, _ownerDocument.default)(_this.mountNode).activeElement;\n\n if (!_this.dialogRef.contains(currentActiveElement)) {\n _this.dialogRef.focus();\n }\n };\n\n _this.handlePortalRef = function (ref) {\n _this.mountNode = ref ? ref.getMountNode() : ref;\n };\n\n _this.handleModalRef = function (ref) {\n _this.modalRef = ref;\n };\n\n _this.onRootRef = function (ref) {\n _this.dialogRef = ref;\n };\n\n _this.state = {\n exited: !props.open\n };\n return _this;\n }\n\n (0, _createClass2.default)(Modal, [{\n key: \"componentDidMount\",\n value: function componentDidMount() {\n this.mounted = true;\n\n if (this.props.open) {\n this.handleOpen();\n }\n }\n }, {\n key: \"componentDidUpdate\",\n value: function componentDidUpdate(prevProps) {\n if (prevProps.open && !this.props.open) {\n this.handleClose();\n } else if (!prevProps.open && this.props.open) {\n this.lastFocus = (0, _ownerDocument.default)(this.mountNode).activeElement;\n this.handleOpen();\n }\n }\n }, {\n key: \"componentWillUnmount\",\n value: function componentWillUnmount() {\n this.mounted = false;\n\n if (this.props.open || getHasTransition(this.props) && !this.state.exited) {\n this.handleClose('unmount');\n }\n }\n }, {\n key: \"autoFocus\",\n value: function autoFocus() {\n // We might render an empty child.\n if (this.props.disableAutoFocus || !this.dialogRef) {\n return;\n }\n\n var currentActiveElement = (0, _ownerDocument.default)(this.mountNode).activeElement;\n\n if (!this.dialogRef.contains(currentActiveElement)) {\n if (!this.dialogRef.hasAttribute('tabIndex')) {\n true ? (0, _warning.default)(false, ['Material-UI: the modal content node does not accept focus.', 'For the benefit of assistive technologies, ' + 'the tabIndex of the node is being set to \"-1\".'].join('\\n')) : undefined;\n this.dialogRef.setAttribute('tabIndex', -1);\n }\n\n this.lastFocus = currentActiveElement;\n this.dialogRef.focus();\n }\n }\n }, {\n key: \"restoreLastFocus\",\n value: function restoreLastFocus() {\n if (this.props.disableRestoreFocus || !this.lastFocus) {\n return;\n } // Not all elements in IE 11 have a focus method.\n // Because IE 11 market share is low, we accept the restore focus being broken\n // and we silent the issue.\n\n\n if (this.lastFocus.focus) {\n this.lastFocus.focus();\n }\n\n this.lastFocus = null;\n }\n }, {\n key: \"isTopModal\",\n value: function isTopModal() {\n return this.props.manager.isTopModal(this);\n }\n }, {\n key: \"render\",\n value: function render() {\n var _this$props = this.props,\n BackdropComponent = _this$props.BackdropComponent,\n BackdropProps = _this$props.BackdropProps,\n children = _this$props.children,\n classes = _this$props.classes,\n className = _this$props.className,\n closeAfterTransition = _this$props.closeAfterTransition,\n container = _this$props.container,\n disableAutoFocus = _this$props.disableAutoFocus,\n disableBackdropClick = _this$props.disableBackdropClick,\n disableEnforceFocus = _this$props.disableEnforceFocus,\n disableEscapeKeyDown = _this$props.disableEscapeKeyDown,\n disablePortal = _this$props.disablePortal,\n disableRestoreFocus = _this$props.disableRestoreFocus,\n hideBackdrop = _this$props.hideBackdrop,\n keepMounted = _this$props.keepMounted,\n manager = _this$props.manager,\n onBackdropClick = _this$props.onBackdropClick,\n onClose = _this$props.onClose,\n onEscapeKeyDown = _this$props.onEscapeKeyDown,\n onRendered = _this$props.onRendered,\n open = _this$props.open,\n other = (0, _objectWithoutProperties2.default)(_this$props, [\"BackdropComponent\", \"BackdropProps\", \"children\", \"classes\", \"className\", \"closeAfterTransition\", \"container\", \"disableAutoFocus\", \"disableBackdropClick\", \"disableEnforceFocus\", \"disableEscapeKeyDown\", \"disablePortal\", \"disableRestoreFocus\", \"hideBackdrop\", \"keepMounted\", \"manager\", \"onBackdropClick\", \"onClose\", \"onEscapeKeyDown\", \"onRendered\", \"open\"]);\n var exited = this.state.exited;\n var hasTransition = getHasTransition(this.props);\n\n if (!keepMounted && !open && (!hasTransition || exited)) {\n return null;\n }\n\n var childProps = {}; // It's a Transition like component\n\n if (hasTransition) {\n childProps.onExited = (0, _helpers.createChainedFunction)(this.handleExited, children.props.onExited);\n }\n\n if (children.props.role === undefined) {\n childProps.role = children.props.role || 'document';\n }\n\n if (children.props.tabIndex === undefined) {\n childProps.tabIndex = children.props.tabIndex || '-1';\n }\n\n return _react.default.createElement(_Portal.default, {\n ref: this.handlePortalRef,\n container: container,\n disablePortal: disablePortal,\n onRendered: this.handleRendered\n }, _react.default.createElement(\"div\", (0, _extends2.default)({\n ref: this.handleModalRef,\n onKeyDown: this.handleKeyDown,\n role: \"presentation\",\n className: (0, _classnames.default)(classes.root, className, (0, _defineProperty2.default)({}, classes.hidden, exited))\n }, other), hideBackdrop ? null : _react.default.createElement(BackdropComponent, (0, _extends2.default)({\n open: open,\n onClick: this.handleBackdropClick\n }, BackdropProps)), _react.default.createElement(_RootRef.default, {\n rootRef: this.onRootRef\n }, _react.default.cloneElement(children, childProps))));\n }\n }], [{\n key: \"getDerivedStateFromProps\",\n value: function getDerivedStateFromProps(nextProps) {\n if (nextProps.open) {\n return {\n exited: false\n };\n }\n\n if (!getHasTransition(nextProps)) {\n // Otherwise let handleExited take care of marking exited.\n return {\n exited: true\n };\n }\n\n return null;\n }\n }]);\n return Modal;\n}(_react.default.Component);\n\n true ? Modal.propTypes = {\n /**\n * A backdrop component. This property enables custom backdrop rendering.\n */\n BackdropComponent: _utils.componentPropType,\n\n /**\n * Properties applied to the [`Backdrop`](/api/backdrop/) element.\n */\n BackdropProps: _propTypes.default.object,\n\n /**\n * A single child content element.\n */\n children: _propTypes.default.element,\n\n /**\n * Override or extend the styles applied to the component.\n * See [CSS API](#css-api) below for more details.\n */\n classes: _propTypes.default.object.isRequired,\n\n /**\n * @ignore\n */\n className: _propTypes.default.string,\n\n /**\n * When set to true the Modal waits until a nested Transition is completed before closing.\n */\n closeAfterTransition: _propTypes.default.bool,\n\n /**\n * A node, component instance, or function that returns either.\n * The `container` will have the portal children appended to it.\n */\n container: _propTypes.default.oneOfType([_propTypes.default.object, _propTypes.default.func]),\n\n /**\n * If `true`, the modal will not automatically shift focus to itself when it opens, and\n * replace it to the last focused element when it closes.\n * This also works correctly with any modal children that have the `disableAutoFocus` prop.\n *\n * Generally this should never be set to `true` as it makes the modal less\n * accessible to assistive technologies, like screen readers.\n */\n disableAutoFocus: _propTypes.default.bool,\n\n /**\n * If `true`, clicking the backdrop will not fire any callback.\n */\n disableBackdropClick: _propTypes.default.bool,\n\n /**\n * If `true`, the modal will not prevent focus from leaving the modal while open.\n *\n * Generally this should never be set to `true` as it makes the modal less\n * accessible to assistive technologies, like screen readers.\n */\n disableEnforceFocus: _propTypes.default.bool,\n\n /**\n * If `true`, hitting escape will not fire any callback.\n */\n disableEscapeKeyDown: _propTypes.default.bool,\n\n /**\n * Disable the portal behavior.\n * The children stay within it's parent DOM hierarchy.\n */\n disablePortal: _propTypes.default.bool,\n\n /**\n * If `true`, the modal will not restore focus to previously focused element once\n * modal is hidden.\n */\n disableRestoreFocus: _propTypes.default.bool,\n\n /**\n * If `true`, the backdrop is not rendered.\n */\n hideBackdrop: _propTypes.default.bool,\n\n /**\n * Always keep the children in the DOM.\n * This property can be useful in SEO situation or\n * when you want to maximize the responsiveness of the Modal.\n */\n keepMounted: _propTypes.default.bool,\n\n /**\n * @ignore\n *\n * A modal manager used to track and manage the state of open\n * Modals. This enables customizing how modals interact within a container.\n */\n manager: _propTypes.default.object,\n\n /**\n * Callback fired when the backdrop is clicked.\n */\n onBackdropClick: _propTypes.default.func,\n\n /**\n * Callback fired when the component requests to be closed.\n * The `reason` parameter can optionally be used to control the response to `onClose`.\n *\n * @param {object} event The event source of the callback\n * @param {string} reason Can be:`\"escapeKeyDown\"`, `\"backdropClick\"`\n */\n onClose: _propTypes.default.func,\n\n /**\n * Callback fired when the escape key is pressed,\n * `disableEscapeKeyDown` is false and the modal is in focus.\n */\n onEscapeKeyDown: _propTypes.default.func,\n\n /**\n * Callback fired once the children has been mounted into the `container`.\n * It signals that the `open={true}` property took effect.\n */\n onRendered: _propTypes.default.func,\n\n /**\n * If `true`, the modal is open.\n */\n open: _propTypes.default.bool.isRequired\n} : undefined;\nModal.defaultProps = {\n BackdropComponent: _Backdrop.default,\n closeAfterTransition: false,\n disableAutoFocus: false,\n disableBackdropClick: false,\n disableEnforceFocus: false,\n disableEscapeKeyDown: false,\n disablePortal: false,\n disableRestoreFocus: false,\n hideBackdrop: false,\n keepMounted: false,\n // Modals don't open on the server so this won't conflict with concurrent requests.\n manager: new _ModalManager.default()\n};\n\nvar _default = (0, _withStyles.default)(styles, {\n flip: false,\n name: 'MuiModal'\n})(Modal);\n\nexports.default = _default;\n\n//# sourceURL=webpack:///./node_modules/@material-ui/core/Modal/Modal.js?"); - - /***/ }), - - /***/ "./node_modules/@material-ui/core/Modal/ModalManager.js": - /*!**************************************************************!*\ - !*** ./node_modules/@material-ui/core/Modal/ModalManager.js ***! - \**************************************************************/ - /*! no static exports found */ - /***/ (function(module, exports, __webpack_require__) { - - "use strict"; - eval("\n\nvar _interopRequireDefault = __webpack_require__(/*! @babel/runtime/helpers/interopRequireDefault */ \"./node_modules/@babel/runtime/helpers/interopRequireDefault.js\");\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.default = void 0;\n\nvar _classCallCheck2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/classCallCheck */ \"./node_modules/@babel/runtime/helpers/classCallCheck.js\"));\n\nvar _createClass2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/createClass */ \"./node_modules/@babel/runtime/helpers/createClass.js\"));\n\nvar _style = _interopRequireDefault(__webpack_require__(/*! dom-helpers/style */ \"./node_modules/dom-helpers/style/index.js\"));\n\nvar _scrollbarSize = _interopRequireDefault(__webpack_require__(/*! dom-helpers/util/scrollbarSize */ \"./node_modules/dom-helpers/util/scrollbarSize.js\"));\n\nvar _ownerDocument = _interopRequireDefault(__webpack_require__(/*! ../utils/ownerDocument */ \"./node_modules/@material-ui/core/utils/ownerDocument.js\"));\n\nvar _isOverflowing = _interopRequireDefault(__webpack_require__(/*! ./isOverflowing */ \"./node_modules/@material-ui/core/Modal/isOverflowing.js\"));\n\nvar _manageAriaHidden = __webpack_require__(/*! ./manageAriaHidden */ \"./node_modules/@material-ui/core/Modal/manageAriaHidden.js\");\n\nfunction findIndexOf(data, callback) {\n var idx = -1;\n data.some(function (item, index) {\n if (callback(item)) {\n idx = index;\n return true;\n }\n\n return false;\n });\n return idx;\n}\n\nfunction getPaddingRight(node) {\n return parseInt((0, _style.default)(node, 'paddingRight') || 0, 10);\n}\n\nfunction setContainerStyle(data) {\n // We are only interested in the actual `style` here because we will override it.\n data.style = {\n overflow: data.container.style.overflow,\n paddingRight: data.container.style.paddingRight\n };\n var style = {\n overflow: 'hidden'\n };\n\n if (data.overflowing) {\n var scrollbarSize = (0, _scrollbarSize.default)(); // Use computed style, here to get the real padding to add our scrollbar width.\n\n style.paddingRight = \"\".concat(getPaddingRight(data.container) + scrollbarSize, \"px\"); // .mui-fixed is a global helper.\n\n var fixedNodes = (0, _ownerDocument.default)(data.container).querySelectorAll('.mui-fixed');\n\n for (var i = 0; i < fixedNodes.length; i += 1) {\n var paddingRight = getPaddingRight(fixedNodes[i]);\n data.prevPaddings.push(paddingRight);\n fixedNodes[i].style.paddingRight = \"\".concat(paddingRight + scrollbarSize, \"px\");\n }\n }\n\n Object.keys(style).forEach(function (key) {\n data.container.style[key] = style[key];\n });\n}\n\nfunction removeContainerStyle(data) {\n // The modal might be closed before it had the chance to be mounted in the DOM.\n if (data.style) {\n Object.keys(data.style).forEach(function (key) {\n data.container.style[key] = data.style[key];\n });\n }\n\n var fixedNodes = (0, _ownerDocument.default)(data.container).querySelectorAll('.mui-fixed');\n\n for (var i = 0; i < fixedNodes.length; i += 1) {\n fixedNodes[i].style.paddingRight = \"\".concat(data.prevPaddings[i], \"px\");\n }\n}\n/**\n * @ignore - do not document.\n *\n * Proper state management for containers and the modals in those containers.\n * Simplified, but inspired by react-overlay's ModalManager class.\n * Used by the Modal to ensure proper styling of containers.\n */\n\n\nvar ModalManager =\n/*#__PURE__*/\nfunction () {\n function ModalManager() {\n var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};\n (0, _classCallCheck2.default)(this, ModalManager);\n var _options$hideSiblingN = options.hideSiblingNodes,\n hideSiblingNodes = _options$hideSiblingN === void 0 ? true : _options$hideSiblingN,\n _options$handleContai = options.handleContainerOverflow,\n handleContainerOverflow = _options$handleContai === void 0 ? true : _options$handleContai;\n this.hideSiblingNodes = hideSiblingNodes;\n this.handleContainerOverflow = handleContainerOverflow; // this.modals[modalIdx] = modal\n\n this.modals = []; // this.data[containerIdx] = {\n // modals: [],\n // container,\n // overflowing,\n // prevPaddings,\n // }\n\n this.data = [];\n }\n\n (0, _createClass2.default)(ModalManager, [{\n key: \"add\",\n value: function add(modal, container) {\n var modalIdx = this.modals.indexOf(modal);\n\n if (modalIdx !== -1) {\n return modalIdx;\n }\n\n modalIdx = this.modals.length;\n this.modals.push(modal); // If the modal we are adding is already in the DOM.\n\n if (modal.modalRef) {\n (0, _manageAriaHidden.ariaHidden)(modal.modalRef, false);\n }\n\n if (this.hideSiblingNodes) {\n (0, _manageAriaHidden.ariaHiddenSiblings)(container, modal.mountNode, modal.modalRef, true);\n }\n\n var containerIdx = findIndexOf(this.data, function (item) {\n return item.container === container;\n });\n\n if (containerIdx !== -1) {\n this.data[containerIdx].modals.push(modal);\n return modalIdx;\n }\n\n var data = {\n modals: [modal],\n container: container,\n overflowing: (0, _isOverflowing.default)(container),\n prevPaddings: []\n };\n this.data.push(data);\n return modalIdx;\n }\n }, {\n key: \"mount\",\n value: function mount(modal) {\n var containerIdx = findIndexOf(this.data, function (item) {\n return item.modals.indexOf(modal) !== -1;\n });\n var data = this.data[containerIdx];\n\n if (!data.style && this.handleContainerOverflow) {\n setContainerStyle(data);\n }\n }\n }, {\n key: \"remove\",\n value: function remove(modal) {\n var modalIdx = this.modals.indexOf(modal);\n\n if (modalIdx === -1) {\n return modalIdx;\n }\n\n var containerIdx = findIndexOf(this.data, function (item) {\n return item.modals.indexOf(modal) !== -1;\n });\n var data = this.data[containerIdx];\n data.modals.splice(data.modals.indexOf(modal), 1);\n this.modals.splice(modalIdx, 1); // If that was the last modal in a container, clean up the container.\n\n if (data.modals.length === 0) {\n if (this.handleContainerOverflow) {\n removeContainerStyle(data);\n } // In case the modal wasn't in the DOM yet.\n\n\n if (modal.modalRef) {\n (0, _manageAriaHidden.ariaHidden)(modal.modalRef, true);\n }\n\n if (this.hideSiblingNodes) {\n (0, _manageAriaHidden.ariaHiddenSiblings)(data.container, modal.mountNode, modal.modalRef, false);\n }\n\n this.data.splice(containerIdx, 1);\n } else if (this.hideSiblingNodes) {\n // Otherwise make sure the next top modal is visible to a screen reader.\n var nextTop = data.modals[data.modals.length - 1]; // as soon as a modal is adding its modalRef is undefined. it can't set\n // aria-hidden because the dom element doesn't exist either\n // when modal was unmounted before modalRef gets null\n\n if (nextTop.modalRef) {\n (0, _manageAriaHidden.ariaHidden)(nextTop.modalRef, false);\n }\n }\n\n return modalIdx;\n }\n }, {\n key: \"isTopModal\",\n value: function isTopModal(modal) {\n return !!this.modals.length && this.modals[this.modals.length - 1] === modal;\n }\n }]);\n return ModalManager;\n}();\n\nvar _default = ModalManager;\nexports.default = _default;\n\n//# sourceURL=webpack:///./node_modules/@material-ui/core/Modal/ModalManager.js?"); - - /***/ }), - - /***/ "./node_modules/@material-ui/core/Modal/index.js": - /*!*******************************************************!*\ - !*** ./node_modules/@material-ui/core/Modal/index.js ***! - \*******************************************************/ - /*! no static exports found */ - /***/ (function(module, exports, __webpack_require__) { - - "use strict"; - eval("\n\nvar _interopRequireDefault = __webpack_require__(/*! @babel/runtime/helpers/interopRequireDefault */ \"./node_modules/@babel/runtime/helpers/interopRequireDefault.js\");\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nObject.defineProperty(exports, \"default\", {\n enumerable: true,\n get: function get() {\n return _Modal.default;\n }\n});\nObject.defineProperty(exports, \"ModalManager\", {\n enumerable: true,\n get: function get() {\n return _ModalManager.default;\n }\n});\n\nvar _Modal = _interopRequireDefault(__webpack_require__(/*! ./Modal */ \"./node_modules/@material-ui/core/Modal/Modal.js\"));\n\nvar _ModalManager = _interopRequireDefault(__webpack_require__(/*! ./ModalManager */ \"./node_modules/@material-ui/core/Modal/ModalManager.js\"));\n\n//# sourceURL=webpack:///./node_modules/@material-ui/core/Modal/index.js?"); - - /***/ }), - - /***/ "./node_modules/@material-ui/core/Modal/isOverflowing.js": - /*!***************************************************************!*\ - !*** ./node_modules/@material-ui/core/Modal/isOverflowing.js ***! - \***************************************************************/ - /*! no static exports found */ - /***/ (function(module, exports, __webpack_require__) { - - "use strict"; - eval("\n\nvar _interopRequireDefault = __webpack_require__(/*! @babel/runtime/helpers/interopRequireDefault */ \"./node_modules/@babel/runtime/helpers/interopRequireDefault.js\");\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.isBody = isBody;\nexports.default = isOverflowing;\n\nvar _isWindow = _interopRequireDefault(__webpack_require__(/*! dom-helpers/query/isWindow */ \"./node_modules/dom-helpers/query/isWindow.js\"));\n\nvar _ownerDocument = _interopRequireDefault(__webpack_require__(/*! ../utils/ownerDocument */ \"./node_modules/@material-ui/core/utils/ownerDocument.js\"));\n\nvar _ownerWindow = _interopRequireDefault(__webpack_require__(/*! ../utils/ownerWindow */ \"./node_modules/@material-ui/core/utils/ownerWindow.js\"));\n\nfunction isBody(node) {\n return node && node.tagName.toLowerCase() === 'body';\n} // Do we have a vertical scroll bar?\n\n\nfunction isOverflowing(container) {\n var doc = (0, _ownerDocument.default)(container);\n var win = (0, _ownerWindow.default)(doc);\n /* istanbul ignore next */\n\n if (!(0, _isWindow.default)(doc) && !isBody(container)) {\n return container.scrollHeight > container.clientHeight;\n } // Takes in account potential non zero margin on the body.\n\n\n var style = win.getComputedStyle(doc.body);\n var marginLeft = parseInt(style.getPropertyValue('margin-left'), 10);\n var marginRight = parseInt(style.getPropertyValue('margin-right'), 10);\n return marginLeft + doc.body.clientWidth + marginRight < win.innerWidth;\n}\n\n//# sourceURL=webpack:///./node_modules/@material-ui/core/Modal/isOverflowing.js?"); - - /***/ }), - - /***/ "./node_modules/@material-ui/core/Modal/manageAriaHidden.js": - /*!******************************************************************!*\ - !*** ./node_modules/@material-ui/core/Modal/manageAriaHidden.js ***! - \******************************************************************/ - /*! no static exports found */ - /***/ (function(module, exports, __webpack_require__) { - - "use strict"; - eval("\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.ariaHidden = ariaHidden;\nexports.ariaHiddenSiblings = ariaHiddenSiblings;\nvar BLACKLIST = ['template', 'script', 'style'];\n\nfunction isHideable(node) {\n return node.nodeType === 1 && BLACKLIST.indexOf(node.tagName.toLowerCase()) === -1;\n}\n\nfunction siblings(container, mount, currentNode, callback) {\n var blacklist = [mount, currentNode];\n [].forEach.call(container.children, function (node) {\n if (blacklist.indexOf(node) === -1 && isHideable(node)) {\n callback(node);\n }\n });\n}\n\nfunction ariaHidden(node, show) {\n if (show) {\n node.setAttribute('aria-hidden', 'true');\n } else {\n node.removeAttribute('aria-hidden');\n }\n}\n\nfunction ariaHiddenSiblings(container, mountNode, currentNode, show) {\n siblings(container, mountNode, currentNode, function (node) {\n return ariaHidden(node, show);\n });\n}\n\n//# sourceURL=webpack:///./node_modules/@material-ui/core/Modal/manageAriaHidden.js?"); - - /***/ }), - - /***/ "./node_modules/@material-ui/core/NativeSelect/NativeSelect.js": - /*!*********************************************************************!*\ - !*** ./node_modules/@material-ui/core/NativeSelect/NativeSelect.js ***! - \*********************************************************************/ - /*! no static exports found */ - /***/ (function(module, exports, __webpack_require__) { - - "use strict"; - eval("\n\nvar _interopRequireDefault = __webpack_require__(/*! @babel/runtime/helpers/interopRequireDefault */ \"./node_modules/@babel/runtime/helpers/interopRequireDefault.js\");\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.default = exports.styles = void 0;\n\nvar _extends2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/extends */ \"./node_modules/@babel/runtime/helpers/extends.js\"));\n\nvar _objectWithoutProperties2 = _interopRequireDefault(__webpack_require__(/*! @babel/runtime/helpers/objectWithoutProperties */ \"./node_modules/@babel/runtime/helpers/objectWithoutProperties.js\"));\n\nvar _react = _interopRequireDefault(__webpack_require__(/*! react */ \"./node_modules/react/index.js\"));\n\nvar _propTypes = _interopRequireDefault(__webpack_require__(/*! prop-types */ \"./node_modules/prop-types/index.js\"));\n\nvar _utils = __webpack_require__(/*! @material-ui/utils */ \"./node_modules/@material-ui/utils/index.es.js\");\n\nvar _NativeSelectInput = _interopRequireDefault(__webpack_require__(/*! ./NativeSelectInput */ \"./node_modules/@material-ui/core/NativeSelect/NativeSelectInput.js\"));\n\nvar _withStyles = _interopRequireDefault(__webpack_require__(/*! ../styles/withStyles */ \"./node_modules/@material-ui/core/styles/withStyles.js\"));\n\nvar _formControlState = _interopRequireDefault(__webpack_require__(/*! ../FormControl/formControlState */ \"./node_modules/@material-ui/core/FormControl/formControlState.js\"));\n\nvar _withFormControlContext = _interopRequireDefault(__webpack_require__(/*! ../FormControl/withFormControlContext */ \"./node_modules/@material-ui/core/FormControl/withFormControlContext.js\"));\n\nvar _ArrowDropDown = _interopRequireDefault(__webpack_require__(/*! ../internal/svg-icons/ArrowDropDown */ \"./node_modules/@material-ui/core/internal/svg-icons/ArrowDropDown.js\"));\n\nvar _Input = _interopRequireDefault(__webpack_require__(/*! ../Input */ \"./node_modules/@material-ui/core/Input/index.js\"));\n\n// @inheritedComponent Input\nvar styles = function styles(theme) {\n return {\n /* Styles applied to the `Input` component `root` class. */\n root: {\n position: 'relative',\n width: '100%'\n },\n\n /* Styles applied to the `Input` component `select` class. */\n select: {\n '-moz-appearance': 'none',\n // Reset\n '-webkit-appearance': 'none',\n // Reset\n // When interacting quickly, the text can end up selected.\n // Native select can't be selected either.\n userSelect: 'none',\n paddingRight: 32,\n borderRadius: 0,\n // Reset\n height: '1.1875em',\n // Reset (19px), match the native input line-height\n width: 'calc(100% - 32px)',\n minWidth: 16,\n // So it doesn't collapse.\n cursor: 'pointer',\n '&:focus': {\n // Show that it's not an text input\n backgroundColor: theme.palette.type === 'light' ? 'rgba(0, 0, 0, 0.05)' : 'rgba(255, 255, 255, 0.05)',\n borderRadius: 0 // Reset Chrome style\n\n },\n // Remove IE 11 arrow\n '&::-ms-expand': {\n display: 'none'\n },\n '&$disabled': {\n cursor: 'default'\n },\n '&[multiple]': {\n height: 'auto'\n },\n '&:not([multiple]) option, &:not([multiple]) optgroup': {\n backgroundColor: theme.palette.background.paper\n }\n },\n\n /* Styles applied to the `Input` component if `variant=\"filled\"`. */\n filled: {\n width: 'calc(100% - 44px)'\n },\n\n /* Styles applied to the `Input` component if `variant=\"outlined\"`. */\n outlined: {\n width: 'calc(100% - 46px)',\n borderRadius: theme.shape.borderRadius\n },\n\n /* Styles applied to the `Input` component `selectMenu` class. */\n selectMenu: {\n width: 'auto',\n // Fix Safari textOverflow\n height: 'auto',\n // Reset\n textOverflow: 'ellipsis',\n whiteSpace: 'nowrap',\n overflow: 'hidden',\n minHeight: '1.1875em' // Reset (19px), match the native input line-height\n\n },\n\n /* Styles applied to the `Input` component `disabled` class. */\n disabled: {},\n\n /* Styles applied to the `Input` component `icon` class. */\n icon: {\n // We use a position absolute over a flexbox in order to forward the pointer events\n // to the input.\n position: 'absolute',\n right: 0,\n top: 'calc(50% - 12px)',\n // Center vertically\n color: theme.palette.action.active,\n 'pointer-events': 'none' // Don't block pointer events on the select under the icon.\n\n }\n };\n};\n/**\n * An alternative to ` host component that allows setting these optional\n * props: `checked`, `value`, `defaultChecked`, and `defaultValue`.\n *\n * If `checked` or `value` are not supplied (or null/undefined), user actions\n * that affect the checked state or value will trigger updates to the element.\n *\n * If they are supplied (and not null/undefined), the rendered element will not\n * trigger updates to the element. Instead, the props must change in order for\n * the rendered element to be updated.\n *\n * The rendered element will be initialized as unchecked (or `defaultChecked`)\n * with an empty value (or `defaultValue`).\n *\n * See http://www.w3.org/TR/2012/WD-html5-20121025/the-input-element.html\n */\n\n\nfunction getHostProps(element, props) {\n var node = element;\n var checked = props.checked;\n\n var hostProps = _assign({}, props, {\n defaultChecked: undefined,\n defaultValue: undefined,\n value: undefined,\n checked: checked != null ? checked : node._wrapperState.initialChecked\n });\n\n return hostProps;\n}\nfunction initWrapperState(element, props) {\n {\n ReactControlledValuePropTypes.checkPropTypes('input', props);\n\n if (props.checked !== undefined && props.defaultChecked !== undefined && !didWarnCheckedDefaultChecked) {\n error('%s contains an input of type %s with both checked and defaultChecked props. ' + 'Input elements must be either controlled or uncontrolled ' + '(specify either the checked prop, or the defaultChecked prop, but not ' + 'both). Decide between using a controlled or uncontrolled input ' + 'element and remove one of these props. More info: ' + 'https://fb.me/react-controlled-components', getCurrentFiberOwnerNameInDevOrNull() || 'A component', props.type);\n\n didWarnCheckedDefaultChecked = true;\n }\n\n if (props.value !== undefined && props.defaultValue !== undefined && !didWarnValueDefaultValue) {\n error('%s contains an input of type %s with both value and defaultValue props. ' + 'Input elements must be either controlled or uncontrolled ' + '(specify either the value prop, or the defaultValue prop, but not ' + 'both). Decide between using a controlled or uncontrolled input ' + 'element and remove one of these props. More info: ' + 'https://fb.me/react-controlled-components', getCurrentFiberOwnerNameInDevOrNull() || 'A component', props.type);\n\n didWarnValueDefaultValue = true;\n }\n }\n\n var node = element;\n var defaultValue = props.defaultValue == null ? '' : props.defaultValue;\n node._wrapperState = {\n initialChecked: props.checked != null ? props.checked : props.defaultChecked,\n initialValue: getToStringValue(props.value != null ? props.value : defaultValue),\n controlled: isControlled(props)\n };\n}\nfunction updateChecked(element, props) {\n var node = element;\n var checked = props.checked;\n\n if (checked != null) {\n setValueForProperty(node, 'checked', checked, false);\n }\n}\nfunction updateWrapper(element, props) {\n var node = element;\n\n {\n var controlled = isControlled(props);\n\n if (!node._wrapperState.controlled && controlled && !didWarnUncontrolledToControlled) {\n error('A component is changing an uncontrolled input of type %s to be controlled. ' + 'Input elements should not switch from uncontrolled to controlled (or vice versa). ' + 'Decide between using a controlled or uncontrolled input ' + 'element for the lifetime of the component. More info: https://fb.me/react-controlled-components', props.type);\n\n didWarnUncontrolledToControlled = true;\n }\n\n if (node._wrapperState.controlled && !controlled && !didWarnControlledToUncontrolled) {\n error('A component is changing a controlled input of type %s to be uncontrolled. ' + 'Input elements should not switch from controlled to uncontrolled (or vice versa). ' + 'Decide between using a controlled or uncontrolled input ' + 'element for the lifetime of the component. More info: https://fb.me/react-controlled-components', props.type);\n\n didWarnControlledToUncontrolled = true;\n }\n }\n\n updateChecked(element, props);\n var value = getToStringValue(props.value);\n var type = props.type;\n\n if (value != null) {\n if (type === 'number') {\n if (value === 0 && node.value === '' || // We explicitly want to coerce to number here if possible.\n // eslint-disable-next-line\n node.value != value) {\n node.value = toString(value);\n }\n } else if (node.value !== toString(value)) {\n node.value = toString(value);\n }\n } else if (type === 'submit' || type === 'reset') {\n // Submit/reset inputs need the attribute removed completely to avoid\n // blank-text buttons.\n node.removeAttribute('value');\n return;\n }\n\n {\n // When syncing the value attribute, the value comes from a cascade of\n // properties:\n // 1. The value React property\n // 2. The defaultValue React property\n // 3. Otherwise there should be no change\n if (props.hasOwnProperty('value')) {\n setDefaultValue(node, props.type, value);\n } else if (props.hasOwnProperty('defaultValue')) {\n setDefaultValue(node, props.type, getToStringValue(props.defaultValue));\n }\n }\n\n {\n // When syncing the checked attribute, it only changes when it needs\n // to be removed, such as transitioning from a checkbox into a text input\n if (props.checked == null && props.defaultChecked != null) {\n node.defaultChecked = !!props.defaultChecked;\n }\n }\n}\nfunction postMountWrapper(element, props, isHydrating) {\n var node = element; // Do not assign value if it is already set. This prevents user text input\n // from being lost during SSR hydration.\n\n if (props.hasOwnProperty('value') || props.hasOwnProperty('defaultValue')) {\n var type = props.type;\n var isButton = type === 'submit' || type === 'reset'; // Avoid setting value attribute on submit/reset inputs as it overrides the\n // default value provided by the browser. See: #12872\n\n if (isButton && (props.value === undefined || props.value === null)) {\n return;\n }\n\n var initialValue = toString(node._wrapperState.initialValue); // Do not assign value if it is already set. This prevents user text input\n // from being lost during SSR hydration.\n\n if (!isHydrating) {\n {\n // When syncing the value attribute, the value property should use\n // the wrapperState._initialValue property. This uses:\n //\n // 1. The value React property when present\n // 2. The defaultValue React property when present\n // 3. An empty string\n if (initialValue !== node.value) {\n node.value = initialValue;\n }\n }\n }\n\n {\n // Otherwise, the value attribute is synchronized to the property,\n // so we assign defaultValue to the same thing as the value property\n // assignment step above.\n node.defaultValue = initialValue;\n }\n } // Normally, we'd just do `node.checked = node.checked` upon initial mount, less this bug\n // this is needed to work around a chrome bug where setting defaultChecked\n // will sometimes influence the value of checked (even after detachment).\n // Reference: https://bugs.chromium.org/p/chromium/issues/detail?id=608416\n // We need to temporarily unset name to avoid disrupting radio button groups.\n\n\n var name = node.name;\n\n if (name !== '') {\n node.name = '';\n }\n\n {\n // When syncing the checked attribute, both the checked property and\n // attribute are assigned at the same time using defaultChecked. This uses:\n //\n // 1. The checked React property when present\n // 2. The defaultChecked React property when present\n // 3. Otherwise, false\n node.defaultChecked = !node.defaultChecked;\n node.defaultChecked = !!node._wrapperState.initialChecked;\n }\n\n if (name !== '') {\n node.name = name;\n }\n}\nfunction restoreControlledState(element, props) {\n var node = element;\n updateWrapper(node, props);\n updateNamedCousins(node, props);\n}\n\nfunction updateNamedCousins(rootNode, props) {\n var name = props.name;\n\n if (props.type === 'radio' && name != null) {\n var queryRoot = rootNode;\n\n while (queryRoot.parentNode) {\n queryRoot = queryRoot.parentNode;\n } // If `rootNode.form` was non-null, then we could try `form.elements`,\n // but that sometimes behaves strangely in IE8. We could also try using\n // `form.getElementsByName`, but that will only return direct children\n // and won't include inputs that use the HTML5 `form=` attribute. Since\n // the input might not even be in a form. It might not even be in the\n // document. Let's just use the local `querySelectorAll` to ensure we don't\n // miss anything.\n\n\n var group = queryRoot.querySelectorAll('input[name=' + JSON.stringify('' + name) + '][type=\"radio\"]');\n\n for (var i = 0; i < group.length; i++) {\n var otherNode = group[i];\n\n if (otherNode === rootNode || otherNode.form !== rootNode.form) {\n continue;\n } // This will throw if radio buttons rendered by different copies of React\n // and the same name are rendered into the same form (same as #1939).\n // That's probably okay; we don't support it just as we don't support\n // mixing React radio buttons with non-React ones.\n\n\n var otherProps = getFiberCurrentPropsFromNode$1(otherNode);\n\n if (!otherProps) {\n {\n throw Error( \"ReactDOMInput: Mixing React and non-React radio inputs with the same `name` is not supported.\" );\n }\n } // We need update the tracked value on the named cousin since the value\n // was changed but the input saw no event or value set\n\n\n updateValueIfChanged(otherNode); // If this is a controlled radio button group, forcing the input that\n // was previously checked to update will cause it to be come re-checked\n // as appropriate.\n\n updateWrapper(otherNode, otherProps);\n }\n }\n} // In Chrome, assigning defaultValue to certain input types triggers input validation.\n// For number inputs, the display value loses trailing decimal points. For email inputs,\n// Chrome raises \"The specified value is not a valid email address\".\n//\n// Here we check to see if the defaultValue has actually changed, avoiding these problems\n// when the user is inputting text\n//\n// https://github.com/facebook/react/issues/7253\n\n\nfunction setDefaultValue(node, type, value) {\n if ( // Focused number inputs synchronize on blur. See ChangeEventPlugin.js\n type !== 'number' || node.ownerDocument.activeElement !== node) {\n if (value == null) {\n node.defaultValue = toString(node._wrapperState.initialValue);\n } else if (node.defaultValue !== toString(value)) {\n node.defaultValue = toString(value);\n }\n }\n}\n\nvar didWarnSelectedSetOnOption = false;\nvar didWarnInvalidChild = false;\n\nfunction flattenChildren(children) {\n var content = ''; // Flatten children. We'll warn if they are invalid\n // during validateProps() which runs for hydration too.\n // Note that this would throw on non-element objects.\n // Elements are stringified (which is normally irrelevant\n // but matters for ).\n\n React.Children.forEach(children, function (child) {\n if (child == null) {\n return;\n }\n\n content += child; // Note: we don't warn about invalid children here.\n // Instead, this is done separately below so that\n // it happens during the hydration codepath too.\n });\n return content;\n}\n/**\n * Implements an
    NameStatus
    diff --git a/app/views/media_objects/_embed_checkout.html.erb b/app/views/media_objects/_embed_checkout.html.erb index 5dd0b340f5..d77d8d9494 100644 --- a/app/views/media_objects/_embed_checkout.html.erb +++ b/app/views/media_objects/_embed_checkout.html.erb @@ -14,7 +14,7 @@ Unless required by applicable law or agreed to in writing, software distributed --- END LICENSE_HEADER BLOCK --- %> <% if !@masterFiles.blank? %> - <% master_file = @media_object.master_files.first %> + <% master_file = @media_object.sections.first %>
    <% if !current_user %> diff --git a/app/views/media_objects/_item_view.html.erb b/app/views/media_objects/_item_view.html.erb index efb7f1794e..872a449881 100644 --- a/app/views/media_objects/_item_view.html.erb +++ b/app/views/media_objects/_item_view.html.erb @@ -42,8 +42,8 @@ Unless required by applicable law or agreed to in writing, software distributed <%= react_component("MediaObjectRamp", { urls: { base_url: request.protocol + request.host_with_port, fullpath_url: request.fullpath }, - master_files_count: @media_object.master_files.size, - has_structure: @media_object.master_files.any?{ |mf| mf.has_structuralMetadata? }, + sections_count: @media_object.sections.size, + has_structure: @media_object.sections.any?{ |mf| mf.has_structuralMetadata? }, title: { content: render('title') }, share: { canShare: (will_partial_list_render? :share), content: lending_enabled?(@media_object) ? (render('share') if can_stream) : render('share') }, timeline: { canCreate: (current_ability.can? :create, Timeline), content: lending_enabled?(@media_object) ? (render('timeline') if can_stream) : render('timeline') }, @@ -62,7 +62,7 @@ Unless required by applicable law or agreed to in writing, software distributed // display the video player $(document).ready(function () { const mediaObjectId = <%= @media_object.id.to_json.html_safe %>; - const sectionIds = <%= @media_object.ordered_master_file_ids.to_json.html_safe %>; + const sectionIds = <%= @media_object.section_ids.to_json.html_safe %>; const transcriptSections = <%= @media_object.sections_with_files(tag: 'transcript').to_json.html_safe %>; // Enable action buttons after derivative is loaded diff --git a/app/views/media_objects/tree.html.erb b/app/views/media_objects/tree.html.erb index dafa98fafd..4aabbcc28f 100644 --- a/app/views/media_objects/tree.html.erb +++ b/app/views/media_objects/tree.html.erb @@ -16,7 +16,7 @@ Unless required by applicable law or agreed to in writing, software distributed
    • <%=@media_object.title%> (<%=@media_object.id%>)
        - <% @media_object.ordered_master_files.to_a.each do |mf| %> + <% @media_object.sections.each do |mf| %>
      • <%=stream_label_for(mf)%> (<%=mf.id%>)
          <% mf.derivatives.each do |d| %> diff --git a/spec/controllers/catalog_controller_spec.rb b/spec/controllers/catalog_controller_spec.rb index b1188c3e7c..1d8a748e79 100644 --- a/spec/controllers/catalog_controller_spec.rb +++ b/spec/controllers/catalog_controller_spec.rb @@ -190,7 +190,7 @@ before(:each) do @media_object = FactoryBot.create(:fully_searchable_media_object) @master_file = FactoryBot.create(:master_file, :with_structure, media_object: @media_object, title: 'Test Label') - @media_object.ordered_master_files += [@master_file] + @media_object.sections += [@master_file] @media_object.save! # Explicitly run indexing job to ensure fields are indexed for structure searching MediaObjectIndexingJob.perform_now(@media_object.id) diff --git a/spec/controllers/master_files_controller_spec.rb b/spec/controllers/master_files_controller_spec.rb index 6500216700..f6e3e07fd7 100644 --- a/spec/controllers/master_files_controller_spec.rb +++ b/spec/controllers/master_files_controller_spec.rb @@ -100,7 +100,7 @@ file = fixture_file_upload('/videoshort.mp4', 'video/mp4') post :create, params: { Filedata: [file], original: 'any', container_id: media_object.id } - master_file = media_object.reload.ordered_master_files.to_a.first + master_file = media_object.reload.sections.first expect(master_file.file_format).to eq "Moving image" expect(flash[:error]).to be_nil @@ -110,7 +110,7 @@ file = fixture_file_upload('/jazz-performance.mp3', 'audio/mp3') post :create, params: { Filedata: [file], original: 'any', container_id: media_object.id } - master_file = media_object.reload.ordered_master_files.to_a.first + master_file = media_object.reload.sections.first expect(master_file.file_format).to eq "Sound" end @@ -147,7 +147,7 @@ class << file post :create, params: { Filedata: [file], original: 'any', container_id: media_object.id } master_file = MasterFile.all.last - expect(media_object.reload.ordered_master_files.to_a).to include master_file + expect(media_object.reload.sections).to include master_file expect(master_file.media_object.id).to eq(media_object.id) expect(flash[:error]).to be_nil @@ -159,7 +159,7 @@ class << file master_file = MasterFile.all.last media_object.reload - expect(media_object.ordered_master_files.to_a).to include master_file + expect(media_object.sections).to include master_file expect(master_file.media_object.id).to eq(media_object.id) expect(flash[:error]).to be_nil @@ -171,7 +171,7 @@ class << file master_file = MasterFile.all.last media_object.reload - expect(media_object.ordered_master_files.to_a).to include master_file + expect(media_object.sections).to include master_file expect(master_file.media_object.id).to eq(media_object.id) expect(flash[:error]).to be_nil @@ -189,7 +189,7 @@ class << file post :create, params: { Filedata: [file], original: 'any', container_id: media_object.id } master_file = MasterFile.all.last - expect(media_object.reload.ordered_master_files.to_a).to include master_file + expect(media_object.reload.sections).to include master_file expect(master_file.media_object.id).to eq(media_object.id) expect(flash[:error]).to be_nil @@ -206,14 +206,14 @@ class << file login_as :administrator expect(controller.current_ability.can?(:destroy, master_file)).to be_truthy expect { post :destroy, params: { id: master_file.id } }.to change { MasterFile.count }.by(-1) - expect(master_file.media_object.reload.ordered_master_files.to_a).not_to include master_file + expect(master_file.media_object.reload.sections).not_to include master_file end it "deletes a master file as manager of owning collection" do login_user master_file.media_object.collection.managers.first expect(controller.current_ability.can?(:destroy, master_file)).to be_truthy expect { post(:destroy, params: { id: master_file.id }) }.to change { MasterFile.count }.by(-1) - expect(master_file.media_object.reload.ordered_master_files.to_a).not_to include master_file + expect(master_file.media_object.reload.sections).not_to include master_file end it "redirect to restricted content page for end users" do @@ -685,18 +685,16 @@ class << file login_as :administrator end it 'moves the master file' do - expect(current_media_object.master_file_ids).to include master_file.id - expect(current_media_object.ordered_master_file_ids).to include master_file.id - expect(target_media_object.master_file_ids).not_to include master_file.id - expect(target_media_object.ordered_master_file_ids).not_to include master_file.id + expect(current_media_object.section_ids).to include master_file.id + expect(target_media_object.section_ids).not_to include master_file.id post('move', params: { id: master_file.id, target: target_media_object.id }) + current_media_object.reload + target_media_object.reload expect(response).to redirect_to(edit_media_object_path(current_media_object)) expect(flash[:success]).not_to be_blank expect(master_file.reload.media_object_id).to eq target_media_object.id - expect(current_media_object.reload.master_file_ids).not_to include master_file.id - expect(current_media_object.ordered_master_file_ids).not_to include master_file.id - expect(target_media_object.reload.master_file_ids).to include master_file.id - expect(target_media_object.ordered_master_file_ids).to include master_file.id + expect(current_media_object.section_ids).not_to include master_file.id + expect(target_media_object.section_ids).to include master_file.id end end end diff --git a/spec/controllers/media_objects_controller_spec.rb b/spec/controllers/media_objects_controller_spec.rb index 74ee109890..7369ccf240 100644 --- a/spec/controllers/media_objects_controller_spec.rb +++ b/spec/controllers/media_objects_controller_spec.rb @@ -160,7 +160,7 @@ login_as(:administrator) session[:intercom_collections] = {} session[:intercom_default_collection] = '' - media_object.ordered_master_files = [master_file_with_structure] + media_object.sections = [master_file_with_structure] allow_any_instance_of(Avalon::Intercom).to receive(:fetch_user_collections).and_return target_collections end it 'should refetch user collections from target and set session' do @@ -304,7 +304,7 @@ end it "should create a new media_object" do # master_file_obj = FactoryBot.create(:master_file, master_file.slice(:files)) - media_object = FactoryBot.create(:media_object)#, master_files: [master_file_obj]) + media_object = FactoryBot.create(:media_object)#, sections: [master_file_obj]) fields = {other_identifier_type: []} descMetadata_fields.each {|f| fields[f] = media_object.send(f) } # fields = media_object.attributes.select {|k,v| descMetadata_fields.include? k.to_sym } @@ -314,17 +314,17 @@ expect(new_media_object.title).to eq media_object.title expect(new_media_object.creator).to eq media_object.creator expect(new_media_object.date_issued).to eq media_object.date_issued - expect(new_media_object.ordered_master_files.to_a.map(&:id)).to match_array new_media_object.master_file_ids + expect(new_media_object.section_ids.size).to eq 1 expect(new_media_object.duration).to eq '6315' expect(new_media_object.format).to eq ['video/mp4'] expect(new_media_object.avalon_resource_type).to eq ['moving image'] - expect(new_media_object.master_files.first.date_digitized).to eq('2015-12-31T00:00:00Z') - expect(new_media_object.master_files.first.identifier).to include('40000000045312') - expect(new_media_object.master_files.first.structuralMetadata.has_content?).to be_truthy - expect(new_media_object.master_files.first.captions.has_content?).to be_truthy - expect(new_media_object.master_files.first.captions.mime_type).to eq('text/vtt') - expect(new_media_object.master_files.first.derivatives.count).to eq(2) - expect(new_media_object.master_files.first.derivatives.first.location_url).to eq(absolute_location) + expect(new_media_object.sections.first.date_digitized).to eq('2015-12-31T00:00:00Z') + expect(new_media_object.sections.first.identifier).to include('40000000045312') + expect(new_media_object.sections.first.structuralMetadata.has_content?).to be_truthy + expect(new_media_object.sections.first.captions.has_content?).to be_truthy + expect(new_media_object.sections.first.captions.mime_type).to eq('text/vtt') + expect(new_media_object.sections.first.derivatives.count).to eq(2) + expect(new_media_object.sections.first.derivatives.first.location_url).to eq(absolute_location) expect(new_media_object.workflow.last_completed_step).to eq([HYDRANT_STEPS.last.step]) end it "should create a new published media_object" do @@ -406,6 +406,7 @@ # master_file_obj = FactoryBot.create(:master_file, master_file.slice(:files)) media_object = FactoryBot.create(:fully_searchable_media_object, :with_completed_workflow) master_file = FactoryBot.create(:master_file, :with_derivative, :with_thumbnail, :with_poster, :with_structure, :with_captions, :with_comments, media_object: media_object) + media_object.reload allow_any_instance_of(MasterFile).to receive(:extract_frame).and_return('some data') media_object.update_dependent_properties! api_hash = media_object.to_ingest_api_hash @@ -415,7 +416,7 @@ expect(new_media_object.title).to eq media_object.title expect(new_media_object.creator).to eq media_object.creator expect(new_media_object.date_issued).to eq media_object.date_issued - expect(new_media_object.ordered_master_files.to_a.map(&:id)).to match_array new_media_object.master_file_ids + expect(new_media_object.section_ids.size).to eq 1 expect(new_media_object.duration).to eq media_object.duration expect(new_media_object.format).to eq media_object.format expect(new_media_object.note).to eq media_object.note @@ -427,13 +428,13 @@ expect(new_media_object.rights_statement).to eq media_object.rights_statement expect(new_media_object.series).to eq media_object.series expect(new_media_object.avalon_resource_type).to eq media_object.avalon_resource_type - expect(new_media_object.master_files.first.date_digitized).to eq(media_object.master_files.first.date_digitized) - expect(new_media_object.master_files.first.identifier).to eq(media_object.master_files.first.identifier) - expect(new_media_object.master_files.first.structuralMetadata.has_content?).to be_truthy - expect(new_media_object.master_files.first.captions.has_content?).to be_truthy - expect(new_media_object.master_files.first.captions.mime_type).to eq(media_object.master_files.first.captions.mime_type) - expect(new_media_object.master_files.first.derivatives.count).to eq(media_object.master_files.first.derivatives.count) - expect(new_media_object.master_files.first.derivatives.first.location_url).to eq(media_object.master_files.first.derivatives.first.location_url) + expect(new_media_object.sections.first.date_digitized).to eq(media_object.sections.first.date_digitized) + expect(new_media_object.sections.first.identifier).to eq(media_object.sections.first.identifier) + expect(new_media_object.sections.first.structuralMetadata.has_content?).to be_truthy + expect(new_media_object.sections.first.captions.has_content?).to be_truthy + expect(new_media_object.sections.first.captions.mime_type).to eq(media_object.sections.first.captions.mime_type) + expect(new_media_object.sections.first.derivatives.count).to eq(media_object.sections.first.derivatives.count) + expect(new_media_object.sections.first.derivatives.first.location_url).to eq(media_object.sections.first.derivatives.first.location_url) expect(new_media_object.workflow.last_completed_step).to eq(media_object.workflow.last_completed_step) end it "should return 422 if master_file update failed" do @@ -483,29 +484,29 @@ expect(JSON.parse(response.body)['id'].class).to eq String expect(JSON.parse(response.body)).not_to include('errors') media_object.reload - expect(media_object.master_files.to_a.size).to eq 2 + expect(media_object.sections.to_a.size).to eq 2 end it "should update the poster and thumbnail for its masterfile" do media_object = FactoryBot.create(:media_object) put 'json_update', params: { format: 'json', id: media_object.id, files: [master_file], collection_id: media_object.collection_id } media_object.reload - expect(media_object.master_files.to_a.size).to eq 1 - expect(ExtractStillJob).to have_been_enqueued.with(media_object.master_files.first.id, { type: 'both', offset: 2000, headers: nil }) + expect(media_object.sections.to_a.size).to eq 1 + expect(ExtractStillJob).to have_been_enqueued.with(media_object.sections.first.id, { type: 'both', offset: 2000, headers: nil }) end it "should update the waveform for its masterfile" do media_object = FactoryBot.create(:media_object) put 'json_update', params: { format: 'json', id: media_object.id, files: [master_file], collection_id: media_object.collection_id } media_object.reload - expect(media_object.master_files.to_a.size).to eq 1 - expect(WaveformJob).to have_been_enqueued.with(media_object.master_files.first.id) + expect(media_object.sections.to_a.size).to eq 1 + expect(WaveformJob).to have_been_enqueued.with(media_object.sections.first.id) end - it "should delete existing master_files and add a new master_file to a media_object" do + it "should delete existing sections and add a new master_file to a media_object" do allow_any_instance_of(MasterFile).to receive(:stop_processing!) put 'json_update', params: { format: 'json', id: media_object.id, files: [master_file], collection_id: media_object.collection_id, replace_masterfiles: true } expect(JSON.parse(response.body)['id'].class).to eq String expect(JSON.parse(response.body)).not_to include('errors') media_object.reload - expect(media_object.master_files.to_a.size).to eq 1 + expect(media_object.sections.to_a.size).to eq 1 end it "should return 404 if media object doesn't exist" do allow_any_instance_of(MediaObject).to receive(:save).and_return false @@ -814,11 +815,11 @@ it "should choose the correct default master_file" do mf1 = FactoryBot.create(:master_file, media_object: media_object) mf2 = FactoryBot.create(:master_file, media_object: media_object) - expect(media_object.ordered_master_files.to_a.first).to eq(mf1) - media_object.ordered_master_files = media_object.ordered_master_files.to_a.reverse + expect(media_object.sections.first).to eq(mf1) + media_object.sections = media_object.sections.reverse media_object.save! controller.instance_variable_set('@media_object', media_object) - expect(media_object.ordered_master_files.to_a.first).to eq(mf2) + expect(media_object.sections.first).to eq(mf2) expect(controller.send('set_active_file')).to eq(mf2) end end @@ -1199,15 +1200,27 @@ expect(json['published_by']).to eq(media_object.avalon_publisher) expect(json['published']).to eq(media_object.published?) expect(json['summary']).to eq(media_object.abstract) - expect(json['fields'].symbolize_keys).to eq(media_object.to_ingest_api_hash(false)[:fields]) + + # FIXME: https://github.com/avalonmediasystem/avalon/issues/5834 + ingest_api_hash = media_object.to_ingest_api_hash(false) + json['fields'].each do |k,v| + if k == "avalon_resource_type" + expect(v.map(&:downcase)).to eq(ingest_api_hash[:fields][k.to_sym]) + elsif k == "record_identifier" + # no-op since not indexed + else + expect(v).to eq(ingest_api_hash[:fields][k.to_sym]) + end + end + # Symbolize keys for master files and derivatives json['files'].each do |mf| mf.symbolize_keys! mf[:files].each { |d| d.symbolize_keys! } end expect(json['files']).to eq(media_object.to_ingest_api_hash(false)[:files]) - expect(json['files'].first[:id]).to eq(media_object.master_files.first.id) - expect(json['files'].first[:files].first[:id]).to eq(media_object.master_files.first.derivatives.first.id) + expect(json['files'].first[:id]).to eq(media_object.sections.first.id) + expect(json['files'].first[:files].first[:id]).to eq(media_object.sections.first.derivatives.first.id) end it "should return 404 if requested media_object not present" do @@ -1335,18 +1348,18 @@ delete :destroy, params: { id: media_object.id } expect(flash[:notice]).to include("media object deleted") expect(MediaObject.exists?(media_object.id)).to be_falsey - expect(MasterFile.exists?(media_object.master_file_ids.first)).to be_falsey + expect(MasterFile.exists?(media_object.section_ids.first)).to be_falsey end it "should remove a MediaObject with multiple MasterFiles" do media_object = FactoryBot.create(:media_object, :with_master_file, collection: collection) FactoryBot.create(:master_file, media_object: media_object) - master_file_ids = media_object.master_files.map(&:id) + section_ids = media_object.section_ids media_object.reload delete :destroy, params: { id: media_object.id } expect(flash[:notice]).to include("media object deleted") expect(MediaObject.exists?(media_object.id)).to be_falsey - master_file_ids.each { |mf_id| expect(MasterFile.exists?(mf_id)).to be_falsey } + section_ids.each { |mf_id| expect(MasterFile.exists?(mf_id)).to be_falsey } end it "should fail when id doesn't exist" do @@ -1534,14 +1547,14 @@ mf.media_object = media_object mf.save end - master_file_ids = media_object.ordered_master_files.to_a.collect(&:id) + section_ids = media_object.section_ids media_object.save login_user media_object.collection.managers.first - put 'update', params: { :id => media_object.id, :master_file_ids => master_file_ids.reverse, :step => 'structure' } + put 'update', params: { :id => media_object.id, :master_file_ids => section_ids.reverse, :step => 'structure' } media_object.reload - expect(media_object.ordered_master_files.to_a.collect(&:id)).to eq master_file_ids.reverse + expect(media_object.section_ids).to eq section_ids.reverse end it 'sets the MIME type' do media_object = FactoryBot.create(:media_object) @@ -1569,11 +1582,11 @@ it "should update all the labels" do login_user media_object.collection.managers.first part_params = {} - media_object.ordered_master_files.to_a.each_with_index { |mf,i| part_params[mf.id] = { id: mf.id, title: "Part #{i}", permalink: '', poster_offset: '00:00:00.000' } } + media_object.sections.each_with_index { |mf,i| part_params[mf.id] = { id: mf.id, title: "Part #{i}", permalink: '', poster_offset: '00:00:00.000' } } params = { id: media_object.id, master_files: part_params, save: 'Save', step: 'file-upload', donot_advance: 'true' } patch 'update', params: params media_object.reload - media_object.ordered_master_files.to_a.each_with_index do |mf,i| + media_object.sections.each_with_index do |mf,i| expect(mf.title).to eq "Part #{i}" end end @@ -1692,19 +1705,19 @@ end describe "#show_progress" do - it "should return information about the processing state of the media object's master_files" do + it "should return information about the processing state of the media object's sections" do media_object = FactoryBot.create(:media_object, :with_master_file) login_as 'administrator' get :show_progress, params: { id: media_object.id, format: 'json' } expect(JSON.parse(response.body)["overall"]).to_not be_empty end - it "should return information about the processing state of the media object's master_files for managers" do + it "should return information about the processing state of the media object's sections for managers" do media_object = FactoryBot.create(:media_object, :with_master_file) login_user media_object.collection.managers.first get :show_progress, params: { id: media_object.id, format: 'json' } expect(JSON.parse(response.body)["overall"]).to_not be_empty end - it "should return something even if the media object has no master_files" do + it "should return something even if the media object has no sections" do media_object = FactoryBot.create(:media_object) login_as 'administrator' get :show_progress, params: { id: media_object.id, format: 'json' } @@ -1728,18 +1741,18 @@ let(:playlist) { FactoryBot.create(:playlist, user: user) } before do - media_object.ordered_master_files = [master_file, master_file_with_structure] + media_object.sections = [master_file, master_file_with_structure] end it "should create a single playlist_item for a single master_file" do expect { - post :add_to_playlist, params: { id: media_object.id, post: { playlist_id: playlist.id, masterfile_id: media_object.ordered_master_file_ids[0], playlistitem_scope: 'section' } } + post :add_to_playlist, params: { id: media_object.id, post: { playlist_id: playlist.id, masterfile_id: media_object.section_ids[0], playlistitem_scope: 'section' } } }.to change { playlist.items.count }.from(0).to(1) - expect(playlist.items[0].title).to eq("#{media_object.title} - #{media_object.ordered_master_files.to_a[0].title}") + expect(playlist.items[0].title).to eq("#{media_object.title} - #{media_object.sections[0].title}") end it "should create playlist_items for each span in a single master_file's structure" do expect { - post :add_to_playlist, params: { id: media_object.id, post: { playlist_id: playlist.id, masterfile_id: media_object.ordered_master_file_ids[1], playlistitem_scope: 'structure' } } + post :add_to_playlist, params: { id: media_object.id, post: { playlist_id: playlist.id, masterfile_id: media_object.section_ids[1], playlistitem_scope: 'structure' } } }.to change { playlist.items.count }.from(0).to(13) expect(playlist.items[0].title).to eq("Test Item - CD 1 - Copland, Three Piano Excerpts from Our Town - Track 1. Story of Our Town") expect(playlist.items[12].title).to eq("Test Item - CD 1 - Track 13. Copland, Danzon Cubano") @@ -1748,21 +1761,21 @@ expect { post :add_to_playlist, params: { id: media_object.id, post: { playlist_id: playlist.id, playlistitem_scope: 'section' } } }.to change { playlist.items.count }.from(0).to(2) - expect(playlist.items[0].title).to eq(media_object.ordered_master_files.to_a[0].embed_title) - expect(playlist.items[1].title).to eq(media_object.ordered_master_files.to_a[1].embed_title) + expect(playlist.items[0].title).to eq(media_object.sections[0].embed_title) + expect(playlist.items[1].title).to eq(media_object.sections[1].embed_title) end it "should create playlist_items for each span in a master_file structures in a media_object" do expect { post :add_to_playlist, params: { id: media_object.id, post: { playlist_id: playlist.id, playlistitem_scope: 'structure' } } }.to change { playlist.items.count }.from(0).to(14) expect(response.response_code).to eq(200) - expect(playlist.items[0].title).to eq("#{media_object.title} - #{media_object.ordered_master_files.to_a[0].title}") + expect(playlist.items[0].title).to eq("#{media_object.title} - #{media_object.sections[0].title}") expect(playlist.items[13].title).to eq("Test Item - CD 1 - Track 13. Copland, Danzon Cubano") end it 'redirects with flash message when playlist is owned by another user' do login_as :user other_playlist = FactoryBot.create(:playlist) - post :add_to_playlist, params: { id: media_object.id, post: { playlist_id: other_playlist.id, masterfile_id: media_object.ordered_master_file_ids[0], playlistitem_scope: 'section' } } + post :add_to_playlist, params: { id: media_object.id, post: { playlist_id: other_playlist.id, masterfile_id: media_object.section_ids[0], playlistitem_scope: 'section' } } expect(response).to have_http_status(403) expect(JSON.parse(response.body).symbolize_keys).to eq({message: "

          You are not authorized to update this playlist.

          ", status: 403}) end @@ -1772,7 +1785,7 @@ media_object perform_enqueued_jobs(only: MediaObjectIndexingJob) WebMock.reset_executed_requests! - post :add_to_playlist, params: { id: media_object.id, post: { playlist_id: playlist.id, masterfile_id: media_object.ordered_master_file_ids[0], playlistitem_scope: 'section' } } + post :add_to_playlist, params: { id: media_object.id, post: { playlist_id: playlist.id, masterfile_id: media_object.section_ids[0], playlistitem_scope: 'section' } } expect(a_request(:any, /#{ActiveFedora.fedora.base_uri}/)).not_to have_been_made end end diff --git a/spec/factories/media_objects.rb b/spec/factories/media_objects.rb index d1eec6d595..f023a97e04 100644 --- a/spec/factories/media_objects.rb +++ b/spec/factories/media_objects.rb @@ -49,6 +49,7 @@ rights_statement { ['http://rightsstatements.org/vocab/InC-EDU/1.0/'] } terms_of_use { [ 'Terms of Use: Be kind. Rewind.' ] } series { [Faker::Lorem.word] } + sections { [] } # after(:create) do |mo| # mo.update_datastream(:descMetadata, { # note: {note[Faker::Lorem.paragraph], @@ -65,9 +66,7 @@ after(:create) do |mo| mf = FactoryBot.create(:master_file) mf.media_object = mo - mf.save - # mo.ordered_master_files += [mf] - mo.save + # Above line will cause a save of both the master file and parent media object end end trait :with_completed_workflow do diff --git a/spec/features/login_redirect_spec.rb b/spec/features/login_redirect_spec.rb index e77f9c5b0e..d61beec9d2 100644 --- a/spec/features/login_redirect_spec.rb +++ b/spec/features/login_redirect_spec.rb @@ -18,12 +18,12 @@ let(:user) { FactoryBot.create(:user, :with_identity) } describe '/media_objects/:id' do - let(:media_object) { FactoryBot.create(:fully_searchable_media_object, master_files: [master_file]) } - let(:master_file) { FactoryBot.create(:master_file, :with_derivative) } + let(:media_object) { FactoryBot.create(:fully_searchable_media_object) } + let!(:master_file) { FactoryBot.create(:master_file, :with_derivative, media_object: media_object) } it 'redirects to item page' do visit media_object_path(media_object) - visit hls_manifest_master_file_path(media_object.master_files.first, "high") + visit hls_manifest_master_file_path(media_object.sections.first, "high") sign_in user expect(page.current_path).to eq media_object_path(media_object) end diff --git a/spec/helpers/media_objects_helper_spec.rb b/spec/helpers/media_objects_helper_spec.rb index b0ae812285..7a95375fd8 100644 --- a/spec/helpers/media_objects_helper_spec.rb +++ b/spec/helpers/media_objects_helper_spec.rb @@ -62,17 +62,17 @@ describe '#gather_all_comments' do let(:media_object) { instance_double("MediaObject", comment: ['MO Comment']) } - let(:master_files) { [instance_double("MasterFile", comment: [])] } + let(:sections) { [instance_double("MasterFile", comment: [])] } it 'returns a list of unique comment strings' do - expect(helper.gather_all_comments(media_object, master_files)).to eq ["MO Comment"] + expect(helper.gather_all_comments(media_object, sections)).to eq ["MO Comment"] end context 'with a master file comment' do - let(:master_files) { [instance_double("MasterFile", comment: ["MF Comment"], display_title: "MF1")] } + let(:sections) { [instance_double("MasterFile", comment: ["MF Comment"], display_title: "MF1")] } it 'returns a list of unique comment strings' do - expect(helper.gather_all_comments(media_object, master_files)).to eq ["MO Comment", "[MF1] MF Comment"] + expect(helper.gather_all_comments(media_object, sections)).to eq ["MO Comment", "[MF1] MF Comment"] end end end diff --git a/spec/jobs/media_object_indexing_job_spec.rb b/spec/jobs/media_object_indexing_job_spec.rb index 15ffad9f87..cfa79b97d1 100644 --- a/spec/jobs/media_object_indexing_job_spec.rb +++ b/spec/jobs/media_object_indexing_job_spec.rb @@ -18,15 +18,16 @@ let(:job) { MediaObjectIndexingJob.new } describe "perform" do + let(:comment) { "A comment" } let!(:media_object) { FactoryBot.create(:media_object) } - let!(:master_file) { FactoryBot.create(:master_file, media_object: media_object) } + let!(:master_file) { FactoryBot.create(:master_file, media_object: media_object, comment: [comment]) } it 'indexes the media object including master_file fields' do before_doc = ActiveFedora::SolrService.query("id:#{media_object.id}").first - expect(before_doc["section_id_ssim"]).to be_blank + expect(before_doc["all_comments_ssim"]).to be_blank job.perform(media_object.id) after_doc = ActiveFedora::SolrService.query("id:#{media_object.id}").first - expect(after_doc["section_id_ssim"]).to eq [master_file.id] + expect(after_doc["all_comments_ssim"]).to eq [comment] end end end diff --git a/spec/lib/avalon/batch/entry_spec.rb b/spec/lib/avalon/batch/entry_spec.rb index f39b46119c..ee67569b65 100644 --- a/spec/lib/avalon/batch/entry_spec.rb +++ b/spec/lib/avalon/batch/entry_spec.rb @@ -164,7 +164,7 @@ describe '#process!' do let(:entry_files) { [{ file: File.join(testdir, filename), offset: '00:00:00.500', label: 'Quis quo', date_digitized: '2015-10-30', skip_transcoding: false }] } - let(:master_file) { entry.media_object.master_files.first } + let(:master_file) { entry.media_object.sections.first } before do entry.process! end diff --git a/spec/lib/avalon/intercom_spec.rb b/spec/lib/avalon/intercom_spec.rb index a92a30d0a4..6feb06963d 100644 --- a/spec/lib/avalon/intercom_spec.rb +++ b/spec/lib/avalon/intercom_spec.rb @@ -86,7 +86,7 @@ expect(response).to eq({ message: 'StandardError', status: 500 }) end it "should respond with a link to the pushed object on target" do - media_object.ordered_master_files=[master_file_with_structure] + media_object.sections=[master_file_with_structure] media_object_hash = media_object.to_ingest_api_hash(false) media_object_hash.merge!( { 'collection_id' => 'cupcake_collection', 'import_bib_record' => true, 'publish' => false } diff --git a/spec/models/batch_entries_spec.rb b/spec/models/batch_entries_spec.rb index 8d8e88c8b5..abc115ec08 100644 --- a/spec/models/batch_entries_spec.rb +++ b/spec/models/batch_entries_spec.rb @@ -106,7 +106,7 @@ media_object = FactoryBot.create(:media_object) batch_entry = FactoryBot.build(:batch_entries, media_object_pid: media_object.id) - expect(media_object.master_files.to_a.count).to eq(0) + expect(media_object.sections.count).to eq(0) expect(JSON.parse(batch_entry.payload)['files'].count).to eq(1) expect(batch_entry.encoding_finished?).to be_truthy @@ -119,7 +119,7 @@ media_object = master_file.media_object batch_entry = FactoryBot.build(:batch_entries, media_object_pid: media_object.id) - expect(media_object.master_files.to_a.count).to eq(1) + expect(media_object.sections.count).to eq(1) expect(JSON.parse(batch_entry.payload)['files'].count).to eq(1) expect(batch_entry.encoding_finished?).to be_truthy @@ -132,7 +132,7 @@ media_object = master_file.media_object batch_entry = FactoryBot.build(:batch_entries, media_object_pid: media_object.id) - expect(media_object.master_files.to_a.count).to eq(1) + expect(media_object.sections.count).to eq(1) expect(JSON.parse(batch_entry.payload)['files'].count).to eq(1) expect(batch_entry.encoding_finished?).to be_truthy @@ -145,7 +145,7 @@ media_object = master_file.media_object batch_entry = FactoryBot.build(:batch_entries, media_object_pid: media_object.id) - expect(media_object.master_files.to_a.count).to eq(1) + expect(media_object.sections.count).to eq(1) expect(JSON.parse(batch_entry.payload)['files'].count).to eq(1) expect(batch_entry.encoding_finished?).to be_truthy @@ -158,7 +158,7 @@ media_object = master_file.media_object batch_entry = FactoryBot.build(:batch_entries, media_object_pid: media_object.id) - expect(media_object.master_files.to_a.count).to eq(1) + expect(media_object.sections.count).to eq(1) expect(JSON.parse(batch_entry.payload)['files'].count).to eq(1) expect(batch_entry.encoding_finished?).to be_falsey diff --git a/spec/models/file_upload_step_spec.rb b/spec/models/file_upload_step_spec.rb index 2d4959e5bf..5ee88ae891 100644 --- a/spec/models/file_upload_step_spec.rb +++ b/spec/models/file_upload_step_spec.rb @@ -16,8 +16,8 @@ describe FileUploadStep do describe '#update_master_files' do - let!(:master_file) {FactoryBot.create(:master_file, title: 'foo')} - let!(:media_object) {FactoryBot.create(:media_object, master_files: [master_file])} + let(:master_file) {FactoryBot.create(:master_file, title: 'foo')} + let(:media_object) {FactoryBot.create(:media_object, sections: [master_file])} it 'should not regenerate a section permalink when the title is changed' do step_context = {media_object: media_object, master_files: {master_file.id => {title: 'new title'}}} expect{FileUploadStep.new.update_master_files(step_context)}.to_not change{master_file.permalink} diff --git a/spec/models/ingest_batch_spec.rb b/spec/models/ingest_batch_spec.rb index cf643c6d1a..159f6cc953 100644 --- a/spec/models/ingest_batch_spec.rb +++ b/spec/models/ingest_batch_spec.rb @@ -22,22 +22,16 @@ expect(ingest_batch.media_object_ids).to eq(media_object_ids) end describe '#finished?' do - it 'returns true when all the master files are finished' do - media_object = FactoryBot.create(:media_object, master_files: [FactoryBot.create(:master_file, :cancelled_processing), FactoryBot.create(:master_file, :completed_processing)]) + it 'returns true when all the sections are finished' do + media_object = FactoryBot.create(:media_object, sections: [FactoryBot.create(:master_file, :cancelled_processing), FactoryBot.create(:master_file, :completed_processing)]) ingest_batch = IngestBatch.new(media_object_ids: [media_object.id], email: 'email@something.com') expect(ingest_batch.finished?).to be true end - # fix: adding master_files to media object parts is broken - it 'returns false when one or more master files are not finished' do - skip "Fix problems with this test" - - media_object = MediaObject.new(id:'avalon:ingest-batch-test') - media_object.add_relationship(:has_part, FactoryBot.build(:master_file, :completed_processing)) - media_object.parts << FactoryBot.build(:master_file) - media_object.save - ingest_batch = IngestBatch.new(media_object_ids: ['avalon:ingest-batch-test'], email: 'email@something.com') - expect(ingest_batch.finished?).to be true + it 'returns false when one or more sections are not finished' do + media_object = FactoryBot.create(:media_object, sections: [FactoryBot.create(:master_file), FactoryBot.create(:master_file)]) + ingest_batch = IngestBatch.new(media_object_ids: [media_object.id], email: 'email@something.com') + expect(ingest_batch.finished?).to be false end end diff --git a/spec/models/master_file_spec.rb b/spec/models/master_file_spec.rb index ef1d0ae5e3..9c00267597 100644 --- a/spec/models/master_file_spec.rb +++ b/spec/models/master_file_spec.rb @@ -84,7 +84,7 @@ end end - describe "master_files=" do + describe "derivatives=" do let(:derivative) {Derivative.create} let(:master_file) {FactoryBot.create(:master_file)} it "should set hasDerivation relationships on self" do @@ -329,14 +329,9 @@ let(:media_path) { File.expand_path("../../master_files-#{SecureRandom.uuid}",__FILE__)} let(:dropbox_path) { File.expand_path("../../collection-#{SecureRandom.uuid}",__FILE__)} let(:upload) { ActionDispatch::Http::UploadedFile.new :tempfile => tempfile, :filename => original, :type => 'video/mp4' } - let(:media_object) { MediaObject.new } + let!(:media_object) { FactoryBot.create(:media_object, sections: [subject]) } let(:collection) { Admin::Collection.new } - subject { - mf = MasterFile.new - mf.media_object = media_object - mf.setContent(upload, dropbox_dir: collection.dropbox_absolute_path) - mf - } + subject { FactoryBot.create(:master_file) } before(:each) do @old_media_path = Settings.encoding.working_file_path @@ -356,11 +351,13 @@ it "should move an uploaded file into the root of the collection's dropbox" do Settings.encoding.working_file_path = nil + subject.setContent(upload, dropbox_dir: collection.dropbox_absolute_path) expect(subject.file_location).to eq(File.realpath(File.join(collection.dropbox_absolute_path,original))) end it "should copy an uploaded file to the media path" do Settings.encoding.working_file_path = media_path + subject.setContent(upload, dropbox_dir: collection.dropbox_absolute_path) expect(File.fnmatch("#{media_path}/*/#{original}", subject.working_file_path.first)).to be true end @@ -373,6 +370,7 @@ it "appends a numerical suffix" do Settings.encoding.working_file_path = nil + subject.setContent(upload, dropbox_dir: collection.dropbox_absolute_path) expect(subject.file_location).to eq(File.realpath(File.join(collection.dropbox_absolute_path,duplicate))) end end @@ -384,14 +382,9 @@ let(:dropbox_file_path) { File.join(dropbox_path, 'nested-dir', original)} let(:media_path) { File.expand_path("../../master_files-#{SecureRandom.uuid}",__FILE__)} let(:dropbox_path) { File.expand_path("../../collection-#{SecureRandom.uuid}",__FILE__)} - let(:media_object) { MediaObject.new } + let!(:media_object) { FactoryBot.create(:media_object, sections: [subject]) } let(:collection) { Admin::Collection.new } - subject { - mf = MasterFile.new - mf.media_object = media_object - mf.setContent(File.new(dropbox_file_path), dropbox_dir: collection.dropbox_absolute_path) - mf - } + subject { FactoryBot.create(:master_file) } before(:each) do @old_media_path = Settings.encoding.working_file_path @@ -412,6 +405,7 @@ it "should not move a file in a subdirectory of the collection's dropbox" do Settings.encoding.working_file_path = nil + subject.setContent(File.new(dropbox_file_path), dropbox_dir: collection.dropbox_absolute_path) expect(subject.file_location).to eq dropbox_file_path expect(File.exist?(dropbox_file_path)).to eq true expect(File.exist?(File.join(collection.dropbox_absolute_path,original))).to eq false @@ -419,6 +413,7 @@ it "should copy an uploaded file to the media path" do Settings.encoding.working_file_path = media_path + subject.setContent(File.new(dropbox_file_path), dropbox_dir: collection.dropbox_absolute_path) expect(File.fnmatch("#{media_path}/*/#{original}", subject.working_file_path.first)).to be true end end @@ -429,7 +424,7 @@ let(:file_size) { 12345 } let(:auth_header) { {"Authorization"=>"Bearer ya29.a0AfH6SMC6vSj4D6po1aDxAr6JmY92azh3lxevSuPKxf9QPPSKmMzqbZvI7B3oIACqqMVono1P0XD2F1Jl_rkayoI6JGz-P2cpg44-55oJFcWychAvUliWeRKf1cifMo9JF10YmXxhIfrG5mu7Ahy9FZpudN92p2JhvTI"} } - subject { MasterFile.new } + subject { FactoryBot.create(:master_file) } it "should set the right properties" do allow(subject).to receive(:reloadTechnicalMetadata!).and_return(nil) @@ -517,8 +512,8 @@ end it 'should have an appropriate title for the embed code with no label (more than 1 section)' do - allow(subject.media_object).to receive(:ordered_master_files).and_return([subject,subject]) - allow(subject.media_object).to receive(:master_file_ids).and_return([subject.id,subject.id]) + allow(subject.media_object).to receive(:sections).and_return([subject,subject]) + allow(subject.media_object).to receive(:section_ids).and_return([subject.id,subject.id]) expect( subject.embed_title ).to eq( 'test - video.mp4' ) end @@ -759,9 +754,13 @@ let(:master_file) { FactoryBot.build(:master_file) } before do allow(ActiveEncode::Base).to receive(:find).and_return(nil) + allow(master_file).to receive(:finished_processing?).and_return(false) end it 'does not error if the master file has no encode' do - expect { master_file.send(:stop_processing!) }.not_to raise_error + expect { master_file.stop_processing! }.not_to raise_error + end + it 'enqueues cancel job if currently processing' do + expect { master_file.stop_processing! }.to have_enqueued_job(ActiveEncodeJobs::CancelEncodeJob) end end @@ -802,10 +801,12 @@ it 'sets a new media object as its parent' do master_file.media_object = media_object2 - expect(media_object1.reload.master_file_ids).not_to include master_file.id - expect(media_object1.reload.ordered_master_file_ids).not_to include master_file.id - expect(media_object2.reload.master_file_ids).to include master_file.id - expect(media_object2.reload.ordered_master_file_ids).to include master_file.id + media_object1.reload + media_object2.reload + expect(media_object1.master_file_ids).not_to include master_file.id + expect(media_object1.section_ids).not_to include master_file.id + expect(media_object2.master_file_ids).to include master_file.id + expect(media_object2.section_ids).to include master_file.id end end diff --git a/spec/models/media_object_spec.rb b/spec/models/media_object_spec.rb index 33da0c443e..65afba3c18 100644 --- a/spec/models/media_object_spec.rb +++ b/spec/models/media_object_spec.rb @@ -456,13 +456,13 @@ describe '#finished_processing?' do it 'returns true if the statuses indicate processing is finished' do - media_object.ordered_master_files += [FactoryBot.create(:master_file, :cancelled_processing)] - media_object.ordered_master_files += [FactoryBot.create(:master_file, :completed_processing)] + media_object.sections += [FactoryBot.create(:master_file, :cancelled_processing)] + media_object.sections += [FactoryBot.create(:master_file, :completed_processing)] expect(media_object.finished_processing?).to be true end it 'returns true if the statuses indicate processing is not finished' do - media_object.ordered_master_files += [FactoryBot.create(:master_file, :cancelled_processing)] - media_object.ordered_master_files += [FactoryBot.create(:master_file)] + media_object.sections += [FactoryBot.create(:master_file, :cancelled_processing)] + media_object.sections += [FactoryBot.create(:master_file)] expect(media_object.finished_processing?).to be false end end @@ -471,10 +471,11 @@ let(:master_file1) { FactoryBot.create(:master_file, media_object: media_object, duration: '40') } let(:master_file2) { FactoryBot.create(:master_file, media_object: media_object, duration: '40') } let(:master_file3) { FactoryBot.create(:master_file, media_object: media_object, duration: nil) } - let(:master_files) { [] } + let(:sections) { [] } before do - master_files + sections + media_object.reload # Explicitly run indexing job to ensure fields are indexed for structure searching MediaObjectIndexingJob.perform_now(media_object.id) end @@ -485,19 +486,19 @@ end end context 'with two master files' do - let(:master_files) { [master_file1, master_file2] } + let(:sections) { [master_file1, master_file2] } it 'returns the correct duration' do expect(media_object.send(:calculate_duration)).to eq(80) end end context 'with two master files one nil' do - let(:master_files) { [master_file1, master_file3] } + let(:sections) { [master_file1, master_file3] } it 'returns the correct duration' do expect(media_object.send(:calculate_duration)).to eq(40) end end context 'with one master file that is nil' do - let(:master_files) { [master_file3] } + let(:sections) { [master_file3] } it 'returns the correct duration' do expect(media_object.send(:calculate_duration)).to eq(0) end @@ -506,21 +507,21 @@ describe '#destroy' do let(:media_object) { FactoryBot.create(:media_object, :with_master_file) } - let(:master_file) { media_object.master_files.first } + let(:master_file) { media_object.sections.first } before do allow(master_file).to receive(:stop_processing!) end - it 'destroys related master_files' do + it 'destroys related sections' do expect { media_object.destroy }.to change { MasterFile.exists?(master_file) }.from(true).to(false) end it 'destroys multiple sections' do FactoryBot.create(:master_file, media_object: media_object) media_object.reload - expect(media_object.master_files.size).to eq 2 - media_object.master_files.each do |mf| + expect(media_object.sections.size).to eq 2 + media_object.sections.each do |mf| allow(mf).to receive(:stop_processing!) end expect { media_object.destroy }.to change { MasterFile.count }.from(2).to(0) @@ -818,7 +819,7 @@ mf2 = FactoryBot.create(:master_file, title: 'Test Label2', physical_description: 'cave paintings', media_object: media_object) media_object.reload - #expect(media_object.ordered_master_files.size).to eq(2) + #expect(media_object.sections.size).to eq(2) expect(media_object.section_physical_descriptions).to match(['cave paintings']) end end @@ -854,10 +855,10 @@ describe 'descMetadata' do it 'sets original_name to default value' do - expect(media_object.descMetadata.original_name).to eq 'descMetadata.xml' + # requires a reload now? + expect(media_object.reload.descMetadata.original_name).to eq 'descMetadata.xml' end it 'is a valid MODS document' do - media_object = FactoryBot.create(:media_object, :with_master_file) xsd_path = File.join(Rails.root, 'spec', 'fixtures', 'mods-3-6.xsd') # Note: we instantiate Schema with a file handle so that relative paths # to included schema definitions can be resolved @@ -995,7 +996,7 @@ context "no error" do it 'merges' do - expect { media_object.merge! media_objects }.to change { media_object.master_files.to_a.count }.by(2) + expect { media_object.merge! media_objects }.to change { media_object.sections.count }.by(2) expect(media_objects.any? { |mo| MediaObject.exists?(mo.id) }).to be_falsey end end @@ -1011,7 +1012,7 @@ expect(fails).to eq([media_objects.first]) expect(media_objects.first.errors.count).to eq(1) - expect(media_object.master_files.to_a.count).to eq(2) + expect(media_object.sections.count).to eq(2) expect(MediaObject.exists?(media_objects.first.id)).to be_truthy expect(MediaObject.exists?(media_objects.second.id)).to be_falsey end @@ -1165,7 +1166,7 @@ describe "#has_captions" do let(:captionless_media_object) { FactoryBot.create(:media_object, :with_master_file) } - let(:captioned_media_object) { FactoryBot.create(:media_object, master_files: [master_file1, master_file2]) } + let(:captioned_media_object) { FactoryBot.create(:media_object, sections: [master_file1, master_file2]) } let(:master_file1) { FactoryBot.create(:master_file) } let(:master_file2) { FactoryBot.create(:master_file, :with_captions) } it "returns false when child master files contain no captions" do @@ -1179,7 +1180,7 @@ describe "#has_transcripts" do let(:transcriptless_media_object) { FactoryBot.create(:media_object, :with_master_file) } - let(:transcript_media_object) { FactoryBot.create(:media_object, master_files: [master_file1, master_file2]) } + let(:transcript_media_object) { FactoryBot.create(:media_object, sections: [master_file1, master_file2]) } let(:master_file1) { FactoryBot.create(:master_file) } let(:master_file2) { FactoryBot.create(:master_file, supplemental_files: [transcript]) } let(:transcript) { FactoryBot.create(:supplemental_file, :with_transcript_tag, :with_transcript_file) } @@ -1191,4 +1192,103 @@ expect(transcript_media_object.has_transcripts).to be true end end + + describe 'section_list' do + let(:section) { FactoryBot.create(:master_file) } + let(:section2) { FactoryBot.create(:master_file) } + let!(:media_object) { FactoryBot.create(:media_object, master_files: [section2, section], sections: [section, section2]) } + + describe 'section_ids' do + it 'returns an ordered list of master file ids' do + expect(media_object.section_ids).to eq [section.id, section2.id] + end + end + + describe 'section_ids=' do + it 'sets ordered list of master file ids without modifying master_file_ids' do + expect(media_object.master_file_ids).to contain_exactly(section.id, section2.id) + expect(media_object.section_ids).to eq [section.id, section2.id] + media_object.section_ids = [section2.id] + expect(media_object.master_file_ids).to contain_exactly(section.id, section2.id) + expect(media_object.section_ids).to eq [section2.id] + end + end + + describe 'sections' do + it 'returns an ordered list of master file objects' do + expect(media_object.sections).to eq [section, section2] + end + end + + describe 'sections=' do + it 'sets ordered list of master file objects without modifying master_file_ids' do + expect(media_object.master_files).to contain_exactly(section, section2) + expect(media_object.sections).to eq [section, section2] + media_object.sections = [section2] + expect(media_object.master_files).to contain_exactly(section, section2) + expect(media_object.sections).to eq [section2] + end + end + + it '#sections and #section_ids stay sync' do + expect(media_object.section_ids).to eq [section.id, section2.id] + expect(media_object.sections).to eq [section, section2] + media_object.sections = [section2] + expect(media_object.section_ids).to eq [section2.id] + expect(media_object.sections).to eq [section2] + media_object.section_ids = [section.id] + expect(media_object.section_ids).to eq [section.id] + expect(media_object.sections).to eq [section] + end + + context 'migrating ordered_aggregation' do + let!(:media_object) do + mo = FactoryBot.build(:media_object) + mo.ordered_master_files = [section, section2] + # Trick the callback to avoid persisting section_list + mo.instance_variable_set(:@section_ids, mo.master_file_ids) + mo.save + mo.reload + end + + it 'reads from ordered_aggregation' do + expect(media_object.ordered_master_files.to_a).to eq [section, section2] + expect(media_object.section_list).to eq nil + mo = MediaObject.find(media_object.id) + expect(mo.section_list).not_to eq nil + expect(mo.ordered_master_files.to_a).to eq [section, section2] + expect(mo.sections).to eq mo.ordered_master_files.to_a + expect(mo.section_ids).to eq mo.ordered_master_file_ids + end + + it 'prefers reading from section_list when set' do + expect(media_object.section_list).to eq nil + mo = MediaObject.find(media_object.id) + new_section = FactoryBot.create(:master_file) + mo.sections += [new_section] + mo.save + mo = MediaObject.find(media_object.id) + expect(mo.section_list).not_to eq nil + expect(mo.sections).not_to eq mo.ordered_master_files.to_a + expect(mo.section_ids).to eq [section.id, section2.id, new_section.id] + end + end + end + + describe '#reload' do + let(:section) { FactoryBot.create(:master_file) } + + context 'resets cached values' do + it 'resets sections' do + expect(media_object.sections).to eq [] + expect(media_object.section_ids).to eq [] + media_object.sections += [section] + expect(media_object.sections).to eq [section] + expect(media_object.section_ids).to eq [section.id] + media_object.reload + expect(media_object.sections).to eq [] + expect(media_object.section_ids).to eq [] + end + end + end end diff --git a/spec/models/working_file_path_spec.rb b/spec/models/working_file_path_spec.rb index 050e0d975d..2e410837f7 100644 --- a/spec/models/working_file_path_spec.rb +++ b/spec/models/working_file_path_spec.rb @@ -93,7 +93,7 @@ it 'sends the working_file_path to active_encode' do MasterFileBuilder.build(media_object, params) - master_file = media_object.reload.master_files.first + master_file = media_object.reload.sections.first expect(File.exist? master_file.working_file_path.first).to be true input = FileLocator.new(master_file.working_file_path.first).uri.to_s expect(encoder_class).to have_received(:create).with(input, { headers: nil, master_file_id: master_file.id, preset: workflow }) @@ -104,7 +104,7 @@ it 'sends the working_file_path to active_encode' do MasterFileBuilder.build(media_object, params) - master_file = media_object.reload.master_files.first + master_file = media_object.reload.sections.first expect(File.exist? master_file.working_file_path.first).to be true input_path = FileLocator.new(master_file.working_file_path.first).uri.to_s expect(encoder_class).to have_received(:create).with(input_path, master_file_id: master_file.id, outputs: [{ label: "high", url: input_path }], preset: "pass_through") @@ -119,7 +119,7 @@ it 'sends the working_file_path to active_encode' do MasterFileBuilder.build(media_object, params) - master_file = media_object.reload.master_files.first + master_file = media_object.reload.sections.first expect(File.exist? master_file.working_file_path.first).to be true input = FileLocator.new(master_file.working_file_path.first).uri.to_s expect(encoder_class).to have_received(:create).with(input, { headers: nil, master_file_id: master_file.id, preset: workflow }) @@ -130,7 +130,7 @@ it 'sends the working_file_path to active_encode' do MasterFileBuilder.build(media_object, params) - master_file = media_object.reload.master_files.first + master_file = media_object.reload.sections.first expect(File.exist? master_file.working_file_path.first).to be true input_path = FileLocator.new(master_file.working_file_path.first).uri.to_s expect(encoder_class).to have_received(:create).with(input_path, master_file_id: master_file.id, outputs: [{ label: "high", url: input_path }], preset: "pass_through") @@ -152,7 +152,7 @@ it 'sends the working_file_path to active_encode' do entry.process! - master_file = media_object.reload.master_files.first + master_file = media_object.reload.sections.first expect(File.exist? master_file.working_file_path.first).to be true input = FileLocator.new(master_file.working_file_path.first).uri.to_s expect(encoder_class).to have_received(:create).with(input, { headers: nil, master_file_id: master_file.id, preset: workflow }) @@ -163,7 +163,7 @@ it 'sends the working_file_path to active_encode' do entry.process! - master_file = media_object.reload.master_files.first + master_file = media_object.reload.sections.first expect(File.exist? master_file.working_file_path.first).to be true input_path = FileLocator.new(master_file.working_file_path.first).uri.to_s expect(encoder_class).to have_received(:create).with(input_path, master_file_id: master_file.id, outputs: [{ label: "high", url: input_path }], preset: "pass_through" ) @@ -191,7 +191,7 @@ # TODO: Ensure all working file copies are cleaned up by the background job it 'sends the working_file_path to active_encode' do entry.process! - master_file = media_object.reload.master_files.first + master_file = media_object.reload.sections.first working_file_path_high = master_file.working_file_path.find { |file| file.include? "high" } working_file_path_medium = master_file.working_file_path.find { |file| file.include? "medium" } working_file_path_low = master_file.working_file_path.find { |file| file.include? "low" } @@ -224,7 +224,7 @@ it 'sends the file_location to active_encode' do MasterFileBuilder.build(media_object, params) - master_file = media_object.reload.master_files.first + master_file = media_object.reload.sections.first input = FileLocator.new(master_file.file_location).uri.to_s expect(encoder_class).to have_received(:create).with(input, { headers: nil, master_file_id: master_file.id, preset: workflow }) end @@ -234,7 +234,7 @@ it 'sends the file_location to active_encode' do MasterFileBuilder.build(media_object, params) - master_file = media_object.reload.master_files.first + master_file = media_object.reload.sections.first input_path = FileLocator.new(master_file.file_location).uri.to_s expect(encoder_class).to have_received(:create).with(input_path, master_file_id: master_file.id, outputs: [{ label: "high", url: input_path }], preset: "pass_through") end @@ -248,7 +248,7 @@ it 'sends the file_location to active_encode' do MasterFileBuilder.build(media_object, params) - master_file = media_object.reload.master_files.first + master_file = media_object.reload.sections.first input = FileLocator.new(master_file.file_location).uri.to_s expect(encoder_class).to have_received(:create).with(input, { headers: nil, master_file_id: master_file.id, preset: workflow }) end @@ -258,7 +258,7 @@ it 'sends the file_location to active_encode' do MasterFileBuilder.build(media_object, params) - master_file = media_object.reload.master_files.first + master_file = media_object.reload.sections.first input_path = FileLocator.new(master_file.file_location).uri.to_s expect(encoder_class).to have_received(:create).with(input_path, master_file_id: master_file.id, outputs: [{ label: "high", url: input_path }], preset: "pass_through") end @@ -279,7 +279,7 @@ it 'sends the file_location to active_encode' do entry.process! - master_file = media_object.reload.master_files.first + master_file = media_object.reload.sections.first input = FileLocator.new(master_file.file_location).uri.to_s expect(encoder_class).to have_received(:create).with(input, { headers: nil, master_file_id: master_file.id, preset: workflow }) end @@ -289,7 +289,7 @@ it 'sends the file_location to active_encode' do entry.process! - master_file = media_object.reload.master_files.first + master_file = media_object.reload.sections.first input_path = FileLocator.new(master_file.file_location).uri.to_s expect(encoder_class).to have_received(:create).with(input_path, master_file_id: master_file.id, outputs: [{ label: "high", url: input_path }], preset: "pass_through") end @@ -314,7 +314,7 @@ it 'sends the derivative locations to active_encode' do entry.process! - master_file = media_object.reload.master_files.first + master_file = media_object.reload.sections.first input_path = Addressable::URI.convert_path(File.absolute_path(filename_high)).to_s expect(encoder_class).to have_received(:create).with( diff --git a/spec/presenters/speedy_af/proxy/media_object_spec.rb b/spec/presenters/speedy_af/proxy/media_object_spec.rb index db15d8e3cd..b7cb57b63c 100644 --- a/spec/presenters/speedy_af/proxy/media_object_spec.rb +++ b/spec/presenters/speedy_af/proxy/media_object_spec.rb @@ -65,6 +65,8 @@ expect(presenter.other_identifier).to eq media_object.other_identifier expect(presenter.comment).to eq media_object.comment.to_a expect(presenter.visibility).to eq media_object.visibility + expect(presenter.section_list).to eq media_object.section_list + expect(presenter.section_ids).to eq media_object.section_ids end end @@ -83,4 +85,24 @@ expect(presenter.lending_period).to be_present end end + + describe '#sections' do + let(:section1) { FactoryBot.create(:master_file, media_object: media_object) } + let(:section2) { FactoryBot.create(:master_file, media_object: media_object) } + let(:media_object) { FactoryBot.create(:media_object) } + + context 'when no sections present' do + it 'returns empty array without reifying' do + expect(presenter.sections).to eq [] + expect(presenter.real?).to eq false + end + end + + it 'returns array of master file proxy objects in proper order' do + section1 + section2 + expect(presenter.sections.map(&:id)).to eq media_object.section_ids + expect(presenter.sections.map(&:class)).to eq [SpeedyAF::Proxy::MasterFile, SpeedyAF::Proxy::MasterFile] + end + end end From 3dbc7b9937feee1805957089680b537431537c65 Mon Sep 17 00:00:00 2001 From: Chris Colvard Date: Wed, 5 Jun 2024 15:20:07 -0400 Subject: [PATCH 058/152] Overrides of ActiveFedora to avoid doing costly loads of list_source and master_files when saving (#5850) --- app/controllers/media_objects_controller.rb | 4 -- app/models/media_object.rb | 10 +++- config/initializers/active_fedora_general.rb | 52 ++++++++++++++++++++ spec/models/media_object_spec.rb | 2 + 4 files changed, 63 insertions(+), 5 deletions(-) diff --git a/app/controllers/media_objects_controller.rb b/app/controllers/media_objects_controller.rb index 1fd28f319a..0d6a36c329 100644 --- a/app/controllers/media_objects_controller.rb +++ b/app/controllers/media_objects_controller.rb @@ -249,10 +249,6 @@ def update_media_object if api_params[:replace_masterfiles] old_ordered_sections.each do |mf| p = MasterFile.find(mf) - # FIXME: Figure out why this next line is necessary - # without it the save below will fail with Ldp::Gone when attempting to set master_files in the MediaObject before_save callback - # This could be avoided by doing a reload after all of the destroys but I'm afraid that would mess up other changes already staged in-memory. - @media_object.master_files.delete(p) # Need to manually remove from section_ids in memory to match changes that will persist when the master file is destroyed @media_object.section_ids -= [p.id] p.destroy diff --git a/app/models/media_object.rb b/app/models/media_object.rb index c42b9f4a2d..668efa0009 100644 --- a/app/models/media_object.rb +++ b/app/models/media_object.rb @@ -52,7 +52,15 @@ class MediaObject < ActiveFedora::Base self.save!(validate: false) unless self.master_file_ids.sort == self.section_ids.sort end before_save do - self.master_files = self.sections unless self.new_record? || self.master_file_ids.sort == self.section_ids.sort + unless self.new_record? || self.master_file_ids.sort == self.section_ids.sort + # Instead of using the master_files association writer manually set the hasPart triples on the media object + # The association writer in ActiveFedora fetches all of the master files including associated resources (via find) + # before writing the master file ids to the proxy ActiveFedora::IndirectContainer subresource id/master_files. + # This approach requires some overrides of ActiveFedora to fill in some missing functionality. + # These overrides have been appended to config/initializers/active_fedora_general.rb + self.attribute_will_change! :master_files + self.resource.set_value(::RDF::Vocab::DC.hasPart, self.section_ids.collect {|id| ::RDF::Resource.new(MasterFile.id_to_uri(id))}) + end end after_save :update_dependent_permalinks_job, if: Proc.new { |mo| mo.persisted? && mo.published? } diff --git a/config/initializers/active_fedora_general.rb b/config/initializers/active_fedora_general.rb index 257d280a91..fa8353b0fa 100644 --- a/config/initializers/active_fedora_general.rb +++ b/config/initializers/active_fedora_general.rb @@ -91,3 +91,55 @@ def set_entities(permission, type, values, changeable) end end # End of overrides for AccessControl dirty tracking and autosaving + +# Override ActiveFedora::Associations::Builder::Orders::FixFirstLast to remove attempts to set first and last from list_source on saving +ActiveFedora::Associations::Builder::Orders::FixFirstLast.module_eval do + def save(*args) + super + end + + def save!(*args) + super + end +end + +# Override to add handling of :master_files associations +# This override allows setting the hasPart triples of the master_files association manually +# without going through the indirectly_contains association writer. +# Without this override new hasPart triples signaled as changes via attribute_will_change! are not +# detected as changes for the ChangeSet and are not persisted. +ActiveFedora::ChangeSet.class_eval do + # @return [Hash] hash of predicate uris to statements + def changes + @changes ||= changed_attributes.each_with_object({}) do |key, result| + if object.association(key.to_sym).is_a? ActiveFedora::Associations::Association + # ActiveFedora::Reflection::RDFPropertyReflection + predicate = object.association(key.to_sym).reflection.predicate + values = graph.query({ subject: object.rdf_subject, predicate: predicate }) + result[predicate] = values if predicate.present? + elsif object.class.properties.keys.include?(key) + predicate = graph.reflections.reflect_on_property(key).predicate + results = graph.query({ subject: object.rdf_subject, predicate: predicate }) + new_graph = child_graphs(results.map(&:object)) + results.each do |res| + new_graph << res + end + result[predicate] = new_graph + elsif key == 'type'.freeze + # working around https://github.com/ActiveTriples/ActiveTriples/issues/122 + predicate = ::RDF.type + result[predicate] = graph.query({ subject: object.rdf_subject, predicate: predicate }).select do |statement| + !statement.object.to_s.start_with?("http://fedora.info/definitions/v4/repository#", "http://www.w3.org/ns/ldp#") + end + elsif object.local_attributes.include?(key) + raise "Unable to find a graph predicate corresponding to the attribute: \"#{key}\"" + end + end + end +end + +ActiveFedora::Reflection::IndirectlyContainsReflection.class_eval do + def predicate + options[:has_member_relation] || ::RDF::Vocab::LDP.contains + end +end diff --git a/spec/models/media_object_spec.rb b/spec/models/media_object_spec.rb index 65afba3c18..ab7ed7ebf0 100644 --- a/spec/models/media_object_spec.rb +++ b/spec/models/media_object_spec.rb @@ -1271,6 +1271,8 @@ expect(mo.section_list).not_to eq nil expect(mo.sections).not_to eq mo.ordered_master_files.to_a expect(mo.section_ids).to eq [section.id, section2.id, new_section.id] + expect(mo.master_file_ids).to eq mo.section_ids + expect(mo.master_files).to eq mo.sections end end end From a3df08341cb757a9bd74c8024307b23a197c8405 Mon Sep 17 00:00:00 2001 From: cjcolvar Date: Mon, 3 Jun 2024 16:33:20 -0400 Subject: [PATCH 059/152] Make media object json response consistent between SpeedyAF proxy and ActiveFedora object --- app/models/media_object.rb | 4 ++-- app/models/mods_behaviors.rb | 9 +++++++-- app/presenters/speedy_af/proxy/media_object.rb | 8 ++++++++ config/initializers/presenter_config.rb | 3 ++- spec/controllers/media_objects_controller_spec.rb | 4 +++- spec/factories/media_objects.rb | 6 ++++++ 6 files changed, 28 insertions(+), 6 deletions(-) diff --git a/app/models/media_object.rb b/app/models/media_object.rb index 668efa0009..8d31885aa8 100644 --- a/app/models/media_object.rb +++ b/app/models/media_object.rb @@ -318,8 +318,8 @@ def to_solr(include_child_fields: false) solr_doc["title_ssort"] = self.title solr_doc["creator_ssort"] = Array(self.creator).join(', ') solr_doc["date_ingested_ssim"] = self.create_date.strftime "%F" if self.create_date.present? - solr_doc['avalon_resource_type_ssim'] = self.avalon_resource_type.map(&:titleize) - solr_doc['identifier_ssim'] = self.identifier.map(&:downcase) + solr_doc['avalon_resource_type_ssim'] = self.avalon_resource_type + solr_doc['identifier_ssim'] = self.identifier solr_doc['note_ssm'] = self.note.collect { |n| n.to_json } solr_doc['other_identifier_ssm'] = self.other_identifier.collect { |oi| oi.to_json } solr_doc['related_item_url_ssm'] = self.related_item_url.collect { |r| r.to_json } diff --git a/app/models/mods_behaviors.rb b/app/models/mods_behaviors.rb index e29467e80e..1996d13de7 100644 --- a/app/models/mods_behaviors.rb +++ b/app/models/mods_behaviors.rb @@ -42,7 +42,7 @@ def to_solr(solr_doc = Hash.new, opts = {}) solr_doc['abstract_ssi'] = self.find_by_terms(:abstract).text solr_doc['publisher_ssim'] = gather_terms(self.find_by_terms(:publisher)) solr_doc['contributor_ssim'] = gather_terms(self.find_by_terms(:contributor)) - solr_doc['subject_ssim'] = gather_terms(self.find_by_terms(:subject)) + solr_doc['subject_ssim'] = gather_terms(self.find_by_terms(:topical_subject)) solr_doc['genre_ssim'] = gather_terms(self.find_by_terms(:genre)) # solr_doc['physical_dtl_sim'] = gather_terms(self.find_by_terms(:format)) # solr_doc['contents_sim'] = gather_terms(self.find_by_terms(:parts_list)) @@ -52,7 +52,7 @@ def to_solr(solr_doc = Hash.new, opts = {}) # solr_doc['collection_sim'] = gather_terms(self.find_by_terms(:archival_collection)) solr_doc['series_ssim'] = gather_terms(self.find_by_terms(:series)) #filter formats based upon whitelist - solr_doc['resource_type_ssim'] = (gather_terms(self.find_by_terms(:resource_type)) & ['moving image', 'sound recording' ]).map(&:titleize) + solr_doc['resource_type_ssim'] = (gather_terms(self.find_by_terms(:resource_type)) & ['moving image', 'sound recording' ]) solr_doc['location_ssim'] = gather_terms(self.find_by_terms(:geographic_subject)) # Blacklight facets - these are the same facet fields used in our Blacklight app @@ -78,6 +78,11 @@ def to_solr(solr_doc = Hash.new, opts = {}) solr_doc['terms_of_use_ssi'] = (self.find_by_terms(:terms_of_use) - self.find_by_terms(:rights_statement)).text solr_doc['rights_statement_ssi'] = self.find_by_terms(:rights_statement).text solr_doc['other_identifier_sim'] = gather_terms(self.find_by_terms(:other_identifier)) + solr_doc['bibliographic_id_ssi'] = self.bibliographic_id.first + solr_doc['bibliographic_id_source_ssi'] = self.bibliographic_id.source.first + solr_doc['uniform_title_ssim'] = gather_terms(self.find_by_terms(:uniform_title)) + solr_doc['statement_of_responsibility_ssi'] = gather_terms(self.find_by_terms(:statement_of_responsibility)) + solr_doc['record_identifier_ssim'] = gather_terms(self.find_by_terms(:record_identifier)) # Extract 4-digit year for creation date facet in Hydra and pub_date facet in Blacklight solr_doc['date_issued_ssi'] = self.find_by_terms(:date_issued).text diff --git a/app/presenters/speedy_af/proxy/media_object.rb b/app/presenters/speedy_af/proxy/media_object.rb index cf84fe2fce..308d1ddd4a 100644 --- a/app/presenters/speedy_af/proxy/media_object.rb +++ b/app/presenters/speedy_af/proxy/media_object.rb @@ -132,6 +132,14 @@ def language attrs[:language_code].present? ? attrs[:language_code].map { |code| { code: code, text: LanguageTerm.find(code).text } } : [] end + def bibliographic_id + if attrs[:bibliographic_id].present? && attrs[:bibliographic_id_source].present? + { id: attrs[:bibliographic_id], source: attrs[:bibliographic_id_source] } + else + nil + end + end + def sections_with_files(tag: '*') sections.select { |master_file| master_file.supplemental_files(tag: tag).present? }.map(&:id) end diff --git a/config/initializers/presenter_config.rb b/config/initializers/presenter_config.rb index b238467210..9aa9e49763 100644 --- a/config/initializers/presenter_config.rb +++ b/config/initializers/presenter_config.rb @@ -60,7 +60,8 @@ uniform_title: [], resource_type: [], record_identifier: [], - series: [] + series: [], + format: [] } include VirtualGroups include MediaObjectIntercom diff --git a/spec/controllers/media_objects_controller_spec.rb b/spec/controllers/media_objects_controller_spec.rb index 7369ccf240..6fb35eb498 100644 --- a/spec/controllers/media_objects_controller_spec.rb +++ b/spec/controllers/media_objects_controller_spec.rb @@ -1181,7 +1181,7 @@ context "with json format" do subject(:json) { JSON.parse(response.body) } let(:administrator) { FactoryBot.create(:administrator) } - let!(:media_object) { FactoryBot.create(:media_object) } + let!(:media_object) { FactoryBot.create(:fully_searchable_media_object) } let!(:master_file) { FactoryBot.create(:master_file, :with_derivative, media_object: media_object) } before do @@ -1190,6 +1190,8 @@ end it "should return json for specific media_object" do + # Run indexing job to ensure object isn't reified in this request + perform_enqueued_jobs(only: MediaObjectIndexingJob) get 'show', params: { id: media_object.id, format:'json' } expect(json['id']).to eq(media_object.id) expect(json['title']).to eq(media_object.title) diff --git a/spec/factories/media_objects.rb b/spec/factories/media_objects.rb index f023a97e04..33dc032b92 100644 --- a/spec/factories/media_objects.rb +++ b/spec/factories/media_objects.rb @@ -50,6 +50,12 @@ terms_of_use { [ 'Terms of Use: Be kind. Rewind.' ] } series { [Faker::Lorem.word] } sections { [] } + statement_of_responsibility { Faker::Lorem.word } + uniform_title { [Faker::Lorem.sentence] } + identifier { [Faker::Alphanumeric.alphanumeric(number: 8, min_alpha: 1, min_numeric: 1).downcase, + Faker::Alphanumeric.alphanumeric(number: 8, min_alpha: 1, min_numeric: 1).upcase, + Faker::Barcode.isbn] } + resource_type { ['moving image'] } # after(:create) do |mo| # mo.update_datastream(:descMetadata, { # note: {note[Faker::Lorem.paragraph], From 6e0a08ca7c17de2f442287bb6f3fce9fa7e3cfed Mon Sep 17 00:00:00 2001 From: Chris Colvard Date: Wed, 5 Jun 2024 15:38:43 -0400 Subject: [PATCH 060/152] Playlist description fixes (#5847) * Use i18n for consistent labelling and simple_format for display of newline characters * Remove restrictions on playlist description length by converting to column to type text --- app/models/playlist.rb | 1 - ...ments_and_tags.html.erb => _description_and_tags.html.erb} | 4 ++-- app/views/playlists/_show_playlist_details.html.erb | 2 +- app/views/playlists/show.html.erb | 2 +- db/schema.rb | 4 ++-- 5 files changed, 6 insertions(+), 7 deletions(-) rename app/views/playlists/{_comments_and_tags.html.erb => _description_and_tags.html.erb} (89%) diff --git a/app/models/playlist.rb b/app/models/playlist.rb index 56b58c7a25..07600fa74d 100644 --- a/app/models/playlist.rb +++ b/app/models/playlist.rb @@ -24,7 +24,6 @@ class Playlist < ActiveRecord::Base validates :user, presence: true validates :title, presence: true - validates :comment, length: { maximum: 255 } validates :visibility, presence: true validates :visibility, inclusion: { in: proc { [PUBLIC, PRIVATE, PRIVATE_WITH_TOKEN] } } diff --git a/app/views/playlists/_comments_and_tags.html.erb b/app/views/playlists/_description_and_tags.html.erb similarity index 89% rename from app/views/playlists/_comments_and_tags.html.erb rename to app/views/playlists/_description_and_tags.html.erb index 278582f3bd..80254767d5 100644 --- a/app/views/playlists/_comments_and_tags.html.erb +++ b/app/views/playlists/_description_and_tags.html.erb @@ -14,8 +14,8 @@ Unless required by applicable law or agreed to in writing, software distributed --- END LICENSE_HEADER BLOCK --- %> <% if @playlist.comment.present? %> -

          Comments

          -<%= @playlist.comment %> +

          <%= t("activerecord.attributes.playlist.comment") %>

          +<%= simple_format @playlist.comment %> <% end %> <% if @playlist.tags.present? %> diff --git a/app/views/playlists/_show_playlist_details.html.erb b/app/views/playlists/_show_playlist_details.html.erb index 210cb6bae1..751a099ebe 100644 --- a/app/views/playlists/_show_playlist_details.html.erb +++ b/app/views/playlists/_show_playlist_details.html.erb @@ -36,7 +36,7 @@ Unless required by applicable law or agreed to in writing, software distributed <% if @playlist.comment.blank? %> No description <% else %> - <%= @playlist.comment %> + <%= simple_format @playlist.comment %> <% end %>
          <%= t("playlist.visibility") %>:
          diff --git a/app/views/playlists/show.html.erb b/app/views/playlists/show.html.erb index 5b72257141..80dbac5fec 100644 --- a/app/views/playlists/show.html.erb +++ b/app/views/playlists/show.html.erb @@ -35,7 +35,7 @@ Unless required by applicable law or agreed to in writing, software distributed playlist_item_ids: @playlist.item_ids, token: @playlist_token, share: { canShare: (will_partial_list_render? :share), content: render('share') }, - comment_tag: { content: render('comments_and_tags') } + comment_tag: { content: render('description_and_tags') } } ) %>
    diff --git a/db/schema.rb b/db/schema.rb index 19b78c1c20..96885f150b 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[7.0].define(version: 2024_04_24_200346) do +ActiveRecord::Schema[7.0].define(version: 2024_05_31_201828) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -196,7 +196,7 @@ create_table "playlists", force: :cascade do |t| t.string "title" t.bigint "user_id", null: false - t.string "comment" + t.text "comment" t.string "visibility" t.datetime "created_at", precision: nil, null: false t.datetime "updated_at", precision: nil, null: false From 31213f407a19214adac5ee9b31822fde4d211c98 Mon Sep 17 00:00:00 2001 From: Chris Colvard Date: Wed, 5 Jun 2024 15:39:04 -0400 Subject: [PATCH 061/152] Index avalon_resource_type the same as it is stored in fedora (#5846) Shift titleization into front end through a helper method utilized by the Blacklight facet. Resolves #5836 --- app/controllers/catalog_controller.rb | 2 +- app/helpers/application_helper.rb | 4 ++++ app/models/media_object.rb | 2 +- spec/helpers/application_helper_spec.rb | 9 +++++++++ 4 files changed, 15 insertions(+), 2 deletions(-) diff --git a/app/controllers/catalog_controller.rb b/app/controllers/catalog_controller.rb index 8fd86e5493..948342619a 100644 --- a/app/controllers/catalog_controller.rb +++ b/app/controllers/catalog_controller.rb @@ -80,7 +80,7 @@ class CatalogController < ApplicationController # # :show may be set to false if you don't want the facet to be drawn in the # facet bar - config.add_facet_field 'avalon_resource_type_ssim', label: 'Format', limit: 5, collapse: false + config.add_facet_field 'avalon_resource_type_ssim', label: 'Format', limit: 5, collapse: false, helper_method: :titleize config.add_facet_field 'creator_ssim', label: 'Main contributor', limit: 5 config.add_facet_field 'date_sim', label: 'Date', limit: 5 config.add_facet_field 'genre_ssim', label: 'Genres', limit: 5 diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index a5f1419f36..2c9debd60b 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -226,6 +226,10 @@ def truncate_center label, output_label_length, end_length = 0 omission: "...#{label.last(end_length)}") end + def titleize value + value.is_a?(Array) ? value.map(&:titleize) : value.titleize + end + def master_file_meta_properties( m ) formatted_duration = m.duration ? Time.new(m.duration.to_i / 1000).iso8601 : '' item_type = m.is_video? ? 'http://schema.org/VideoObject' : 'http://schema.org/AudioObject' diff --git a/app/models/media_object.rb b/app/models/media_object.rb index 668efa0009..ac46a9f6d0 100644 --- a/app/models/media_object.rb +++ b/app/models/media_object.rb @@ -318,7 +318,7 @@ def to_solr(include_child_fields: false) solr_doc["title_ssort"] = self.title solr_doc["creator_ssort"] = Array(self.creator).join(', ') solr_doc["date_ingested_ssim"] = self.create_date.strftime "%F" if self.create_date.present? - solr_doc['avalon_resource_type_ssim'] = self.avalon_resource_type.map(&:titleize) + solr_doc['avalon_resource_type_ssim'] = self.avalon_resource_type solr_doc['identifier_ssim'] = self.identifier.map(&:downcase) solr_doc['note_ssm'] = self.note.collect { |n| n.to_json } solr_doc['other_identifier_ssm'] = self.other_identifier.collect { |oi| oi.to_json } diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index e09724ed26..2de1ba4286 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -87,6 +87,15 @@ end end + describe "#titleize" do + it "titleizes a string" do + expect(helper.titleize("lower case phrase")).to eq "Lower Case Phrase" + end + it "titleizes an array of strings" do + expect(helper.titleize(["ant", "adam ant", "tic tac toe"])).to eq ["Ant", "Adam Ant", "Tic Tac Toe"] + end + end + describe "#truncate_center" do it "should return empty string if empty string received" do expect(helper.truncate_center("", 5)).to eq "" From 9e3d827fdd013049cd0f0219a681e26cbf35969b Mon Sep 17 00:00:00 2001 From: Chris Colvard Date: Wed, 5 Jun 2024 15:39:18 -0400 Subject: [PATCH 062/152] Index MODS recordChangeDate on parent media object (#5845) This field will act as a better indicator of when the object's metadata has changed instead of relying on media object's solr timestamp which gets updated any time the object gets reindexed. This new value is used in the ATOM feed and is available for use in OAI-PMH. Resolves #5580 --- app/models/mods_behaviors.rb | 2 ++ app/views/catalog/_document_media_object.atom.builder | 2 +- spec/models/media_object_spec.rb | 3 +++ spec/requests/atom_feed_spec.rb | 6 +++--- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/app/models/mods_behaviors.rb b/app/models/mods_behaviors.rb index e29467e80e..103b1542b2 100644 --- a/app/models/mods_behaviors.rb +++ b/app/models/mods_behaviors.rb @@ -89,6 +89,8 @@ def to_solr(solr_doc = Hash.new, opts = {}) # For full text, we stuff it into the mods_tesim field which is already configured for Mods doucments solr_doc['mods_tesim'] = self.ng_xml.xpath('//text()').collect { |t| t.text } + solr_doc['descMetadata_modified_dtsi'] = ActiveFedora::Indexing::DefaultDescriptors.iso8601_date(self.record_change_date.first) + # TODO: Find a better way to handle super long fields other than simply dropping them from the solr doc. solr_doc.delete_if do |field,value| case value diff --git a/app/views/catalog/_document_media_object.atom.builder b/app/views/catalog/_document_media_object.atom.builder index d68e748919..e87973f69e 100644 --- a/app/views/catalog/_document_media_object.atom.builder +++ b/app/views/catalog/_document_media_object.atom.builder @@ -4,7 +4,7 @@ xml.entry do xml.title index_presenter(document).label(document_show_link_field(document)) # updated is required, for now we'll just set it to now, sorry - xml.updated document[:timestamp] + xml.updated document[:descMetadata_modified_dtsi] || document[:timestamp] xml.link "rel" => "alternate", "type" => "application/json", "href" => media_object_url(document.id, format: :json) # add other doc-specific formats, atom only lets us have one per diff --git a/spec/models/media_object_spec.rb b/spec/models/media_object_spec.rb index ab7ed7ebf0..98a78bbeb5 100644 --- a/spec/models/media_object_spec.rb +++ b/spec/models/media_object_spec.rb @@ -637,6 +637,9 @@ media_object.save! expect(media_object.to_solr['read_access_ip_group_ssim']).to include(ip_addr) end + it 'indexes modified time for descMetadata subresource' do + expect(DateTime.parse(media_object.to_solr['descMetadata_modified_dtsi'])).to eq DateTime.parse(media_object.descMetadata.record_change_date.first) + end end describe 'permalink' do diff --git a/spec/requests/atom_feed_spec.rb b/spec/requests/atom_feed_spec.rb index dacf1c5a9c..20106f43c4 100644 --- a/spec/requests/atom_feed_spec.rb +++ b/spec/requests/atom_feed_spec.rb @@ -25,7 +25,7 @@ let(:updated_date) do query = ActiveFedora::SolrQueryBuilder.construct_query(ActiveFedora.id_field => media_object.id) doc = ActiveFedora::SolrService.get(query)['response']['docs'].first - doc["timestamp"] + doc["descMetadata_modified_dtsi"] end it 'returns information about a media object' do @@ -45,12 +45,12 @@ let(:updated_date1) do query = ActiveFedora::SolrQueryBuilder.construct_query(ActiveFedora.id_field => media_object1.id) doc = ActiveFedora::SolrService.get(query)['response']['docs'].first - doc["timestamp"] + doc["descMetadata_modified_dtsi"] end let(:updated_date2) do query = ActiveFedora::SolrQueryBuilder.construct_query(ActiveFedora.id_field => media_object2.id) doc = ActiveFedora::SolrService.get(query)['response']['docs'].first - doc["timestamp"] + doc["descMetadata_modified_dtsi"] end it 'sorts based upon solr timestamp' do From ac67722a88e3b83986f31ada43fec556f15ca186 Mon Sep 17 00:00:00 2001 From: cjcolvar Date: Wed, 5 Jun 2024 16:10:01 -0400 Subject: [PATCH 063/152] Index other fields missing from MODS --- app/models/concerns/media_object_mods.rb | 2 +- app/models/mods_behaviors.rb | 6 ++++-- spec/controllers/media_objects_controller_spec.rb | 13 ++----------- spec/factories/media_objects.rb | 10 ++++++++-- 4 files changed, 15 insertions(+), 16 deletions(-) diff --git a/app/models/concerns/media_object_mods.rb b/app/models/concerns/media_object_mods.rb index a1ce122db4..17261d173a 100644 --- a/app/models/concerns/media_object_mods.rb +++ b/app/models/concerns/media_object_mods.rb @@ -90,7 +90,7 @@ def alternative_title=(value) # has_attributes :translated_title, datastream: :descMetadata, at: [:translated_title], multiple: true def translated_title - descMetadata.alternative_title + descMetadata.translated_title end def translated_title=(value) diff --git a/app/models/mods_behaviors.rb b/app/models/mods_behaviors.rb index 1996d13de7..af82ebba13 100644 --- a/app/models/mods_behaviors.rb +++ b/app/models/mods_behaviors.rb @@ -34,7 +34,9 @@ def to_solr(solr_doc = Hash.new, opts = {}) end solr_doc['title_addl_sim'] = gather_terms(addl_titles) solr_doc['heading_sim'] = self.find_by_terms(:main_title).text - + solr_doc['uniform_title_ssim'] = gather_terms(self.find_by_terms(:uniform_title)) + solr_doc['alternative_title_ssim'] = gather_terms(self.find_by_terms(:alternative_title)) + solr_doc['translated_title_ssim'] = gather_terms(self.find_by_terms(:translated_title)) solr_doc['creator_ssim'] = gather_terms(self.find_by_terms(:creator)) # solr_doc['creator_ssi'] = self.find_by_terms(:creator).text @@ -80,13 +82,13 @@ def to_solr(solr_doc = Hash.new, opts = {}) solr_doc['other_identifier_sim'] = gather_terms(self.find_by_terms(:other_identifier)) solr_doc['bibliographic_id_ssi'] = self.bibliographic_id.first solr_doc['bibliographic_id_source_ssi'] = self.bibliographic_id.source.first - solr_doc['uniform_title_ssim'] = gather_terms(self.find_by_terms(:uniform_title)) solr_doc['statement_of_responsibility_ssi'] = gather_terms(self.find_by_terms(:statement_of_responsibility)) solr_doc['record_identifier_ssim'] = gather_terms(self.find_by_terms(:record_identifier)) # Extract 4-digit year for creation date facet in Hydra and pub_date facet in Blacklight solr_doc['date_issued_ssi'] = self.find_by_terms(:date_issued).text solr_doc['date_created_ssi'] = self.find_by_terms(:date_created).text + solr_doc['copyright_date_ssi'] = self.find_by_terms(:copyright_date).text # Put both publication date and creation date into the date facet solr_doc['date_sim'] = gather_years(solr_doc['date_issued_ssi']) solr_doc['date_sim'] += gather_years(solr_doc['date_created_ssi']) if solr_doc['date_created_ssi'].present? diff --git a/spec/controllers/media_objects_controller_spec.rb b/spec/controllers/media_objects_controller_spec.rb index 6fb35eb498..ac5c550473 100644 --- a/spec/controllers/media_objects_controller_spec.rb +++ b/spec/controllers/media_objects_controller_spec.rb @@ -1181,7 +1181,7 @@ context "with json format" do subject(:json) { JSON.parse(response.body) } let(:administrator) { FactoryBot.create(:administrator) } - let!(:media_object) { FactoryBot.create(:fully_searchable_media_object) } + let!(:media_object) { FactoryBot.create(:all_fields_media_object) } let!(:master_file) { FactoryBot.create(:master_file, :with_derivative, media_object: media_object) } before do @@ -1203,17 +1203,8 @@ expect(json['published']).to eq(media_object.published?) expect(json['summary']).to eq(media_object.abstract) - # FIXME: https://github.com/avalonmediasystem/avalon/issues/5834 ingest_api_hash = media_object.to_ingest_api_hash(false) - json['fields'].each do |k,v| - if k == "avalon_resource_type" - expect(v.map(&:downcase)).to eq(ingest_api_hash[:fields][k.to_sym]) - elsif k == "record_identifier" - # no-op since not indexed - else - expect(v).to eq(ingest_api_hash[:fields][k.to_sym]) - end - end + json['fields'].each { |k,v| expect(v).to eq(ingest_api_hash[:fields][k.to_sym]) } # Symbolize keys for master files and derivatives json['files'].each do |mf| diff --git a/spec/factories/media_objects.rb b/spec/factories/media_objects.rb index 33dc032b92..901d722386 100644 --- a/spec/factories/media_objects.rb +++ b/spec/factories/media_objects.rb @@ -50,12 +50,11 @@ terms_of_use { [ 'Terms of Use: Be kind. Rewind.' ] } series { [Faker::Lorem.word] } sections { [] } - statement_of_responsibility { Faker::Lorem.word } - uniform_title { [Faker::Lorem.sentence] } identifier { [Faker::Alphanumeric.alphanumeric(number: 8, min_alpha: 1, min_numeric: 1).downcase, Faker::Alphanumeric.alphanumeric(number: 8, min_alpha: 1, min_numeric: 1).upcase, Faker::Barcode.isbn] } resource_type { ['moving image'] } + statement_of_responsibility { Faker::Lorem.word } # after(:create) do |mo| # mo.update_datastream(:descMetadata, { # note: {note[Faker::Lorem.paragraph], @@ -66,6 +65,13 @@ # }) # mo.save # end + + factory :all_fields_media_object do + uniform_title { [Faker::Lorem.sentence] } + alternative_title { [Faker::Lorem.sentence] } + translated_title { [Faker::Lorem.sentence] } + copyright_date { '2011' } + end end end trait :with_master_file do From 7f5418386c23341d1f0ff4658d97125a6bc78062 Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Thu, 6 Jun 2024 11:18:00 -0400 Subject: [PATCH 064/152] Add SupplementalFile ingest API --- app/controllers/application_controller.rb | 2 +- .../supplemental_files_controller.rb | 170 ++++++++--- app/models/supplemental_file.rb | 42 ++- config/routes.rb | 4 +- spec/models/supplemental_file_spec.rb | 44 +++ .../supplemental_files_controller_examples.rb | 288 ++++++++++++++---- 6 files changed, 454 insertions(+), 96 deletions(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 387711224b..d1893db102 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -29,7 +29,7 @@ class ApplicationController < ActionController::Base helper_method :render_bookmarks_control? - around_action :handle_api_request, if: proc{|c| request.format.json? || request.format.atom? } + around_action :handle_api_request, if: proc{|c| request.format.json? || request.format.atom? || request.headers['Avalon-Api-Key'].present? } before_action :rewrite_v4_ids, if: proc{|c| request.method_symbol == :get && [params[:id], params[:content]].flatten.compact.any? { |i| i =~ /^[a-z]+:[0-9]+$/}} before_action :set_no_cache_headers, if: proc{|c| request.xhr? } prepend_before_action :remove_zero_width_chars diff --git a/app/controllers/supplemental_files_controller.rb b/app/controllers/supplemental_files_controller.rb index bb20375b40..0fb3770732 100644 --- a/app/controllers/supplemental_files_controller.rb +++ b/app/controllers/supplemental_files_controller.rb @@ -14,6 +14,9 @@ # frozen_string_literal: true class SupplementalFilesController < ApplicationController + include Rails::Pagination + + before_action :authenticate_user!, except: [:show, :captions] before_action :set_object before_action :authorize_object @@ -30,21 +33,39 @@ class SupplementalFilesController < ApplicationController handle_error(message: exception.full_message, status: 404) end - def create - # FIXME: move filedata to permanent location - raise Avalon::BadRequest, "Missing required parameters" unless supplemental_file_params[:file] + def index + files = paginate SupplementalFile.where("parent_id = ?", @object.id) + render json: files.to_a.collect { |f| f.as_json } + end - @supplemental_file = SupplementalFile.new(label: supplemental_file_params[:label], tags: supplemental_file_params[:tags], parent_id: @object.id) - begin - @supplemental_file.attach_file(supplemental_file_params[:file]) - rescue StandardError, LoadError => e - raise Avalon::SaveError, "File could not be attached: #{e.full_message}" + def create + if metadata_upload? && !attachment + raise Avalon::BadRequest, "Missing required Content-type headers" unless request.headers["Content-Type"] == 'application/json' end + raise Avalon::BadRequest, "Missing required parameters" unless validate_params + + @supplemental_file = SupplementalFile.new(**metadata_from_params) + + if attachment + begin + @supplemental_file.attach_file(attachment) + rescue StandardError, LoadError => e + raise Avalon::SaveError, "File could not be attached: #{e.full_message}" + end - # Raise errror if file wasn't attached - raise Avalon::SaveError, "File could not be attached." unless @supplemental_file.file.attached? + # Raise errror if file wasn't attached + raise Avalon::SaveError, "File could not be attached." unless @supplemental_file.file.attached? - raise Avalon::SaveError, @supplemental_file.errors.full_messages unless @supplemental_file.save + raise Avalon::SaveError, @supplemental_file.errors.full_messages unless @supplemental_file.save + else + # For multi-step API upload, we need to skip file type validation on the metadata portion of the save. + # Captions are validated as VTT or SRT which cannot be validated if there is no file attached. + @supplemental_file.skip_file_type = true + # Transcripts are automatically indexed on creation, so we need to skip indexing to avoid an error + # when saving without an uploaded file. + @supplemental_file.skip_index = true + raise Avalon::SaveError, @supplemental_file.errors.full_messages unless @supplemental_file.save + end @object.supplemental_files += [@supplemental_file] raise Avalon::SaveError, @object.errors[:supplemental_files_json].full_messages unless @object.save @@ -52,43 +73,65 @@ def create flash[:success] = "Supplemental file successfully added." respond_to do |format| - format.html { redirect_to edit_structure_path } - format.json { head :created, location: object_supplemental_file_path } + format.html { + if request.headers['Avalon-Api-Key'].present? + render json: { supplemental_file: @supplemental_file.id }, status: :created + else + redirect_to edit_structure_path + end + } + format.json { render json: { supplemental_file: @supplemental_file.id }, status: :created } end end def show find_supplemental_file - # Redirect or proxy the content - if Settings.supplemental_files.proxy - send_data @supplemental_file.file.download, filename: @supplemental_file.file.filename.to_s, type: @supplemental_file.file.content_type, disposition: 'attachment' - else - redirect_to rails_blob_path(@supplemental_file.file, disposition: "attachment") + respond_to do |format| + format.html { + # Redirect or proxy the content + if Settings.supplemental_files.proxy + send_data @supplemental_file.file.download, filename: @supplemental_file.file.filename.to_s, type: @supplemental_file.file.content_type, disposition: 'attachment' + else + redirect_to rails_blob_path(@supplemental_file.file, disposition: "attachment") + end + } + format.json { render json: @supplemental_file.as_json } end end - # Update the label and tags of the supplemental file def update - raise Avalon::NotFound, "Cannot update the supplemental file: #{params[:id]} not found" unless SupplementalFile.exists? params[:id].to_s - @supplemental_file = SupplementalFile.find(params[:id]) - raise Avalon::NotFound, "Cannot update the supplemental file: #{@supplemental_file.id} not found" unless @object.supplemental_files.any? { |f| f.id == @supplemental_file.id } - raise Avalon::BadRequest, "Updating file contents not allowed" if supplemental_file_params[:file].present? + if metadata_upload? + raise Avalon::BadRequest, "Incorrect request format. Use HTML if updating attached file." if attachment + raise Avalon::BadRequest, "Missing required Content-type headers" unless request.headers["Content-Type"] == 'application/json' + elsif request.headers['Avalon-Api-Key'].present? + raise Avalon::BadRequest, "Incorrect request format. Use JSON if updating metadata." unless attachment + end + raise Avalon::BadRequest, "Missing required parameters" unless validate_params + + find_supplemental_file + + edit_file_information if !attachment + + update_attached_file if attachment - edit_file_information raise Avalon::SaveError, @supplemental_file.errors.full_messages unless @supplemental_file.save flash[:success] = "Supplemental file successfully updated." respond_to do |format| - format.html { redirect_to edit_structure_path } - format.json { head :ok, location: object_supplemental_file_path } + format.html { + if request.headers['Avalon-Api-Key'].present? + render json: { supplemental_file: @supplemental_file.id } + else + redirect_to edit_structure_path + end + } + format.json { render json: { supplemental_file: @supplemental_file.id }, status: :ok } end end def destroy - raise Avalon::NotFound, "Cannot delete the supplemental file: #{params[:id]} not found" unless SupplementalFile.exists? params[:id].to_s - @supplemental_file = SupplementalFile.find(params[:id]) - raise Avalon::NotFound, "Cannot delete the supplemental file: #{@supplemental_file.id} not found" unless @object.supplemental_files.any? { |f| f.id == @supplemental_file.id } + find_supplemental_file @object.supplemental_files -= [@supplemental_file] raise Avalon::SaveError, "An error occurred when deleting the supplemental file: #{@object.errors[:supplemental_files_json].full_messages}" unless @object.save @@ -117,9 +160,34 @@ def set_object @object = fetch_object params[:master_file_id] || params[:media_object_id] end + def validate_params + attachment.present? || [:label, :language, :tags].any? { |v| supplemental_file_params[v].present? } + end + def supplemental_file_params # TODO: Add parameters for minio and s3 - params.fetch(:supplemental_file, {}).permit(:label, :language, :file, tags: []) + sup_file_params = params.fetch(:supplemental_file, {}).permit(:label, :language, :file, tags: []) + return sup_file_params unless metadata_upload? + + meta_params = params[:metadata].present? ? JSON.parse(params[:metadata]).symbolize_keys : params + + type = case meta_params[:type] + when 'caption' + 'caption' + when 'transcript' + 'transcript' + else + nil + end + treat_as_transcript = 'transcript' if meta_params[:treat_as_transcript] == 1 + machine_generated = 'machine_generated' if meta_params[:machine_generated] == 1 + + sup_file_params[:label] ||= meta_params[:label].presence + sup_file_params[:language] ||= meta_params[:language].presence + # The uniq is to prevent multiple instances of 'transcript' tag if an update is performed with + # `{ type: transcript, treat_as_transcript: 1}` + sup_file_params[:tags] ||= [type, treat_as_transcript, machine_generated].compact.uniq + sup_file_params end def find_supplemental_file @@ -133,7 +201,7 @@ def find_supplemental_file def handle_error(message:, status:) - if request.format == :json + if request.format == :json || request.headers['Avalon-Api-Key'].present? render json: { errors: message }, status: status else flash[:error] = message @@ -151,6 +219,22 @@ def edit_structure_path end def edit_file_information + update_tags + + @supplemental_file.label = supplemental_file_params[:label] + return unless supplemental_file_params[:language].present? + @supplemental_file.language = LanguageTerm.find(supplemental_file_params[:language]).code + end + + def update_tags + # The edit page only provides supplemental_file_params[:tags] on object creation. + # Thus, we need to provide individual handling for both updates triggered by page + # actions and updates through the JSON api. + if request.format == 'json' + @supplemental_file.tags = supplemental_file_params[:tags].presence + return + end + file_params = [ { param: "machine_generated_#{params[:id]}".to_sym, tag: "machine_generated", method: :machine_generated? }, { param: "treat_as_transcript_#{params[:id]}".to_sym, tag: "transcript", method: :caption_transcript? } @@ -166,9 +250,27 @@ def edit_file_information @supplemental_file.tags -= [tag] end end - @supplemental_file.label = supplemental_file_params[:label] - return unless supplemental_file_params[:language].present? - @supplemental_file.language = LanguageTerm.find(supplemental_file_params[:language]).code + end + + def metadata_from_params + { + label: supplemental_file_params[:label], + tags: supplemental_file_params[:tags], + language: supplemental_file_params[:language].present? ? LanguageTerm.find(supplemental_file_params[:language]).code : Settings.caption_default.language, + parent_id: @object.id + }.compact + end + + def metadata_upload? + params[:format] == 'json' + end + + def attachment + params[:file] || supplemental_file_params[:file] + end + + def update_attached_file + @supplemental_file.attach_file(attachment) end def object_supplemental_file_path diff --git a/app/models/supplemental_file.rb b/app/models/supplemental_file.rb index 25a2473740..2659f18707 100644 --- a/app/models/supplemental_file.rb +++ b/app/models/supplemental_file.rb @@ -19,29 +19,28 @@ class SupplementalFile < ApplicationRecord scope :with_tag, ->(tag_filter) { where("tags LIKE ?", "%\n- #{tag_filter}\n%") } + attr_accessor :skip_file_type + attr_accessor :skip_index + # TODO: the empty tag should represent a generic supplemental file validates :tags, array_inclusion: ['transcript', 'caption', 'machine_generated', '', nil] validates :language, inclusion: { in: LanguageTerm.map.keys } - validate :validate_file_type, if: :caption? validates :parent_id, presence: true + validate :validate_file_type, if: :validate_caption? serialize :tags, Array # Need to prepend so this runs before the callback added by `has_one_attached` above # See https://github.com/rails/rails/issues/37304 - after_create_commit :update_index, prepend: true - after_update :update_index - - def validate_file_type - errors.add(:file_type, "Uploaded file is not a recognized captions file") unless ['text/vtt', 'text/srt'].include? file.content_type - end + after_create_commit :update_index, prepend: true, unless: :skip_index + after_update_commit :update_index, prepend: true def attach_file(new_file) file.attach(new_file) extension = File.extname(new_file.original_filename) self.file.content_type = Mime::Type.lookup_by_extension(extension.slice(1..-1)).to_s if extension == '.srt' self.label = file.filename.to_s if label.blank? - self.language = tags.include?('caption') ? Settings.caption_default.language : 'eng' + self.language ||= tags.include?('caption') ? Settings.caption_default.language : 'eng' end def mime_type @@ -64,6 +63,25 @@ def caption_transcript? tags.include?('caption') && tags.include?('transcript') end + def as_json(options={}) + type = if tags.include?('caption') + 'caption' + elsif tags.include?('transcript') + 'transcript' + else + 'generic' + end + + { + id: id, + type: type, + label: label, + language: LanguageTerm.find(language).text, + treat_as_transcript: caption_transcript? ? '1' : nil, + machine_generated: machine_generated? ? '1' : nil + }.compact + end + # Adapted from https://github.com/opencoconut/webvtt-ruby/blob/e07d59220260fce33ba5a0c3b355e3ae88b99457/lib/webvtt/parser.rb#L11-L30 def self.convert_from_srt(srt) # normalize timestamps in srt @@ -111,6 +129,14 @@ def segment_transcript transcript private + def validate_file_type + errors.add(:file_type, "Uploaded file is not a recognized captions file") unless ['text/vtt', 'text/srt'].include? file.content_type + end + + def validate_caption? + caption? && !skip_file_type + end + def c_time created_at&.to_datetime || DateTime.now end diff --git a/config/routes.rb b/config/routes.rb index 333402d776..463beacd3b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -139,7 +139,7 @@ end # Supplemental Files - resources :supplemental_files, except: [:new, :index, :edit] + resources :supplemental_files, except: [:new, :edit] end resources :master_files, except: [:new, :index] do @@ -168,7 +168,7 @@ end # Supplemental Files - resources :supplemental_files, except: [:new, :index, :edit] do + resources :supplemental_files, except: [:new, :edit] do member do get 'captions' get 'transcripts', :to => redirect('/master_files/%{master_file_id}/supplemental_files/%{id}') diff --git a/spec/models/supplemental_file_spec.rb b/spec/models/supplemental_file_spec.rb index 5155ef9435..9ad8838fd9 100644 --- a/spec/models/supplemental_file_spec.rb +++ b/spec/models/supplemental_file_spec.rb @@ -126,6 +126,50 @@ end end + describe '#as_json' do + subject { supplemental_file.as_json } + let(:supplemental_file) { FactoryBot.create(:supplemental_file, label: 'Test') } + + context 'generic supplemental file' do + it 'serializes the metadata' do + expect(subject[:id]).to eq supplemental_file.id + expect(subject[:type]).to eq 'generic' + expect(subject[:label]).to eq supplemental_file.label + expect(subject[:language]).to eq 'English' + expect(subject[:treat_as_transcript]).to_not be_present + expect(subject[:machine_generated]).to_not be_present + end + end + + context 'machine generated file' do + let(:supplemental_file) { FactoryBot.create(:supplemental_file, :with_caption_file, tags: ['machine_generated'], label: 'Test') } + it 'includes machine_generated in json' do + expect(subject[:machine_generated]).to eq '1' + end + end + + context 'caption file' do + let(:supplemental_file) { FactoryBot.create(:supplemental_file, :with_caption_file, :with_caption_tag, label: 'Test') } + it 'sets the type properly' do + expect(subject[:type]).to eq 'caption' + end + + context 'as transcript' do + let(:supplemental_file) { FactoryBot.create(:supplemental_file, :with_caption_file, tags: ['caption', 'transcript'], label: 'Test') } + it 'includes treat_as_transcript in JSON' do + expect(subject[:treat_as_transcript]).to eq '1' + end + end + end + + context 'transcript file' do + let(:supplemental_file) { FactoryBot.create(:supplemental_file, :with_transcript_file, :with_transcript_tag, label: 'Test') } + it 'sets the type properly' do + expect(subject[:type]).to eq 'transcript' + end + end + end + describe '.convert_from_srt' do let(:input) { "1\n00:00:03,498 --> 00:00:05,000\n- Example Captions\n" } let(:output) { "WEBVTT\n\n1\n00:00:03.498 --> 00:00:05.000\n- Example Captions" } diff --git a/spec/support/supplemental_files_controller_examples.rb b/spec/support/supplemental_files_controller_examples.rb index b711ef6c0e..365ff25bd5 100644 --- a/spec/support/supplemental_files_controller_examples.rb +++ b/spec/support/supplemental_files_controller_examples.rb @@ -42,13 +42,11 @@ } let(:invalid_update_attributes) { - { file: uploaded_file } + {} } let(:uploaded_file) { fixture_file_upload('/collection_poster.png', 'image/png') } - let(:supplemental_file) { FactoryBot.create(:supplemental_file) } - # This should return the minimal set of values that should be in the session # in order to pass any filters (e.g. authentication) defined in # SupplementalFilesController. Be sure to keep this updated too. @@ -64,27 +62,78 @@ let(:manager) { media_object.collection.managers.first } describe 'security' do - context 'with unauthenticated user' do - it "all routes should return 401" do - expect(post :create, params: { class_id => object.id }).to have_http_status(401) - expect(put :update, params: { class_id => object.id, id: supplemental_file.id }).to have_http_status(401) - expect(delete :destroy, params: { class_id => object.id, id: supplemental_file.id }).to have_http_status(401) - expect(get :show, params: { class_id => object.id, id: supplemental_file.id }).to have_http_status(401) + describe 'ingest api' do + it "all routes should return 401 when no token is present" do + expect(get :index, params: { class_id => object.id, format: 'json' }).to have_http_status(401) + expect(get :show, params: { class_id => object.id, id: supplemental_file.id, format: 'json' }).to have_http_status(401) + expect(post :create, params: { class_id => object.id, format: 'json' }).to have_http_status(401) + expect(put :update, params: { class_id => object.id, id: supplemental_file.id, format: 'json' }).to have_http_status(401) + expect(delete :destroy, params: { class_id => object.id, id: supplemental_file.id }).to have_http_status(401) + end + it "all routes should return 403 when a bad token in present" do + request.headers['Avalon-Api-Key'] = 'badtoken' + expect(get :index, params: { class_id => object.id, format: 'json' }).to have_http_status(403) + expect(get :show, params: { class_id => object.id, id: supplemental_file.id, format: 'json' }).to have_http_status(403) + expect(post :create, params: { class_id => object.id, format: 'json' }).to have_http_status(403) + expect(put :update, params: { class_id => object.id, id: supplemental_file.id, format: 'json' }).to have_http_status(403) + expect(delete :destroy, params: { class_id => object.id, id: supplemental_file.id, format: 'json' }).to have_http_status(403) end end - context 'with end-user without permission' do - before do - login_as :user + describe 'normal auth' do + context 'with unauthenticated user' do + it "all routes should return 401" do + expect(get :index, params: { class_id => object.id }).to have_http_status(401) + expect(get :show, params: { class_id => object.id, id: supplemental_file.id }).to have_http_status(401) + expect(post :create, params: { class_id => object.id }).to have_http_status(401) + expect(put :update, params: { class_id => object.id, id: supplemental_file.id }).to have_http_status(401) + expect(delete :destroy, params: { class_id => object.id, id: supplemental_file.id }).to have_http_status(401) + end end - it "all routes should return 401" do - expect(post :create, params: { class_id => object.id }).to have_http_status(401) - expect(put :update, params: { class_id => object.id, id: supplemental_file.id }).to have_http_status(401) - expect(delete :destroy, params: { class_id => object.id, id: supplemental_file.id }).to have_http_status(401) - expect(get :show, params: { class_id => object.id, id: supplemental_file.id }).to have_http_status(401) + context 'with end-user without permission' do + before do + login_as :user + end + it "all routes should return 401" do + expect(get :index, params: { class_id => object.id }).to have_http_status(401) + expect(get :show, params: { class_id => object.id, id: supplemental_file.id }).to have_http_status(401) + expect(post :create, params: { class_id => object.id }).to have_http_status(401) + expect(put :update, params: { class_id => object.id, id: supplemental_file.id }).to have_http_status(401) + expect(delete :destroy, params: { class_id => object.id, id: supplemental_file.id }).to have_http_status(401) + end end end end + describe "GET #index" do + subject { JSON.parse(response.body) } + let(:master_file) { FactoryBot.create(:master_file, media_object: media_object) } + let(:media_object) { FactoryBot.create(:fully_searchable_media_object) } + let(:supplemental_file) { FactoryBot.create(:supplemental_file, :with_attached_file, parent_id: object.id) } + let(:caption) { FactoryBot.create(:supplemental_file, :with_caption_file, tags: ['caption', 'transcript'], parent_id: object.id) } + let(:transcript) { FactoryBot.create(:supplemental_file, :with_transcript_file, tags: ['transcript', 'machine_generated'], parent_id: object.id) } + let(:object) { object_class == MasterFile ? master_file : media_object } + + before :each do + login_user manager + object.supplemental_files = [supplemental_file, caption, transcript] + object.save + end + + it "returns metadata for all associated supplemental files" do + get :index, params: { class_id => object.id }, session: valid_session + expect(subject.count).to eq 3 + [supplemental_file, caption, transcript].each do |file| + expect(subject.any? { |s| s == JSON.parse(file.as_json.to_json) }).to eq true + end + end + + it "paginates results when requested" do + get :index, params: { class_id => object.id, per_page: 1, page: 1 }, session: valid_session + expect(subject.count).to eq 1 + expect(subject.first.symbolize_keys).to eq supplemental_file.as_json + end + end + describe "GET #show" do let(:master_file) { FactoryBot.create(:master_file, media_object: public_media_object, supplemental_files: [supplemental_file]) } let(:public_media_object) { FactoryBot.create(:fully_searchable_media_object, supplemental_files: [supplemental_file]) } @@ -95,6 +144,13 @@ get :show, params: { class_id => object.id, id: supplemental_file.id }, session: valid_session expect(response).to redirect_to Rails.application.routes.url_helpers.rails_blob_path(supplemental_file.file, disposition: "attachment") end + + context '.json' do + it 'returns the supplemental file metadata' do + get :show, params: { class_id => object.id, id: supplemental_file.id, format: 'json' }, session: valid_session + expect(JSON.parse(response.body).symbolize_keys).to eq supplemental_file.as_json + end + end end describe "POST #create" do @@ -107,52 +163,71 @@ end context 'json request' do + let(:metadata) { { label: 'label', type: 'caption', language: 'French', machine_generated: 1, treat_as_transcript: 1 } } context "with valid params" do - it "updates the SupplementalFile for #{object_class}" do + let(:uploaded_file) { fixture_file_upload(Rails.root.join('spec', 'fixtures', 'captions.vtt'), 'text/vtt') } + let(:valid_create_attributes) { { file: uploaded_file, metadata: metadata.to_json } } + it "creates a SupplementalFile for #{object_class}" do expect { - post :create, params: { class_id => object.id, supplemental_file: valid_create_attributes, format: :json }, session: valid_session + post :create, params: { class_id => object.id, **valid_create_attributes, format: :json }, session: valid_session }.to change { object.reload.supplemental_files.size }.by(1) expect(response).to have_http_status(:created) - expect(response.location).to eq "/#{object_class.model_name.plural}/#{object.id}/supplemental_files/#{assigns(:supplemental_file).id}" + expect(JSON.parse(response.body)).to eq ({ "supplemental_file" => assigns(:supplemental_file).id }) expect(object.supplemental_files.first.id).to eq 1 expect(object.supplemental_files.first.label).to eq 'label' expect(object.supplemental_files.first.file).to be_attached end - let(:tags) { ["transcript"] } - let(:uploaded_file) { fixture_file_upload('/captions.vtt', 'text/vtt') } - let(:valid_create_attributes_with_tags) { valid_create_attributes.merge(tags: tags) } + context 'with just metadata' do + it "creates a SupplementalFile for #{object_class}" do + request.headers['Content-Type'] = 'application/json' + expect { + post :create, params: { class_id => object.id, metadata: metadata.to_json, format: :json }, session: valid_session + }.to change { object.reload.supplemental_files.size }.by(1) + expect(response).to have_http_status(:created) + expect(JSON.parse(response.body)).to eq ({ "supplemental_file" => assigns(:supplemental_file).id }) - it "creates a SupplementalFile with tags for #{object_class}" do - expect { - post :create, params: { class_id => object.id, supplemental_file: valid_create_attributes_with_tags, format: :json }, session: valid_session - }.to change { object.reload.supplemental_files.size }.by(1) - expect(response).to have_http_status(:created) - expect(response.location).to eq "/#{object_class.model_name.plural}/#{object.id}/supplemental_files/#{assigns(:supplemental_file).id}" + expect(object.supplemental_files.first.id).to eq 1 + expect(object.supplemental_files.first.label).to eq 'label' + expect(object.supplemental_files.first.language).to eq 'fre' + expect(object.supplemental_files.first.tags).to match_array ['caption', 'transcript', 'machine_generated'] + expect(object.supplemental_files.first.file).to_not be_attached + end - expect(object.supplemental_files.first.id).to eq 1 - expect(object.supplemental_files.first.label).to eq 'label' - expect(object.supplemental_files.first.tags).to eq tags - expect(object.supplemental_files.first.file).to be_attached + context 'with just inlined metadata' do + it "creates a SupplementalFile for #{object_class}" do + request.headers['Content-Type'] = 'application/json' + expect { + post :create, params: { class_id => object.id, **metadata, format: :json }, session: valid_session + }.to change { object.reload.supplemental_files.size }.by(1) + expect(response).to have_http_status(:created) + expect(JSON.parse(response.body)).to eq ({ "supplemental_file" => assigns(:supplemental_file).id }) + + expect(object.supplemental_files.first.id).to eq 1 + expect(object.supplemental_files.first.label).to eq 'label' + expect(object.supplemental_files.first.language).to eq 'fre' + expect(object.supplemental_files.first.tags).to match_array ['caption', 'transcript', 'machine_generated'] + expect(object.supplemental_files.first.file).to_not be_attached + end + end end - context 'with mime type that does not match extension' do - let(:tags) { ['caption'] } - let(:extension) { 'srt' } - let(:uploaded_file) { fixture_file_upload(Rails.root.join('spec', 'fixtures', 'captions.srt'), 'text/plain') } - it "creates a SupplementalFile with correct content_type" do - expect{ - post :create, params: { class_id => object.id, supplemental_file: valid_create_attributes_with_tags, format: :json}, session: valid_session + context 'with incomplete metadata' do + let(:metadata) { { label: 'label' } } + it "creates a SupplementalFile for #{object_class} with default values" do + request.headers['Content-Type'] = 'application/json' + expect { + post :create, params: { class_id => object.id, metadata: metadata.to_json, format: :json }, session: valid_session }.to change { object.reload.supplemental_files.size }.by(1) expect(response).to have_http_status(:created) - expect(response.location).to eq "/#{object_class.model_name.plural}/#{object.id}/supplemental_files/#{assigns(:supplemental_file).id}" + expect(JSON.parse(response.body)).to eq ({ "supplemental_file" => assigns(:supplemental_file).id }) expect(object.supplemental_files.first.id).to eq 1 expect(object.supplemental_files.first.label).to eq 'label' - expect(object.supplemental_files.first.tags).to eq tags - expect(object.supplemental_files.first.file).to be_attached - expect(object.supplemental_files.first.file.content_type).to eq Mime::Type.lookup_by_extension(extension) + expect(object.supplemental_files.first.language).to eq 'eng' + expect(object.supplemental_files.first.tags).to eq [] + expect(object.supplemental_files.first.file).to_not be_attached end end end @@ -163,11 +238,18 @@ expect(response).to have_http_status(400) end end + + context "with invalid file type" do + it "returns a 500" do + post :create, params: { class_id=> object.id, file: uploaded_file, type: 'caption', format: :json }, session: valid_session + expect(response).to have_http_status(500) + end + end end context 'html request' do context "with valid params" do - it "updates the SupplementalFile" do + it "creates a SupplementalFile for #{object_class}" do expect { post :create, params: { class_id => object.id, supplemental_file: valid_create_attributes, format: :html }, session: valid_session }.to change { object.reload.supplemental_files.size }.by(1) @@ -180,6 +262,64 @@ end end + context "including tags" do + let(:tags) { ["transcript"] } + let(:uploaded_file) { fixture_file_upload('/captions.vtt', 'text/vtt') } + let(:valid_create_attributes_with_tags) { valid_create_attributes.merge(tags: tags) } + + it "creates a SupplementalFile with tags for #{object_class}" do + expect { + post :create, params: { class_id => object.id, supplemental_file: valid_create_attributes_with_tags, format: :html }, session: valid_session + }.to change { object.reload.supplemental_files.size }.by(1) + expect(response).to redirect_to("http://#{@request.host}/media_objects/#{media_object.id}/edit?step=file-upload") + expect(flash[:success]).to be_present + + expect(object.supplemental_files.first.id).to eq 1 + expect(object.supplemental_files.first.label).to eq 'label' + expect(object.supplemental_files.first.tags).to eq tags + expect(object.supplemental_files.first.file).to be_attached + end + end + + context 'with mime type that does not match extension' do + let(:tags) { ['caption'] } + let(:extension) { 'srt' } + let(:uploaded_file) { fixture_file_upload(Rails.root.join('spec', 'fixtures', 'captions.srt'), 'text/plain') } + let(:valid_create_attributes_with_tags) { valid_create_attributes.merge(tags: tags) } + it "creates a SupplementalFile with correct content_type" do + expect{ + post :create, params: { class_id => object.id, supplemental_file: valid_create_attributes_with_tags, format: :html }, session: valid_session + }.to change { object.reload.supplemental_files.size }.by(1) + expect(response).to redirect_to("http://#{@request.host}/media_objects/#{media_object.id}/edit?step=file-upload") + expect(flash[:success]).to be_present + + expect(object.supplemental_files.first.id).to eq 1 + expect(object.supplemental_files.first.label).to eq 'label' + expect(object.supplemental_files.first.tags).to eq tags + expect(object.supplemental_files.first.file).to be_attached + expect(object.supplemental_files.first.file.content_type).to eq Mime::Type.lookup_by_extension(extension) + end + end + + context 'API with just a file' do + let(:uploaded_file) { fixture_file_upload(Rails.root.join('spec', 'fixtures', 'captions.srt'), 'text/plain') } + before { ApiToken.create token: 'secret_token', username: 'archivist1@example.com', email: 'archivist1@example.com' } + it "creates a SupplementalFile for #{object_class}" do + request.headers['Avalon-Api-Key'] = 'secret_token' + expect { + post :create, params: { class_id => object.id, file: uploaded_file, format: :html }, session: valid_session + }.to change { object.reload.supplemental_files.size }.by(1) + expect(response).to have_http_status(:created) + expect(JSON.parse(response.body)).to eq ({ "supplemental_file" => assigns(:supplemental_file).id }) + + expect(object.supplemental_files.first.id).to eq 1 + expect(object.supplemental_files.first.label).to eq 'captions.srt' + expect(object.supplemental_files.first.language).to eq Settings.caption_default.language + expect(object.supplemental_files.first.tags).to be_empty + expect(object.supplemental_files.first.file).to be_attached + end + end + context "with invalid params" do it "returns a 400" do post :create, params: { class_id => object.id, supplemental_file: invalid_create_attributes, format: :html }, session: valid_session @@ -189,7 +329,7 @@ end end - context 'does not attach' do + context 'fails to attach file' do let(:supplemental_file) { FactoryBot.build(:supplemental_file) } before do allow(SupplementalFile).to receive(:new).and_return(supplemental_file) @@ -228,14 +368,27 @@ end context 'json request' do - context "with valid params" do - it "creates a new SupplementalFile" do + before :each do + request.headers['Content-Type'] = 'application/json' + end + context "with valid metadata params" do + let(:valid_update_attributes) { { label: 'new label', type: 'transcript', machine_generated: 1 }.to_json } + it "updates the SupplementalFile metadata for #{object_class}" do expect { - put :update, params: { class_id => object.id, id: supplemental_file.id, supplemental_file: valid_update_attributes, format: :json }, session: valid_session + put :update, params: { class_id => object.id, id: supplemental_file.id, metadata: valid_update_attributes, format: :json }, session: valid_session }.to change { object.reload.supplemental_files.first.label }.from('label').to('new label') + .and change { object.reload.supplemental_files.first.tags }.from([]).to(['transcript', 'machine_generated']) expect(response).to have_http_status(:ok) - expect(response.location).to eq "/#{object_class.model_name.plural}/#{object.id}/supplemental_files/#{assigns(:supplemental_file).id}" + expect(response.body).to eq({ "supplemental_file": supplemental_file.id }.to_json) + end + end + + context "with new file and valid metadata params" do + let(:file_update) { fixture_file_upload(Rails.root.join('spec', 'fixtures', 'captions.vtt'), 'text/vtt') } + it "returns a 400" do + put :update, params: { class_id => object.id, id: supplemental_file.id, file: file_update, supplemental_file: valid_update_attributes, format: :json }, session: valid_session + expect(response).to have_http_status(400) end end @@ -256,7 +409,7 @@ context 'html request' do context "with valid params" do - it "creates a new SupplementalFile" do + it "updates the SupplementalFile for #{object_class}" do expect { put :update, params: { class_id => object.id, id: supplemental_file.id, supplemental_file: valid_update_attributes, format: :html }, session: valid_session }.to change { master_file.reload.supplemental_files.first.label }.from('label').to('new label') @@ -322,6 +475,29 @@ end end + context "API with new file" do + let(:file_update) { fixture_file_upload(Rails.root.join('spec', 'fixtures', 'captions.vtt'), 'text/vtt') } + before { ApiToken.create token: 'secret_token', username: 'archivist1@example.com', email: 'archivist1@example.com' } + it "updates the SupplementalFile attached file for #{object_class}" do + request.headers['Avalon-Api-Key'] = 'secret_token' + expect { + put :update, params: { class_id => object.id, id: supplemental_file.id, file: file_update, format: :html }, session: valid_session + }.to change { object.reload.supplemental_files.first.file } + + expect(response).to have_http_status(:ok) + expect(response.body).to eq({ "supplemental_file": supplemental_file.id }.to_json) + end + end + + context "API without file" do + before { ApiToken.create token: 'secret_token', username: 'archivist1@example.com', email: 'archivist1@example.com' } + it "returns a 400" do + request.headers['Avalon-Api-Key'] = 'secret_token' + put :update, params: { class_id=> object.id, id: supplemental_file.id, metadata: valid_update_attributes, format: :html }, session: valid_session + expect(response).to have_http_status(400) + end + end + context "with invalid params" do it "returns a 400" do put :update, params: { class_id => object.id, id: supplemental_file.id, supplemental_file: invalid_update_attributes, format: :html }, session: valid_session @@ -337,6 +513,16 @@ expect(flash[:error]).to be_present end end + + context "API updating caption with invalid file type" do + let(:supplemental_file) { FactoryBot.create(:supplemental_file, :with_caption_tag, skip_file_type: true) } + before { ApiToken.create token: 'secret_token', username: 'archivist1@example.com', email: 'archivist1@example.com' } + it "returns a 500" do + request.headers['Avalon-Api-Key'] = 'secret_token' + put :update, params: { class_id=> object.id, id: supplemental_file.id, file: uploaded_file, format: :html }, session: valid_session + expect(response).to have_http_status(500) + end + end end end From a78d812263c79d36054f62d41b3f69eb183fee04 Mon Sep 17 00:00:00 2001 From: dwithana Date: Thu, 6 Jun 2024 08:42:56 -0700 Subject: [PATCH 065/152] Add content search endpoint to CORS allowlist --- config/application.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/config/application.rb b/config/application.rb index 562c2af5ae..ad448a4452 100644 --- a/config/application.rb +++ b/config/application.rb @@ -52,6 +52,7 @@ class Application < Rails::Application resource '/master_files/*/supplemental_files/*', headers: :any, methods: [:get] resource '/playlists/*/manifest.json', headers: :any, credentials: true, methods: [:get] resource '/timelines/*/manifest.json', headers: :any, methods: [:get, :post] + resource '/master_files/*/search', headers: :any, methods: [:get] end end From 429ea7eb3da95740aa7660f79c05c7f0cccb99bb Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Thu, 6 Jun 2024 15:53:56 -0400 Subject: [PATCH 066/152] Fix update_index callbacks --- app/models/supplemental_file.rb | 6 +++- spec/models/supplemental_file_spec.rb | 48 +++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/app/models/supplemental_file.rb b/app/models/supplemental_file.rb index 2659f18707..2124c8ba59 100644 --- a/app/models/supplemental_file.rb +++ b/app/models/supplemental_file.rb @@ -32,7 +32,7 @@ class SupplementalFile < ApplicationRecord # Need to prepend so this runs before the callback added by `has_one_attached` above # See https://github.com/rails/rails/issues/37304 - after_create_commit :update_index, prepend: true, unless: :skip_index + after_create_commit :index_file, prepend: true, unless: :skip_index after_update_commit :update_index, prepend: true def attach_file(new_file) @@ -98,9 +98,13 @@ def self.convert_from_srt(srt) "WEBVTT\n\n#{conversion}".strip end + # We need to use both after_create_commit and after_update_commit to update the index properly in both cases. + # However, they cannot call the same method name or only the last defined callback will take effect. + # https://guides.rubyonrails.org/active_record_callbacks.html#aliases-for-after-commit def update_index ActiveFedora::SolrService.add(to_solr, softCommit: true) end + alias index_file update_index # Creates a solr document hash for the {#object} # @return [Hash] the solr document diff --git a/spec/models/supplemental_file_spec.rb b/spec/models/supplemental_file_spec.rb index 9ad8838fd9..5169ecc2ef 100644 --- a/spec/models/supplemental_file_spec.rb +++ b/spec/models/supplemental_file_spec.rb @@ -44,6 +44,12 @@ expect(subject.errors[:file_type]).not_to be_empty end end + context "caption metadata only with skip_file_type" do + let(:subject) { FactoryBot.build(:supplemental_file, :with_caption_tag, skip_file_type: true) } + it 'should skip validation' do + expect(subject.valid?).to be_truthy + end + end end describe 'scopes' do @@ -64,6 +70,48 @@ end end + describe '#update_index' do + let(:transcript) { FactoryBot.build(:supplemental_file, :with_transcript_file, :with_transcript_tag) } + context 'on create' do + it 'triggers callback' do + expect(transcript).to receive(:index_file) + transcript.save + end + + it 'indexes the transcript' do + transcript.save + solr_doc = ActiveFedora::SolrService.query("id:#{RSolr.solr_escape(transcript.to_global_id.to_s)}").first + expect(solr_doc["transcript_tsim"]).to eq ["00:00:03.500 --> 00:00:05.000 Example captions"] + end + + context 'skip index' do + let(:transcript) { FactoryBot.build(:supplemental_file, :with_transcript_file, :with_transcript_tag, skip_index: true) } + it 'does not trigger callback' do + expect(transcript).to_not receive(:index_file) + transcript.save + end + end + end + + context 'on update' do + let(:transcript) { FactoryBot.create(:supplemental_file, :with_transcript_file, :with_transcript_tag) } + it 'triggers callback' do + transcript.file.attach(fixture_file_upload(Rails.root.join('spec', 'fixtures', 'chunk_test.vtt'), 'text/vtt')) + expect(transcript).to receive(:update_index) + transcript.save + end + + it 'updates the indexed transcript' do + before_doc = ActiveFedora::SolrService.query("id:#{RSolr.solr_escape(transcript.to_global_id.to_s)}").first + expect(before_doc["transcript_tsim"].first).to eq "00:00:03.500 --> 00:00:05.000 Example captions" + transcript.file.attach(fixture_file_upload(Rails.root.join('spec', 'fixtures', 'chunk_test.vtt'), 'text/vtt')) + transcript.save + after_doc = ActiveFedora::SolrService.query("id:#{RSolr.solr_escape(transcript.to_global_id.to_s)}").first + expect(after_doc["transcript_tsim"].first).to eq("00:00:01.200 --> 00:00:21.000 [music]") + end + end + end + describe '#to_solr' do let(:caption) { FactoryBot.create(:supplemental_file, :with_caption_file, :with_caption_tag) } let(:transcript) { FactoryBot.create(:supplemental_file, :with_transcript_file, :with_transcript_tag) } From 0f6fecdbd72e3dc93b93ad2a16c4a2e6e570e2e7 Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Thu, 6 Jun 2024 16:43:24 -0400 Subject: [PATCH 067/152] Fix derivative download for collection managers --- app/models/ability.rb | 4 ++++ spec/controllers/master_files_controller_spec.rb | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/app/models/ability.rb b/app/models/ability.rb index 17786e60d9..13bd2aad3e 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -133,6 +133,10 @@ def custom_permissions(user=nil, session=nil) @user.in?(collection.managers) end + can :manage, [Admin::Collection, SpeedyAF::Proxy::Admin::Collection] do |collection| + @user.in?(collection.managers) + end + can :update_depositors, [Admin::Collection, SpeedyAF::Proxy::Admin::Collection] do |collection| is_editor_of?(collection) end diff --git a/spec/controllers/master_files_controller_spec.rb b/spec/controllers/master_files_controller_spec.rb index f6e3e07fd7..cbbb9acdbc 100644 --- a/spec/controllers/master_files_controller_spec.rb +++ b/spec/controllers/master_files_controller_spec.rb @@ -756,7 +756,7 @@ class << file let(:med_derivative) { FactoryBot.create(:derivative, quality: 'medium') } before do allow(Settings.derivative).to receive(:allow_download).and_return(true) - login_as :administrator + login_user collection.managers.first end it 'should download the high quality derivative' do From 23090dc79e1e7348147be6b44ae8bf616c400f4e Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Fri, 7 Jun 2024 09:53:55 -0400 Subject: [PATCH 068/152] Use a download specific ability check Co-authored-by: Chris Colvard --- app/controllers/master_files_controller.rb | 2 +- app/models/ability.rb | 8 ++++---- app/views/media_objects/_file_upload.html.erb | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/controllers/master_files_controller.rb b/app/controllers/master_files_controller.rb index ff23133395..a2e1d16869 100644 --- a/app/controllers/master_files_controller.rb +++ b/app/controllers/master_files_controller.rb @@ -325,7 +325,7 @@ def transcript end def download_derivative - authorize! :manage, @master_file.media_object.collection + authorize! :download, @master_file begin path = derivative_path diff --git a/app/models/ability.rb b/app/models/ability.rb index 13bd2aad3e..1ee62162a6 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -133,10 +133,6 @@ def custom_permissions(user=nil, session=nil) @user.in?(collection.managers) end - can :manage, [Admin::Collection, SpeedyAF::Proxy::Admin::Collection] do |collection| - @user.in?(collection.managers) - end - can :update_depositors, [Admin::Collection, SpeedyAF::Proxy::Admin::Collection] do |collection| is_editor_of?(collection) end @@ -157,6 +153,10 @@ def custom_permissions(user=nil, session=nil) can? :edit, master_file.media_object end + can :download, [MasterFile, SpeedyAF::Proxy::MasterFile] do |master_file| + @user.in?(master_file.media_object.collection.managers) + end + # Users logged in through LTI cannot share can :share, [MediaObject, SpeedyAF::Proxy::MediaObject] end diff --git a/app/views/media_objects/_file_upload.html.erb b/app/views/media_objects/_file_upload.html.erb index e5e93bb6ee..03167c123e 100644 --- a/app/views/media_objects/_file_upload.html.erb +++ b/app/views/media_objects/_file_upload.html.erb @@ -91,7 +91,7 @@ Unless required by applicable law or agreed to in writing, software distributed Move - <% if Settings.derivative.allow_download && (current_ability.can? :manage, @media_object.collection || current_ability.is_administrator?) %> + <% if Settings.derivative.allow_download && (current_ability.can? :download, section) %> <%= link_to "Download", download_derivative_master_file_path(section.id), class: 'btn btn-sm btn-outline', target: '_self' %> From 8cf40fb169e80dd7f231fe8f3307f9afc8d4825f Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Fri, 7 Jun 2024 10:43:12 -0400 Subject: [PATCH 069/152] Add transcript language edit to UI --- app/assets/stylesheets/avalon.scss | 2 +- app/models/supplemental_file.rb | 2 +- .../_supplemental_files_list.html.erb | 21 ++++++++++++------- config/settings.yml | 1 + 4 files changed, 17 insertions(+), 9 deletions(-) diff --git a/app/assets/stylesheets/avalon.scss b/app/assets/stylesheets/avalon.scss index c22e44685b..2a686d88f2 100644 --- a/app/assets/stylesheets/avalon.scss +++ b/app/assets/stylesheets/avalon.scss @@ -978,7 +978,7 @@ h5.card-title { height: 25px; } - &.captions { + &.captions, &.transcripts { div.supplemental-file-data.is-editing:last-child { margin-bottom: 1.75rem; } diff --git a/app/models/supplemental_file.rb b/app/models/supplemental_file.rb index 25a2473740..9fa49aa74f 100644 --- a/app/models/supplemental_file.rb +++ b/app/models/supplemental_file.rb @@ -41,7 +41,7 @@ def attach_file(new_file) extension = File.extname(new_file.original_filename) self.file.content_type = Mime::Type.lookup_by_extension(extension.slice(1..-1)).to_s if extension == '.srt' self.label = file.filename.to_s if label.blank? - self.language = tags.include?('caption') ? Settings.caption_default.language : 'eng' + self.language = Settings.caption_default.language end def mime_type diff --git a/app/views/media_objects/_supplemental_files_list.html.erb b/app/views/media_objects/_supplemental_files_list.html.erb index a6b6389ac4..0e30ded934 100644 --- a/app/views/media_objects/_supplemental_files_list.html.erb +++ b/app/views/media_objects/_supplemental_files_list.html.erb @@ -15,7 +15,7 @@ Unless required by applicable law or agreed to in writing, software distributed %> <% if section.supplemental_files_json.present? %> <% files=tag.empty? ? section.supplemental_files(tag: nil) : section.supplemental_files(tag: tag) %> -
    captions<% end %>"> +
    <% if tag == "caption" %>
    @@ -40,12 +40,19 @@ Unless required by applicable law or agreed to in writing, software distributed <%= form.text_field :label, id: "supplemental_file_input_#{section.id}_#{file.id}", value: file.label %>
    <% if tag == 'transcript' %> - - <%= label_tag "machine_generated_#{file.id}", class: "ml-3" do %> - <%= check_box_tag "machine_generated_#{file.id}", '1', file.machine_generated? %> - Machine Generated - <% end %> - +
    + <%= form.text_field :language, id: "supplemental_file_language_#{section.id}_#{file.id}", value: LanguageTerm.find(file.language).text, + class: "typeahead from-model form-control", + data: { model: 'languageTerm', validate: false } %> +
    +
    + + <%= label_tag "machine_generated_#{file.id}", class: "checkbox", style: "white-space: nowrap; padding-left: 0.45rem;" do %> + <%= check_box_tag "machine_generated_#{file.id}", '1', file.machine_generated? %> + Machine Generated + <% end %> + +
    <% end %> <% if tag == 'caption' %>
    diff --git a/config/settings.yml b/config/settings.yml index 072e366f8c..d8a81d3f4e 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -106,6 +106,7 @@ controlled_digital_lending: # Choose whether every collection has CDL enabled or disabled by default collections_enabled: false default_lending_period: 'P14D' # ISO8601 duration format: P14D == 14.days, PT8H == 8.hours, etc. +# Caption default field also sets the default language information for transcript files caption_default: # Language should be 3 letter ISO 639-2 code language: 'eng' From a169054052613173749b3569c1c17792333bda04 Mon Sep 17 00:00:00 2001 From: cjcolvar Date: Mon, 10 Jun 2024 14:33:46 -0400 Subject: [PATCH 070/152] Accept known issue of identifiers being lower-cased in JSON responses --- app/models/media_object.rb | 3 ++- spec/controllers/media_objects_controller_spec.rb | 9 ++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/app/models/media_object.rb b/app/models/media_object.rb index 8d31885aa8..84767ea07b 100644 --- a/app/models/media_object.rb +++ b/app/models/media_object.rb @@ -319,7 +319,8 @@ def to_solr(include_child_fields: false) solr_doc["creator_ssort"] = Array(self.creator).join(', ') solr_doc["date_ingested_ssim"] = self.create_date.strftime "%F" if self.create_date.present? solr_doc['avalon_resource_type_ssim'] = self.avalon_resource_type - solr_doc['identifier_ssim'] = self.identifier + # Downcasing identifier allows for case-insensitive searching but has the side effect of causing all identiiers to be lower case in JSON responses + solr_doc['identifier_ssim'] = self.identifier.map(&:downcase) solr_doc['note_ssm'] = self.note.collect { |n| n.to_json } solr_doc['other_identifier_ssm'] = self.other_identifier.collect { |oi| oi.to_json } solr_doc['related_item_url_ssm'] = self.related_item_url.collect { |r| r.to_json } diff --git a/spec/controllers/media_objects_controller_spec.rb b/spec/controllers/media_objects_controller_spec.rb index ac5c550473..d9ecc4bdfa 100644 --- a/spec/controllers/media_objects_controller_spec.rb +++ b/spec/controllers/media_objects_controller_spec.rb @@ -1204,7 +1204,14 @@ expect(json['summary']).to eq(media_object.abstract) ingest_api_hash = media_object.to_ingest_api_hash(false) - json['fields'].each { |k,v| expect(v).to eq(ingest_api_hash[:fields][k.to_sym]) } + json['fields'].each do |k,v| + # Known issue: identifiers are downcased when indexing to allow for case-insensitive searching + if k.to_sym == :identifier + expect(v).to eq(ingest_api_hash[:fields][k.to_sym].map(&:downcase)) + else + expect(v).to eq(ingest_api_hash[:fields][k.to_sym]) + end + end # Symbolize keys for master files and derivatives json['files'].each do |mf| From d1fa0c1b03cb77be821b8f82173f2fc0652a8336 Mon Sep 17 00:00:00 2001 From: charumitraravi <52535309+charumitraravi@users.noreply.github.com> Date: Mon, 10 Jun 2024 21:27:32 -0400 Subject: [PATCH 071/152] Added to collection cases , playlist, created browse, handled different environments --- spec/cypress/cypress_dev.config.js | 49 ++- spec/cypress/integration/browse_spec.js | 58 ++++ spec/cypress/integration/collections_spec.js | 118 ++++++- spec/cypress/integration/navigation_spec.js | 7 +- spec/cypress/integration/playlist_spec.js | 319 +++++++++++++------ 5 files changed, 426 insertions(+), 125 deletions(-) create mode 100644 spec/cypress/integration/browse_spec.js diff --git a/spec/cypress/cypress_dev.config.js b/spec/cypress/cypress_dev.config.js index e181c90d40..7a81ccab03 100644 --- a/spec/cypress/cypress_dev.config.js +++ b/spec/cypress/cypress_dev.config.js @@ -1,15 +1,9 @@ const { defineConfig } = require("cypress"); +const path = require('path'); +const fs = require('fs'); module.exports = defineConfig({ - env: { - "USERS_ADMINISTRATOR_EMAIL": "archivist1@example.com", - "USERS_ADMINISTRATOR_PASSWORD": "archivist1", - "USERS_USER_EMAIL":"user1@example.com", - "USERS_USER_PASSWORD": "testing_user1", - "MEDIA_OBJECT_ID": "fj236208t", - "MEDIA_OBJECT_TITLE":"Beginning Responsibility: Lunchroom Manners", - "SEARCH_COLLECTION":"7.7 regression test", - }, + downloadsFolder: "spec/cypress/downloads", fixturesFolder: "spec/cypress/fixtures", screenshotsFolder: "spec/cypress/screenshots", @@ -18,11 +12,44 @@ module.exports = defineConfig({ e2e: { setupNodeEvents(on, config) { // implement node event listeners here + const environmentName = process.env.CYPRESS_ENV || 'local'; + const environmentFilename = `cypress.env.${environmentName}.json`; + const environmentPath = path.resolve(__dirname, environmentFilename); + + console.log('Environment name: %s', environmentName); + console.log('Environment path: %s', environmentPath); + + if (fs.existsSync(environmentPath)) { + console.log('Loading %s', environmentFilename); + const settings = require(environmentPath); + + // Set baseUrl if defined in the environment settings + if (settings.baseUrl) { + config.baseUrl = settings.baseUrl; + console.log('Loading the baseURL.... %s', config.baseUrl); + } + + // Merge environment variables + if (settings.env) { + config.env = { + ...config.env, + ...settings.env, + }; + } + + console.log('Loaded settings for environment %s', environmentName); + } else { + console.error(`Environment config file ${environmentFilename} not found`); + } + + return config; + }, - baseUrl: "https://avalon-dev.dlib.indiana.edu/", + + // baseUrl: "https://avalon-dev.dlib.indiana.edu/", supportFile: "spec/cypress/support/e2e.js", specPattern: "spec/cypress/integration/**/*.js" }, - }); + \ No newline at end of file diff --git a/spec/cypress/integration/browse_spec.js b/spec/cypress/integration/browse_spec.js new file mode 100644 index 0000000000..4bc3967575 --- /dev/null +++ b/spec/cypress/integration/browse_spec.js @@ -0,0 +1,58 @@ +/* + * Copyright 2011-2024, 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 --- +*/ + +context('Browse', () => { + it('should use the base URL', () => { + cy.visit('/'); // This will navigate to CYPRESS_BASE_URL + cy.screenshot() + }); + + // checks navigation to Browse + it('.browse_navigation()', () => { + cy.login('administrator') + cy.visit('/') + cy.contains('Browse').click() + }) + + it('Verify searching for an item by keyword - @T9c1158fb', () => { + cy.visit('/') + cy.get("li[class='nav-item'] a[class='nav-link']").click() + //create a dynamic item here and use a portion of it as a search keyword + const media_object_title = Cypress.env('MEDIA_OBJECT_TITLE') + cy.get("input.global-search-input[placeholder='Search this site']").first().type(media_object_title).should('have.value', media_object_title) // Only yield inputs within form + cy.get('button.global-search-submit').first().click() + cy.contains('a', media_object_title) + .should('exist') + .and('be.visible'); +}) + +it('Verify browsing items by a format - @Tb477685f', () => { + cy.visit('/') + cy.get("li[class='nav-item'] a[class='nav-link']").click() + cy.contains('button', 'Format').click() + cy.contains('a', 'Moving Image').click() + cy.get('.constraint-value').within(() => { + cy.get('.filter-value[title="Moving Image"]') + .should('contain.text', 'Moving Image') + .and('be.visible'); + }); + //can assert the filtered items here +}) + + +}); + + diff --git a/spec/cypress/integration/collections_spec.js b/spec/cypress/integration/collections_spec.js index e1fd4a199e..6f9ec88d65 100644 --- a/spec/cypress/integration/collections_spec.js +++ b/spec/cypress/integration/collections_spec.js @@ -19,7 +19,12 @@ context('Collections', () => { const search_collection = Cypress.env('SEARCH_COLLECTION') const collection_title = `Automation collection title ${Math.floor(Math.random() * 10000) + 1}` - + Cypress.on('uncaught:exception', (err, runnable) => { + // Prevents Cypress from failing the test due to uncaught exceptions in the application code - TypeError: Cannot read properties of undefined (reading 'scrollDown') + if (err.message.includes('Cannot read properties of undefined (reading \'success\')')) { + return false; + } + }); // checks navigation to Browse it('Verify whether an admin user is able to create a collection - @T553cda51', () => { cy.login('administrator') @@ -60,12 +65,121 @@ context('Collections', () => { //slice a random portion of the collection title as the search keyword to ensure variablitity in testing const search_keyword = search_collection.slice(startIndex, startIndex + sliceLength) cy.get('input[placeholder="Search collections..."]').type(search_keyword).should('have.value', search_keyword) - cy.screenshot('search'); cy.get('.card-body').contains('a', search_collection); +}) + + +it('Verify whether a user is able to update Collection information - @Ta1b2fef8', () => { + cy.login('administrator') + cy.visit('/') + cy.get('#manageDropdown').click() + cy.contains('Manage Content').click() + cy.contains('a', collection_title).click(); + cy.get('.admin-collection-details') + .contains('button', 'Edit Collection Info') + .click(); + + //update description + var updatedDescription = ' Adding more details to collection description' + cy.get('#admin_collection_description').invoke('val').then((existingText) => { + updatedDescription = existingText + updatedDescription; + cy.get('#admin_collection_description').type(updatedDescription); + }); + //update title + var new_title = `Updated automation title ${Math.floor(Math.random() * 10000) + 1}` + cy.get('#admin_collection_name').clear().type(new_title); + + //update contact email + cy.get('#admin_collection_contact_email').clear().type('test@yopmail.com'); + + cy.get('input[value="Update Collection"]').click(); + + // Validate updated collection title and update the collection_title global variable + cy.get('.admin-collection-details h2').should('contain.text', new_title).then(() => { + // Update the global variable collection_title with new_title if the assertion passes + collection_title = new_title; + }); + + // Validate updated contact email + cy.get('.admin-collection-details').within(() => { + cy.get('a[href="mailto:test@yopmail.com"]') + .should('have.text', 'test@yopmail.com') + }); + //validate updated description + cy.get('.admin-collection-details .collection-description').should('contain.text', updatedDescription); + +}) + +it('Verify whether aan admin/manager is able assign other users as managers to the collection - @T3c428871', () => { + cy.login('administrator') + cy.visit('/') + cy.get('#manageDropdown').click() + cy.contains('Manage Content').click() + cy.contains('a', collection_title).click(); + const user_manager = Cypress.env('USER_MANAGER') + cy.get("#add_manager_display").type(user_manager).should('have.value', user_manager) + // Verify that the correct suggestions appear in the dropdown and click it + cy.get('.tt-menu .tt-suggestion') + .should('be.visible') + .and('contain', user_manager).click(); + cy.get('button[name="submit_add_manager"]') + .click(); + //reload the page to ensure that the data is updated in the backend + cy.reload(true); + + cy.get('table.table-hover') + .find('td.access_list_label') + .contains('label', user_manager) + .should('be.visible'); + + //Additional assertions to add :Login as user_manager and validate that the collection is visible in the "Manage page" and/or API validation +}) + + + +it('Verify changing item access - Collection staff only (New items) - @T9978b4f7', () => { + cy.login('administrator') + cy.visit('/') + cy.get('#manageDropdown').click() + cy.contains('Manage Content').click() + cy.contains('a', collection_title).click(); + cy.get('.item-access').within(() => { + cy.contains('label', 'Collection staff only') + .find('input[type="radio"]').click().should('be.checked'); + cy.get('input[value = "Save Setting"]').click() + }); + //reload the page to ensure that the data is updated in the backend + cy.reload() + cy.contains('label', 'Collection staff only') + .find('input[type="radio"]').should('be.checked'); + + //Add UI and/or API assertions here............Assert via UI by opening the create item page and verifying the default access control }) +it('Verify changing item access - Collection staff only (Existing items) - @Tdcf756bd', () => { + cy.login('administrator') + cy.visit('/') + cy.get('#manageDropdown').click() + cy.contains('Manage Content').click() + cy.contains('a', collection_title).click(); + cy.get('.item-access').within(() => { + cy.contains('label', 'Collection staff only') + .find('input[type="radio"]').click().should('be.checked'); + cy.get('input[name = "apply_to_existing"]').click() + }); + + //reload the page to ensure that the data is updated in the backend + cy.reload() + cy.contains('label', 'Collection staff only') + .find('input[type="radio"]').should('be.checked'); + +//Add UI and API assertions here............Assert via UI by opening the an ecisting item within the collection and verifying the default access control + +}) + +//Teardown code : delete the created collection it('Verify deleting a collection - @T959a56df', () => { cy.login('administrator') cy.visit('/') diff --git a/spec/cypress/integration/navigation_spec.js b/spec/cypress/integration/navigation_spec.js index 4e6fb4bfe9..763a6176e4 100644 --- a/spec/cypress/integration/navigation_spec.js +++ b/spec/cypress/integration/navigation_spec.js @@ -72,10 +72,5 @@ context('Navigations', () => { }) // Search - is able to enter keyword and perform search - it('.search()', () => { - cy.visit('/') - cy.get("li[class='nav-item'] a[class='nav-link']").click() - cy.get("input.global-search-input[placeholder='Search this site']").first().type('lunchroom').should('have.value', 'lunchroom') // Only yield inputs within form - cy.get('button.global-search-submit').first().click() - }) + }) diff --git a/spec/cypress/integration/playlist_spec.js b/spec/cypress/integration/playlist_spec.js index 5c4f811641..a3bc80d2ec 100644 --- a/spec/cypress/integration/playlist_spec.js +++ b/spec/cypress/integration/playlist_spec.js @@ -1,148 +1,255 @@ -/* +/* * Copyright 2011-2024, 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 --- -*/ + */ context('Playlists', () => { - - // checks navigation when create new playlist is accessed + //Playlist names start with '_' character for easy navigation without pagination + var playlist_title = `_Automation playlist title ${Math.floor(Math.random() * 10000) + 1 + }`; + var playlist_description = `${playlist_title} description`; + var playlist_title_public = `_Automation public playlist title ${Math.floor(Math.random() * 10000) + 1 + }`; + var playlist_description_public = `${playlist_title_public} description`; + + Cypress.on('uncaught:exception', (err, runnable) => { + // Prevents Cypress from failing the test due to uncaught exceptions in the application code - TypeError: Cannot read properties of undefined (reading 'scrollDown') + if ( + err.message.includes( + "Cannot read properties of undefined (reading 'success')" + ) + ) { + return false; + } + }); + + //checks navigation when create new playlist is accessed it('.create_playlists()', () => { - cy.login('administrator') - cy.visit('/playlists/new') - }) - - // is able to create private (default) playlist - it('.validate_create_playlist()', () => { - cy.login('administrator') - cy.visit('/playlists/new') - cy.get('#playlist_title').type('Cypress Testing') - cy.get('#playlist_comment').type('Cypress testing comments') - cy.get('#submit-playlist-form').click() - cy.visit('/playlists') - cy.contains('Cypress Testing') - cy.contains('Visibility') - cy.contains('Created') - cy.contains('Updated') - cy.contains('Actions') - cy.contains('Private') - cy.contains('Size') - cy.contains('Delete') - cy.contains('Edit') - cy.contains('Delete') - }) - - // is able to view playlist by clicking on playlist name - it('.view_playlist()', () => { - cy.login('administrator') - cy.visit('/playlists/new') + cy.login('administrator'); + cy.visit('/playlists/new'); + }); + + it('Verify creating a Playlist - @Tf1b9413d', () => { + cy.login('administrator'); + cy.visit('/'); + cy.get('#playlists_nav').click(); + cy.get('a[href="/playlists/new"]').click(); + // cy.visit('/playlists/new') + + cy.get('#playlist_title').type(playlist_title); + cy.get('#playlist_comment').type(playlist_description); + cy.get('#submit-playlist-form').click(); + + //Validate play list creation success message + cy.get('.alert.alert-info') + .should('be.visible') + .within(() => { + cy.get('p').should('contain', 'Playlist was successfully created.'); + }); + + //Validate the newly created playlist page + // Validate the presence of the video.js element + cy.get('video[data-testid="videojs-audio-element"]') + .should('exist') + .and('have.class', 'video-js') + .and('have.class', 'vjs-big-play-centered'); + + // Validate the presence of the text "This playlist currently has no playable items." + cy.get('div[data-testid="inaccessible-message-display"] p') + .should('be.visible') + .and('contain.text', 'This playlist currently has no playable items.'); + + //validate the playlist details - title, description, buttons , etc + cy.get('div.playlist-title').get('h1').contains(playlist_title); + //verify that the playlist created by default is private + cy.get('div.playlist-title') + .find('span[title="This playlist can only be viewed by you."]') + .should('be.visible'); + cy.contains('h4', 'Description') + .should('be.visible') + .next('p') + .should('have.text', playlist_description); + cy.get('button.copy-playlist-button').should('be.visible'); + cy.get('div.ramp--auto-advance').should('be.visible'); + cy.get('#share-button').should('be.visible'); + cy.get('#edit-playlist-button').should('be.visible'); + }); + + it('.validate_playlist_table()', () => { + cy.login('administrator'); + cy.visit('/'); + cy.get('#playlists_nav').click(); + cy.visit('/playlists'); + cy.contains('Name'); + cy.contains('Visibility'); + cy.contains('Created'); + cy.contains('Updated'); + cy.contains('Actions'); + cy.contains('Private'); + cy.contains('Size'); + cy.contains('Delete'); + cy.contains('Edit'); + }); - cy.get('#playlist_title').type('Cypress Testing2') - cy.get('#playlist_comment').type('Cypress testing2 comments') - cy.get('#submit-playlist-form').click() - - cy.visit('/playlists') - cy.contains('Cypress Testing2').click() - cy.contains('Edit Playlist') - cy.contains('Cypress testing2 comments') - cy.contains('This playlist currently has no playable items') - }) // deletes playlist permanently from playlists page - it('.delete_playlist()', () => { - cy.login('administrator') - cy.visit('/playlists/new') + it('Verify Deleting a Playlist - playlist table - @T53c3887a', () => { + cy.login('administrator'); + cy.visit('/'); + var playlist_title_1 = `__Automation playlist title ${Math.floor(Math.random() * 10000) + 1 + }`; + var playlist_description_1 = `${playlist_title} description`; - cy.get('#playlist_title').type('Cypress Testing3') - cy.get('#playlist_comment').type('Cypress testing3 comments') - cy.get('#submit-playlist-form').click() + cy.get('#playlists_nav').click(); + cy.get('a[href="/playlists/new"]').click(); + // cy.visit('/playlists/new') - cy.visit('/playlists') - cy.contains('Delete').click() - cy.contains('Yes, Delete').click() + cy.get('#playlist_title').type(playlist_title_1); + cy.get('#playlist_comment').type(playlist_description_1); + cy.get('#submit-playlist-form').click(); - cy.visit('/playlists') - cy.contains('Cypress Testing3').should('not.exist') - }) + cy.visit('/playlists'); + cy.get('tr') + .contains('td', playlist_title_1) + .parent('tr') + .find('.btn-danger') + .click(); + cy.screenshot() + cy.contains('Yes, Delete').click(); - // is able to delete playlist from edit playlist page - it('.delete_playlist_edit_page()', () => { - cy.login('administrator') - cy.visit('/playlists/new') + cy.visit('/playlists'); - cy.get('#playlist_title').type('Cypress Testing4') - cy.get('#playlist_comment').type('Cypress testing4 comments') - cy.get('#submit-playlist-form').click() + //Handle pagination case - search for the playlist. Add API validation - cy.visit('/playlists') - cy.contains('Cypress Testing4').click() - cy.contains('Edit Playlist').click() + cy.contains(playlist_title).should('not.exist'); + }); - cy.contains('Delete Playlist').click() - cy.contains('Yes, Delete').click() - cy.contains('Playlist was successfully destroyed.') - cy.visit('/playlists') - cy.contains('Cypress Testing4').should('not.exist') - }) + // is able to delete playlist from edit playlist page + it('Verify Deleting a Playlist - playlist page - @T49ac05b8', () => { + cy.login('user'); + cy.visit('/'); + var playlist_title = `__Automation playlist title ${Math.floor(Math.random() * 10000) + 1 + }`; + var playlist_description = `${playlist_title} description`; + + cy.get('#playlists_nav').click(); + cy.get('a[href="/playlists/new"]').click(); + // cy.visit('/playlists/new') + + cy.get('#playlist_title').type(playlist_title); + cy.get('#playlist_comment').type(playlist_description); + cy.get('#submit-playlist-form').click(); + + cy.visit('/playlists'); + cy.contains(playlist_title).click(); + cy.contains('Edit Playlist').click(); + + cy.contains('Delete Playlist').click(); + cy.contains('Yes, Delete').click(); + cy.contains('Playlist was successfully destroyed.'); + cy.visit('/playlists'); + cy.contains(playlist_title).should('not.exist'); + }); // is able to create public playlist it('.create_public_playlist()', () => { - cy.login('administrator') - cy.visit('/playlists/new') - - cy.get('#playlist_title').type('Cypress Testing5') - cy.get('#playlist_comment').type('Cypress testing5 comments') - cy.contains('Public').click() - cy.get('#submit-playlist-form').click() + cy.login('administrator'); + + cy.get('#playlists_nav').click(); + cy.get('a[href="/playlists/new"]').click(); + // cy.visit('/playlists/new') + + cy.get('#playlist_title').type(playlist_title_public); + cy.get('#playlist_comment').type(playlist_description_public); + cy.contains('Public').click(); + cy.get('#submit-playlist-form').click(); + + cy.visit('/playlists'); + cy.contains(playlist_title_public); + cy.contains(playlist_description_public); + cy.contains('Public'); + cy.get('div.playlist-title') + .find('span[title="This playlist can be viewed by anyone on the web."]') + .should('be.visible'); + }); + + //is able to share a public playlist + it.only('Verify sharing a public playlist - @c89c89d0', () => { + cy.login('administrator'); + cy.visit('/'); + cy.get('#playlists_nav').click(); + playlist_title_public = 'Android test 3' + cy.contains(playlist_title_public).click(); + cy.get('#share-button').click() + cy.get('#share-list').within(()=>{ + cy.url().then((currentUrl) => { + cy.get('#link-object') + .should('have.value', currentUrl); + }); + }) + }); - cy.visit('/playlists') - cy.contains('Cypress Testing5') - cy.contains('Public') + // is able to change public playlist to private + it('Verify editing a playlist from playlist table (Access control) - @T7fa4cea5', () => { + cy.login('administrator') + cy.visit('/') + // cy.visit('/playlists/new') + cy.get('#playlists_nav').click() + cy.get('tr') + .contains('td', playlist_title_public) + .parent('tr') + .contains('Edit') + .click(); + cy.screenshot() + cy.get('#playlist_edit_button').click() + cy.contains('Private').click() + cy.contains('Save Changes').click() + cy.contains('Playlist was successfully updated') + cy.contains('Private') }) // is able to edit playlist name and description - it('.edit_playlist_name()', () => { + it('Verify editing a Playlist from playlist page - @T5055855c', () => { cy.login('administrator') cy.visit('/playlists') + playlist_title_public = '_Automation playlist title 5124' - cy.contains('Cypress Testing5').click() + cy.contains(playlist_title_public).click() cy.contains('Edit Playlist').click() cy.get('#playlist_edit_button').click() - cy.get('#playlist_title').type('changed') - cy.get('#playlist_comment').type('changed') + + var updated_title = "_Edited" + playlist_title_public + var updatedDescription = "_Edited" + playlist_description_public + + cy.get('#playlist_title').clear().type(updated_title) + cy.get('#playlist_comment').clear().type(updatedDescription) cy.contains('Save Changes').click() cy.contains('Playlist was successfully updated') + cy.get('#playlist_view_div').within(() => { + cy.contains('dd',updated_title) + cy.contains('dd', updatedDescription) + }).then(() => { + // If assertions pass, update the playlist_title_public + playlist_title_public = updated_title; + playlist_description_public = updatedDescription + cy.log(`Playlist title updated to: ${playlist_title_public}`); // Log the updated value + }); }) - // is able to change public playlist to private - it('.edit_access_control()', () => { - cy.login('administrator') - cy.visit('/playlists/new') - - cy.get('#playlist_title').type('Cypress Testing5') - cy.get('#playlist_comment').type('Cypress testing5 comments') - cy.contains('Public').click() - cy.get('#submit-playlist-form').click() - cy.visit('/playlists') - cy.contains('Cypress Testing5').click() - cy.contains('Edit Playlist').click() + //Add teardown code here to delete playlist_title and playlist_title_public - cy.get('#playlist_edit_button').click() - cy.contains('Private').click() - cy.contains('Save Changes').click() - cy.contains('Playlist was successfully updated') - cy.contains('Private') - }) -}) + +}); From d72c1d9c47f81bf2d4c341a60c17b0330ae807af Mon Sep 17 00:00:00 2001 From: charumitraravi <52535309+charumitraravi@users.noreply.github.com> Date: Tue, 11 Jun 2024 09:23:50 -0400 Subject: [PATCH 072/152] deleted cypress_dev.config, added separate .env.json files Removed multiple cypress configs, added separate env.json files for multiple test environments, with placeholders for sensitive infotmation --- spec/cypress/cypress.env.dev.json | 14 +++ spec/cypress/cypress.env.local.json | 14 +++ spec/cypress/cypress.env.staging.json | 10 +++ spec/cypress/cypress_dev.config.js | 55 ------------ spec/cypress/integration/collections_spec.js | 90 ++++++++++---------- spec/cypress/integration/playlist_spec.js | 13 +-- 6 files changed, 86 insertions(+), 110 deletions(-) create mode 100644 spec/cypress/cypress.env.dev.json create mode 100644 spec/cypress/cypress.env.local.json create mode 100644 spec/cypress/cypress.env.staging.json delete mode 100644 spec/cypress/cypress_dev.config.js diff --git a/spec/cypress/cypress.env.dev.json b/spec/cypress/cypress.env.dev.json new file mode 100644 index 0000000000..2de97baf9f --- /dev/null +++ b/spec/cypress/cypress.env.dev.json @@ -0,0 +1,14 @@ + + { + "baseUrl": "https://avalon-dev.dlib.indiana.edu/", + "env": { + "MEDIA_OBJECT_ID": "fj236208t", + "MEDIA_OBJECT_TITLE":"Beginning Responsibility: Lunchroom Manners", + "SEARCH_COLLECTION":"7.7 regression test", + "USERS_ADMINISTRATOR_EMAIL": "admin_email", + "USERS_ADMINISTRATOR_PASSWORD": "placeholder_password", + "USERS_USER_EMAIL":"user_email", + "USERS_USER_PASSWORD": "placeholder_password", + "USER_MANAGER": "userstudy" + } + } \ No newline at end of file diff --git a/spec/cypress/cypress.env.local.json b/spec/cypress/cypress.env.local.json new file mode 100644 index 0000000000..28f55bf348 --- /dev/null +++ b/spec/cypress/cypress.env.local.json @@ -0,0 +1,14 @@ + + { + "baseUrl": "http://localhost:3000", + "env": { + "MEDIA_OBJECT_ID": "fj236208t", + "MEDIA_OBJECT_TITLE":"Beginning Responsibility: Lunchroom Manners", + "SEARCH_COLLECTION":"7.7 regression test", + "USERS_ADMINISTRATOR_EMAIL": "archivist1@example.com", + "USERS_ADMINISTRATOR_PASSWORD": "!RH@!,YWYw$n64_", + "USERS_USER_EMAIL":"chravi@iu.edu", + "USERS_USER_PASSWORD": "AnotherPassword1234!", + "USER_MANAGER": "userstudy" + } + } \ No newline at end of file diff --git a/spec/cypress/cypress.env.staging.json b/spec/cypress/cypress.env.staging.json new file mode 100644 index 0000000000..b44745ac1d --- /dev/null +++ b/spec/cypress/cypress.env.staging.json @@ -0,0 +1,10 @@ +{ + "baseUrl": "https://avalon-staging.dlib.indiana.edu/", + "env": { + "MEDIA_OBJECT_TITLE":"Beginning Responsibility: Lunchroom Manners", + "USERS_ADMINISTRATOR_EMAIL": "admin_email", + "USERS_ADMINISTRATOR_PASSWORD": "placeholder_password", + "USERS_USER_EMAIL":"user_email", + "USERS_USER_PASSWORD": "placeholder_password" + } +} \ No newline at end of file diff --git a/spec/cypress/cypress_dev.config.js b/spec/cypress/cypress_dev.config.js deleted file mode 100644 index 7a81ccab03..0000000000 --- a/spec/cypress/cypress_dev.config.js +++ /dev/null @@ -1,55 +0,0 @@ -const { defineConfig } = require("cypress"); -const path = require('path'); -const fs = require('fs'); - -module.exports = defineConfig({ - - downloadsFolder: "spec/cypress/downloads", - fixturesFolder: "spec/cypress/fixtures", - screenshotsFolder: "spec/cypress/screenshots", - videosFolder: "spec/cypress/videos", - browser: process.env.BROWSER || 'electron', // - e2e: { - setupNodeEvents(on, config) { - // implement node event listeners here - const environmentName = process.env.CYPRESS_ENV || 'local'; - const environmentFilename = `cypress.env.${environmentName}.json`; - const environmentPath = path.resolve(__dirname, environmentFilename); - - console.log('Environment name: %s', environmentName); - console.log('Environment path: %s', environmentPath); - - if (fs.existsSync(environmentPath)) { - console.log('Loading %s', environmentFilename); - const settings = require(environmentPath); - - // Set baseUrl if defined in the environment settings - if (settings.baseUrl) { - config.baseUrl = settings.baseUrl; - console.log('Loading the baseURL.... %s', config.baseUrl); - } - - // Merge environment variables - if (settings.env) { - config.env = { - ...config.env, - ...settings.env, - }; - } - - console.log('Loaded settings for environment %s', environmentName); - } else { - console.error(`Environment config file ${environmentFilename} not found`); - } - - return config; - - }, - - // baseUrl: "https://avalon-dev.dlib.indiana.edu/", - supportFile: "spec/cypress/support/e2e.js", - specPattern: "spec/cypress/integration/**/*.js" - }, - -}); - \ No newline at end of file diff --git a/spec/cypress/integration/collections_spec.js b/spec/cypress/integration/collections_spec.js index 6f9ec88d65..bc3820e8f1 100644 --- a/spec/cypress/integration/collections_spec.js +++ b/spec/cypress/integration/collections_spec.js @@ -16,8 +16,8 @@ context('Collections', () => { //Since it takes a while for a newly created collection to reflect in search, we are using static search data - const search_collection = Cypress.env('SEARCH_COLLECTION') - const collection_title = `Automation collection title ${Math.floor(Math.random() * 10000) + 1}` + var search_collection = Cypress.env('SEARCH_COLLECTION') + var collection_title = `Automation collection title ${Math.floor(Math.random() * 10000) + 1}` Cypress.on('uncaught:exception', (err, runnable) => { // Prevents Cypress from failing the test due to uncaught exceptions in the application code - TypeError: Cannot read properties of undefined (reading 'scrollDown') @@ -69,48 +69,7 @@ context('Collections', () => { }) -it('Verify whether a user is able to update Collection information - @Ta1b2fef8', () => { - cy.login('administrator') - cy.visit('/') - cy.get('#manageDropdown').click() - cy.contains('Manage Content').click() - cy.contains('a', collection_title).click(); - cy.get('.admin-collection-details') - .contains('button', 'Edit Collection Info') - .click(); - - //update description - var updatedDescription = ' Adding more details to collection description' - cy.get('#admin_collection_description').invoke('val').then((existingText) => { - updatedDescription = existingText + updatedDescription; - cy.get('#admin_collection_description').type(updatedDescription); - }); - //update title - var new_title = `Updated automation title ${Math.floor(Math.random() * 10000) + 1}` - cy.get('#admin_collection_name').clear().type(new_title); - - //update contact email - cy.get('#admin_collection_contact_email').clear().type('test@yopmail.com'); - - cy.get('input[value="Update Collection"]').click(); - - // Validate updated collection title and update the collection_title global variable - cy.get('.admin-collection-details h2').should('contain.text', new_title).then(() => { - // Update the global variable collection_title with new_title if the assertion passes - collection_title = new_title; - }); - - // Validate updated contact email - cy.get('.admin-collection-details').within(() => { - cy.get('a[href="mailto:test@yopmail.com"]') - .should('have.text', 'test@yopmail.com') - }); - //validate updated description - cy.get('.admin-collection-details .collection-description').should('contain.text', updatedDescription); - -}) - -it('Verify whether aan admin/manager is able assign other users as managers to the collection - @T3c428871', () => { +it('Verify whether an admin/manager is able assign other users as managers to the collection - @T3c428871', () => { cy.login('administrator') cy.visit('/') cy.get('#manageDropdown').click() @@ -178,6 +137,47 @@ it('Verify changing item access - Collection staff only (Existing items) - @Tdcf }) +it('Verify whether a user is able to update Collection information - @Ta1b2fef8', () => { + cy.login('administrator') + cy.visit('/') + cy.get('#manageDropdown').click() + cy.contains('Manage Content').click() + cy.contains('a', collection_title).click(); + cy.get('.admin-collection-details') + .contains('button', 'Edit Collection Info') + .click(); + + //update description + var updatedDescription = ' Adding more details to collection description' + cy.get('#admin_collection_description').invoke('val').then((existingText) => { + updatedDescription = existingText + updatedDescription; + cy.get('#admin_collection_description').type(updatedDescription); + }); + //update title + var new_title = `Updated automation title ${Math.floor(Math.random() * 10000) + 1}` + cy.get('#admin_collection_name').clear().type(new_title); + + //update contact email + cy.get('#admin_collection_contact_email').clear().type('test@yopmail.com'); + + cy.get('input[value="Update Collection"]').click(); + + // Validate updated collection title and update the collection_title global variable + cy.get('.admin-collection-details h2').should('contain.text', new_title).then(() => { + // Update the global variable collection_title with new_title if the assertion passes + collection_title = new_title; + }); + + // Validate updated contact email + cy.get('.admin-collection-details').within(() => { + cy.get('a[href="mailto:test@yopmail.com"]') + .should('have.text', 'test@yopmail.com') + }); + //validate updated description + cy.get('.admin-collection-details .collection-description').should('contain.text', updatedDescription); + +}) + //Teardown code : delete the created collection it('Verify deleting a collection - @T959a56df', () => { @@ -189,7 +189,7 @@ it('Verify changing item access - Collection staff only (Existing items) - @Tdcf //May require adding steps to select a collection to move the existing items, when dealing with non empty collections cy.get('input[value="Yes, I am sure"]').click() cy.contains('h1', 'My Collections') - //May need to update this assertion to ensire that this is valid during pagination of collections. Another alternative would be to check via API or search My collections + //May need to update this assertion to ensure that this is valid during pagination of collections. Another alternative would be to check via API or search My collections cy.contains('a', collection_title).should('not.exist'); }) diff --git a/spec/cypress/integration/playlist_spec.js b/spec/cypress/integration/playlist_spec.js index a3bc80d2ec..074237a14d 100644 --- a/spec/cypress/integration/playlist_spec.js +++ b/spec/cypress/integration/playlist_spec.js @@ -40,12 +40,12 @@ context('Playlists', () => { cy.visit('/playlists/new'); }); + //is able to create a new playlist it('Verify creating a Playlist - @Tf1b9413d', () => { cy.login('administrator'); cy.visit('/'); cy.get('#playlists_nav').click(); cy.get('a[href="/playlists/new"]').click(); - // cy.visit('/playlists/new') cy.get('#playlist_title').type(playlist_title); cy.get('#playlist_comment').type(playlist_description); @@ -113,8 +113,6 @@ context('Playlists', () => { cy.get('#playlists_nav').click(); cy.get('a[href="/playlists/new"]').click(); - // cy.visit('/playlists/new') - cy.get('#playlist_title').type(playlist_title_1); cy.get('#playlist_comment').type(playlist_description_1); cy.get('#submit-playlist-form').click(); @@ -125,13 +123,11 @@ context('Playlists', () => { .parent('tr') .find('.btn-danger') .click(); - cy.screenshot() cy.contains('Yes, Delete').click(); cy.visit('/playlists'); //Handle pagination case - search for the playlist. Add API validation - cy.contains(playlist_title).should('not.exist'); }); @@ -145,8 +141,6 @@ context('Playlists', () => { cy.get('#playlists_nav').click(); cy.get('a[href="/playlists/new"]').click(); - // cy.visit('/playlists/new') - cy.get('#playlist_title').type(playlist_title); cy.get('#playlist_comment').type(playlist_description); cy.get('#submit-playlist-form').click(); @@ -165,6 +159,7 @@ context('Playlists', () => { // is able to create public playlist it('.create_public_playlist()', () => { cy.login('administrator'); + cy.visit('/'); cy.get('#playlists_nav').click(); cy.get('a[href="/playlists/new"]').click(); @@ -185,11 +180,10 @@ context('Playlists', () => { }); //is able to share a public playlist - it.only('Verify sharing a public playlist - @c89c89d0', () => { + it('Verify sharing a public playlist - @c89c89d0', () => { cy.login('administrator'); cy.visit('/'); cy.get('#playlists_nav').click(); - playlist_title_public = 'Android test 3' cy.contains(playlist_title_public).click(); cy.get('#share-button').click() cy.get('#share-list').within(()=>{ @@ -223,7 +217,6 @@ context('Playlists', () => { it('Verify editing a Playlist from playlist page - @T5055855c', () => { cy.login('administrator') cy.visit('/playlists') - playlist_title_public = '_Automation playlist title 5124' cy.contains(playlist_title_public).click() cy.contains('Edit Playlist').click() From 0a40853a94d42b9c270293fde2d321e924e54e70 Mon Sep 17 00:00:00 2001 From: charumitraravi <52535309+charumitraravi@users.noreply.github.com> Date: Tue, 11 Jun 2024 09:25:15 -0400 Subject: [PATCH 073/152] docker compose - added environment variable handling --- docker-compose.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index 6ad80861c3..ee77c7a422 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -211,3 +211,5 @@ services: volumes: - ./:/e2e - npms:/e2e/node_modules + environment: + - CYPRESS_ENV=${CYPRESS_ENV:-local} From 483c4b74eaf44a6ef4baf2eeb1e9a92b91a2720d Mon Sep 17 00:00:00 2001 From: charumitraravi <52535309+charumitraravi@users.noreply.github.com> Date: Tue, 11 Jun 2024 09:58:21 -0400 Subject: [PATCH 074/152] Update cypress.config.js --- spec/cypress/cypress.config.js | 46 ++++++++++++++++++++++++++++------ 1 file changed, 38 insertions(+), 8 deletions(-) diff --git a/spec/cypress/cypress.config.js b/spec/cypress/cypress.config.js index 479d710455..0a19103ff7 100644 --- a/spec/cypress/cypress.config.js +++ b/spec/cypress/cypress.config.js @@ -1,23 +1,53 @@ const { defineConfig } = require("cypress"); +const path = require('path'); +const fs = require('fs'); module.exports = defineConfig({ - env: { - "USERS_ADMINISTRATOR_EMAIL": "administrator@example.com", - "USERS_ADMINISTRATOR_PASSWORD": "password", - "USERS_USER_EMAIL": "user@example.com", - "USERS_USER_PASSWORD": "password", - "MEDIA_OBJECT_ID": "123456789" - }, + downloadsFolder: "spec/cypress/downloads", fixturesFolder: "spec/cypress/fixtures", screenshotsFolder: "spec/cypress/screenshots", videosFolder: "spec/cypress/videos", + browser: process.env.BROWSER || 'electron', // e2e: { setupNodeEvents(on, config) { + // implement node event listeners here + const environmentName = process.env.CYPRESS_ENV || 'local'; + const environmentFilename = `cypress.env.${environmentName}.json`; + const environmentPath = path.resolve(__dirname, environmentFilename); + + console.log('Environment name: %s', environmentName); + console.log('Environment path: %s', environmentPath); + + if (fs.existsSync(environmentPath)) { + console.log('Loading %s', environmentFilename); + const settings = require(environmentPath); + + // Set baseUrl if defined in the environment settings + if (settings.baseUrl) { + config.baseUrl = settings.baseUrl; + console.log('Loading the baseURL.... %s', config.baseUrl); + } + + // Merge environment variables + if (settings.env) { + config.env = { + ...config.env, + ...settings.env, + }; + } + + console.log('Loaded settings for environment %s', environmentName); + } else { + console.error(`Environment config file ${environmentFilename} not found`); + } + return config; }, - baseUrl: "http://localhost:3000", + supportFile: "spec/cypress/support/e2e.js", specPattern: "spec/cypress/integration/**/*.js" }, + }); + \ No newline at end of file From f6f095dc45e271c84b2655649546ed61d831fb7c Mon Sep 17 00:00:00 2001 From: cjcolvar Date: Tue, 11 Jun 2024 10:00:46 -0400 Subject: [PATCH 075/152] Don't titlize for matching since resource type isn't titlized in the index anymore --- app/helpers/application_helper.rb | 5 ++--- spec/helpers/application_helper_spec.rb | 8 ++++---- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 2c9debd60b..ad555e556c 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -58,12 +58,11 @@ def lti_share_url_for(obj, _opts = {}) user_omniauth_callback_lti_url(target_id: target) end - # TODO: Fix me with latest changes from 5.1.4 def image_for(document) master_file_id = document["section_id_ssim"].try :first - video_count = document["avalon_resource_type_ssim"].count{|m| m.start_with?('moving image'.titleize) } rescue 0 - audio_count = document["avalon_resource_type_ssim"].count{|m| m.start_with?('sound recording'.titleize) } rescue 0 + video_count = document["avalon_resource_type_ssim"].count{|m| m.start_with?('moving image') } rescue 0 + audio_count = document["avalon_resource_type_ssim"].count{|m| m.start_with?('sound recording') } rescue 0 if master_file_id if video_count > 0 diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index 2de1ba4286..db9832ad15 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -137,15 +137,15 @@ expect(helper.image_for(doc)).to eq(nil) end it "should return audio icon" do - doc = {"avalon_resource_type_ssim" => ['Sound Recording', 'Sound Recording'] } + doc = {"avalon_resource_type_ssim" => ['sound recording', 'sound recording'] } expect(helper.image_for(doc).start_with?("#{root_url}assets/audio_icon")).to be_truthy end it "should return video icon" do - doc = {"avalon_resource_type_ssim" => ['Moving Image'] } + doc = {"avalon_resource_type_ssim" => ['moving image'] } expect(helper.image_for(doc).start_with?("#{root_url}assets/video_icon")).to be_truthy end it "should return hybrid icon" do - doc = {"avalon_resource_type_ssim" => ['Moving Image', 'Sound Recording'] } + doc = {"avalon_resource_type_ssim" => ['moving image', 'sound recording'] } expect(helper.image_for(doc).start_with?("#{root_url}assets/hybrid_icon")).to be_truthy end it "should return nil when only unprocessed video" do @@ -153,7 +153,7 @@ expect(helper.image_for(doc)).to eq(nil) end it "should return thumbnail" do - doc = {"section_id_ssim" => ['1'], "avalon_resource_type_ssim" => ['Moving Image'] } + doc = {"section_id_ssim" => ['1'], "avalon_resource_type_ssim" => ['moving image'] } expect(helper.image_for(doc)).to eq('/master_files/1/thumbnail') end end From 015e3bca93f636ba0a9e3aa6f02e218e46258789 Mon Sep 17 00:00:00 2001 From: charumitraravi <52535309+charumitraravi@users.noreply.github.com> Date: Tue, 11 Jun 2024 10:22:59 -0400 Subject: [PATCH 076/152] Update cypress.env.local.json --- spec/cypress/cypress.env.local.json | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/spec/cypress/cypress.env.local.json b/spec/cypress/cypress.env.local.json index 28f55bf348..9b5140197c 100644 --- a/spec/cypress/cypress.env.local.json +++ b/spec/cypress/cypress.env.local.json @@ -2,13 +2,6 @@ { "baseUrl": "http://localhost:3000", "env": { - "MEDIA_OBJECT_ID": "fj236208t", - "MEDIA_OBJECT_TITLE":"Beginning Responsibility: Lunchroom Manners", - "SEARCH_COLLECTION":"7.7 regression test", - "USERS_ADMINISTRATOR_EMAIL": "archivist1@example.com", - "USERS_ADMINISTRATOR_PASSWORD": "!RH@!,YWYw$n64_", - "USERS_USER_EMAIL":"chravi@iu.edu", - "USERS_USER_PASSWORD": "AnotherPassword1234!", - "USER_MANAGER": "userstudy" + } } \ No newline at end of file From bb38720a3bded926ffc7d29fdfa49e6da3e68b98 Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Tue, 11 Jun 2024 15:31:48 -0400 Subject: [PATCH 077/152] Fixes from review --- .../supplemental_files_controller.rb | 47 ++++++++----------- app/models/supplemental_file.rb | 20 +++----- config/routes.rb | 7 ++- .../supplemental_files_controller_examples.rb | 32 +++++++------ 4 files changed, 50 insertions(+), 56 deletions(-) diff --git a/app/controllers/supplemental_files_controller.rb b/app/controllers/supplemental_files_controller.rb index 0fb3770732..c9b8f3c8ea 100644 --- a/app/controllers/supplemental_files_controller.rb +++ b/app/controllers/supplemental_files_controller.rb @@ -16,7 +16,6 @@ class SupplementalFilesController < ApplicationController include Rails::Pagination - before_action :authenticate_user!, except: [:show, :captions] before_action :set_object before_action :authorize_object @@ -55,32 +54,27 @@ def create # Raise errror if file wasn't attached raise Avalon::SaveError, "File could not be attached." unless @supplemental_file.file.attached? - - raise Avalon::SaveError, @supplemental_file.errors.full_messages unless @supplemental_file.save - else - # For multi-step API upload, we need to skip file type validation on the metadata portion of the save. - # Captions are validated as VTT or SRT which cannot be validated if there is no file attached. - @supplemental_file.skip_file_type = true - # Transcripts are automatically indexed on creation, so we need to skip indexing to avoid an error - # when saving without an uploaded file. - @supplemental_file.skip_index = true - raise Avalon::SaveError, @supplemental_file.errors.full_messages unless @supplemental_file.save end + raise Avalon::SaveError, @supplemental_file.errors.full_messages unless @supplemental_file.save + @object.supplemental_files += [@supplemental_file] raise Avalon::SaveError, @object.errors[:supplemental_files_json].full_messages unless @object.save flash[:success] = "Supplemental file successfully added." respond_to do |format| - format.html { - if request.headers['Avalon-Api-Key'].present? - render json: { supplemental_file: @supplemental_file.id }, status: :created + format.html { + # This path is for uploading the binary file. We need to provide a JSON response + # for the case of someone uploading through a CLI. + if request.headers['Accept'] == 'application/json' + render json: { id: @supplemental_file.id }, status: :created else redirect_to edit_structure_path end } - format.json { render json: { supplemental_file: @supplemental_file.id }, status: :created } + # This path is for uploading the metadata payload. + format.json { render json: { id: @supplemental_file.id }, status: :created } end end @@ -113,20 +107,23 @@ def update edit_file_information if !attachment - update_attached_file if attachment + @supplemental_file.attach_file(attachment) if attachment raise Avalon::SaveError, @supplemental_file.errors.full_messages unless @supplemental_file.save flash[:success] = "Supplemental file successfully updated." respond_to do |format| - format.html { - if request.headers['Avalon-Api-Key'].present? - render json: { supplemental_file: @supplemental_file.id } + format.html { + # This path is for uploading the binary file. We need to provide a JSON response + # for the case of someone uploading through a CLI. + if request.headers['Accept'] == 'application/json' + render json: { id: @supplemental_file.id } else redirect_to edit_structure_path end } - format.json { render json: { supplemental_file: @supplemental_file.id }, status: :ok } + # This path is for uploading the metadata payload. + format.json { render json: { id: @supplemental_file.id }, status: :ok } end end @@ -177,10 +174,10 @@ def supplemental_file_params when 'transcript' 'transcript' else - nil + nil end - treat_as_transcript = 'transcript' if meta_params[:treat_as_transcript] == 1 - machine_generated = 'machine_generated' if meta_params[:machine_generated] == 1 + treat_as_transcript = 'transcript' if meta_params[:treat_as_transcript] == true + machine_generated = 'machine_generated' if meta_params[:machine_generated] == true sup_file_params[:label] ||= meta_params[:label].presence sup_file_params[:language] ||= meta_params[:language].presence @@ -269,10 +266,6 @@ def attachment params[:file] || supplemental_file_params[:file] end - def update_attached_file - @supplemental_file.attach_file(attachment) - end - def object_supplemental_file_path if @object.is_a? MasterFile master_file_supplemental_file_path(id: @supplemental_file.id, master_file_id: @object.id) diff --git a/app/models/supplemental_file.rb b/app/models/supplemental_file.rb index 2124c8ba59..4d091b8b23 100644 --- a/app/models/supplemental_file.rb +++ b/app/models/supplemental_file.rb @@ -19,20 +19,17 @@ class SupplementalFile < ApplicationRecord scope :with_tag, ->(tag_filter) { where("tags LIKE ?", "%\n- #{tag_filter}\n%") } - attr_accessor :skip_file_type - attr_accessor :skip_index - # TODO: the empty tag should represent a generic supplemental file validates :tags, array_inclusion: ['transcript', 'caption', 'machine_generated', '', nil] validates :language, inclusion: { in: LanguageTerm.map.keys } validates :parent_id, presence: true - validate :validate_file_type, if: :validate_caption? + validate :validate_file_type, if: :caption? serialize :tags, Array # Need to prepend so this runs before the callback added by `has_one_attached` above # See https://github.com/rails/rails/issues/37304 - after_create_commit :index_file, prepend: true, unless: :skip_index + after_create_commit :index_file, prepend: true after_update_commit :update_index, prepend: true def attach_file(new_file) @@ -77,8 +74,8 @@ def as_json(options={}) type: type, label: label, language: LanguageTerm.find(language).text, - treat_as_transcript: caption_transcript? ? '1' : nil, - machine_generated: machine_generated? ? '1' : nil + treat_as_transcript: caption_transcript? ? true : false, + machine_generated: machine_generated? ? true : false }.compact end @@ -102,7 +99,7 @@ def self.convert_from_srt(srt) # However, they cannot call the same method name or only the last defined callback will take effect. # https://guides.rubyonrails.org/active_record_callbacks.html#aliases-for-after-commit def update_index - ActiveFedora::SolrService.add(to_solr, softCommit: true) + ActiveFedora::SolrService.add(to_solr, softCommit: true) if file.present? end alias index_file update_index @@ -134,11 +131,8 @@ def segment_transcript transcript private def validate_file_type - errors.add(:file_type, "Uploaded file is not a recognized captions file") unless ['text/vtt', 'text/srt'].include? file.content_type - end - - def validate_caption? - caption? && !skip_file_type + return unless file.present? + errors.add(:file_type, "Uploaded file is not a recognized captions file") unless ['text/vtt', 'text/srt'].include?(file.content_type) end def c_time diff --git a/config/routes.rb b/config/routes.rb index 463beacd3b..d9d6e7c836 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -139,7 +139,9 @@ end # Supplemental Files - resources :supplemental_files, except: [:new, :edit] + resources :supplemental_files, except: [:new, :index, :edit] do + get :index, constraints: { format: 'json' }, on: :collection + end end resources :master_files, except: [:new, :index] do @@ -168,11 +170,12 @@ end # Supplemental Files - resources :supplemental_files, except: [:new, :edit] do + resources :supplemental_files, except: [:new, :index, :edit] do member do get 'captions' get 'transcripts', :to => redirect('/master_files/%{master_file_id}/supplemental_files/%{id}') end + get :index, constraints: { format: 'json' }, on: :collection end end diff --git a/spec/support/supplemental_files_controller_examples.rb b/spec/support/supplemental_files_controller_examples.rb index 365ff25bd5..eadf174aa5 100644 --- a/spec/support/supplemental_files_controller_examples.rb +++ b/spec/support/supplemental_files_controller_examples.rb @@ -82,7 +82,7 @@ describe 'normal auth' do context 'with unauthenticated user' do it "all routes should return 401" do - expect(get :index, params: { class_id => object.id }).to have_http_status(401) + expect(get :index, params: { class_id => object.id, format: 'json' }).to have_http_status(401) expect(get :show, params: { class_id => object.id, id: supplemental_file.id }).to have_http_status(401) expect(post :create, params: { class_id => object.id }).to have_http_status(401) expect(put :update, params: { class_id => object.id, id: supplemental_file.id }).to have_http_status(401) @@ -94,7 +94,7 @@ login_as :user end it "all routes should return 401" do - expect(get :index, params: { class_id => object.id }).to have_http_status(401) + expect(get :index, params: { class_id => object.id, format: 'json' }).to have_http_status(401) expect(get :show, params: { class_id => object.id, id: supplemental_file.id }).to have_http_status(401) expect(post :create, params: { class_id => object.id }).to have_http_status(401) expect(put :update, params: { class_id => object.id, id: supplemental_file.id }).to have_http_status(401) @@ -120,7 +120,7 @@ end it "returns metadata for all associated supplemental files" do - get :index, params: { class_id => object.id }, session: valid_session + get :index, params: { class_id => object.id, format: 'json' }, session: valid_session expect(subject.count).to eq 3 [supplemental_file, caption, transcript].each do |file| expect(subject.any? { |s| s == JSON.parse(file.as_json.to_json) }).to eq true @@ -128,7 +128,7 @@ end it "paginates results when requested" do - get :index, params: { class_id => object.id, per_page: 1, page: 1 }, session: valid_session + get :index, params: { class_id => object.id, format: 'json', per_page: 1, page: 1 }, session: valid_session expect(subject.count).to eq 1 expect(subject.first.symbolize_keys).to eq supplemental_file.as_json end @@ -163,7 +163,7 @@ end context 'json request' do - let(:metadata) { { label: 'label', type: 'caption', language: 'French', machine_generated: 1, treat_as_transcript: 1 } } + let(:metadata) { { label: 'label', type: 'caption', language: 'French', machine_generated: true, treat_as_transcript: true } } context "with valid params" do let(:uploaded_file) { fixture_file_upload(Rails.root.join('spec', 'fixtures', 'captions.vtt'), 'text/vtt') } let(:valid_create_attributes) { { file: uploaded_file, metadata: metadata.to_json } } @@ -172,7 +172,7 @@ post :create, params: { class_id => object.id, **valid_create_attributes, format: :json }, session: valid_session }.to change { object.reload.supplemental_files.size }.by(1) expect(response).to have_http_status(:created) - expect(JSON.parse(response.body)).to eq ({ "supplemental_file" => assigns(:supplemental_file).id }) + expect(JSON.parse(response.body)).to eq ({ "id" => assigns(:supplemental_file).id }) expect(object.supplemental_files.first.id).to eq 1 expect(object.supplemental_files.first.label).to eq 'label' @@ -186,7 +186,7 @@ post :create, params: { class_id => object.id, metadata: metadata.to_json, format: :json }, session: valid_session }.to change { object.reload.supplemental_files.size }.by(1) expect(response).to have_http_status(:created) - expect(JSON.parse(response.body)).to eq ({ "supplemental_file" => assigns(:supplemental_file).id }) + expect(JSON.parse(response.body)).to eq ({ "id" => assigns(:supplemental_file).id }) expect(object.supplemental_files.first.id).to eq 1 expect(object.supplemental_files.first.label).to eq 'label' @@ -202,7 +202,7 @@ post :create, params: { class_id => object.id, **metadata, format: :json }, session: valid_session }.to change { object.reload.supplemental_files.size }.by(1) expect(response).to have_http_status(:created) - expect(JSON.parse(response.body)).to eq ({ "supplemental_file" => assigns(:supplemental_file).id }) + expect(JSON.parse(response.body)).to eq ({ "id" => assigns(:supplemental_file).id }) expect(object.supplemental_files.first.id).to eq 1 expect(object.supplemental_files.first.label).to eq 'label' @@ -221,7 +221,7 @@ post :create, params: { class_id => object.id, metadata: metadata.to_json, format: :json }, session: valid_session }.to change { object.reload.supplemental_files.size }.by(1) expect(response).to have_http_status(:created) - expect(JSON.parse(response.body)).to eq ({ "supplemental_file" => assigns(:supplemental_file).id }) + expect(JSON.parse(response.body)).to eq ({ "id" => assigns(:supplemental_file).id }) expect(object.supplemental_files.first.id).to eq 1 expect(object.supplemental_files.first.label).to eq 'label' @@ -306,11 +306,12 @@ before { ApiToken.create token: 'secret_token', username: 'archivist1@example.com', email: 'archivist1@example.com' } it "creates a SupplementalFile for #{object_class}" do request.headers['Avalon-Api-Key'] = 'secret_token' + request.headers['Accept'] = 'application/json' expect { post :create, params: { class_id => object.id, file: uploaded_file, format: :html }, session: valid_session }.to change { object.reload.supplemental_files.size }.by(1) expect(response).to have_http_status(:created) - expect(JSON.parse(response.body)).to eq ({ "supplemental_file" => assigns(:supplemental_file).id }) + expect(JSON.parse(response.body)).to eq ({ "id" => assigns(:supplemental_file).id }) expect(object.supplemental_files.first.id).to eq 1 expect(object.supplemental_files.first.label).to eq 'captions.srt' @@ -372,7 +373,7 @@ request.headers['Content-Type'] = 'application/json' end context "with valid metadata params" do - let(:valid_update_attributes) { { label: 'new label', type: 'transcript', machine_generated: 1 }.to_json } + let(:valid_update_attributes) { { label: 'new label', type: 'transcript', machine_generated: true }.to_json } it "updates the SupplementalFile metadata for #{object_class}" do expect { put :update, params: { class_id => object.id, id: supplemental_file.id, metadata: valid_update_attributes, format: :json }, session: valid_session @@ -380,7 +381,7 @@ .and change { object.reload.supplemental_files.first.tags }.from([]).to(['transcript', 'machine_generated']) expect(response).to have_http_status(:ok) - expect(response.body).to eq({ "supplemental_file": supplemental_file.id }.to_json) + expect(response.body).to eq({ "id": supplemental_file.id }.to_json) end end @@ -480,12 +481,13 @@ before { ApiToken.create token: 'secret_token', username: 'archivist1@example.com', email: 'archivist1@example.com' } it "updates the SupplementalFile attached file for #{object_class}" do request.headers['Avalon-Api-Key'] = 'secret_token' + request.headers['Accept'] = 'application/json' expect { put :update, params: { class_id => object.id, id: supplemental_file.id, file: file_update, format: :html }, session: valid_session }.to change { object.reload.supplemental_files.first.file } expect(response).to have_http_status(:ok) - expect(response.body).to eq({ "supplemental_file": supplemental_file.id }.to_json) + expect(response.body).to eq({ "id": supplemental_file.id }.to_json) end end @@ -493,6 +495,7 @@ before { ApiToken.create token: 'secret_token', username: 'archivist1@example.com', email: 'archivist1@example.com' } it "returns a 400" do request.headers['Avalon-Api-Key'] = 'secret_token' + request.headers['Accept'] = 'application/json' put :update, params: { class_id=> object.id, id: supplemental_file.id, metadata: valid_update_attributes, format: :html }, session: valid_session expect(response).to have_http_status(400) end @@ -515,10 +518,11 @@ end context "API updating caption with invalid file type" do - let(:supplemental_file) { FactoryBot.create(:supplemental_file, :with_caption_tag, skip_file_type: true) } + let(:supplemental_file) { FactoryBot.create(:supplemental_file, :with_caption_tag) } before { ApiToken.create token: 'secret_token', username: 'archivist1@example.com', email: 'archivist1@example.com' } it "returns a 500" do request.headers['Avalon-Api-Key'] = 'secret_token' + request.headers['Accept'] = 'application/json' put :update, params: { class_id=> object.id, id: supplemental_file.id, file: uploaded_file, format: :html }, session: valid_session expect(response).to have_http_status(500) end From 27c6db0e9c151f9ccb6ab019cc5ab17e0876e9f7 Mon Sep 17 00:00:00 2001 From: Mason Ballengee Date: Tue, 11 Jun 2024 16:07:16 -0400 Subject: [PATCH 078/152] Fix tests --- spec/models/supplemental_file_spec.rb | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/spec/models/supplemental_file_spec.rb b/spec/models/supplemental_file_spec.rb index 5169ecc2ef..fd50cc22d2 100644 --- a/spec/models/supplemental_file_spec.rb +++ b/spec/models/supplemental_file_spec.rb @@ -44,12 +44,6 @@ expect(subject.errors[:file_type]).not_to be_empty end end - context "caption metadata only with skip_file_type" do - let(:subject) { FactoryBot.build(:supplemental_file, :with_caption_tag, skip_file_type: true) } - it 'should skip validation' do - expect(subject.valid?).to be_truthy - end - end end describe 'scopes' do @@ -83,14 +77,6 @@ solr_doc = ActiveFedora::SolrService.query("id:#{RSolr.solr_escape(transcript.to_global_id.to_s)}").first expect(solr_doc["transcript_tsim"]).to eq ["00:00:03.500 --> 00:00:05.000 Example captions"] end - - context 'skip index' do - let(:transcript) { FactoryBot.build(:supplemental_file, :with_transcript_file, :with_transcript_tag, skip_index: true) } - it 'does not trigger callback' do - expect(transcript).to_not receive(:index_file) - transcript.save - end - end end context 'on update' do @@ -192,7 +178,7 @@ context 'machine generated file' do let(:supplemental_file) { FactoryBot.create(:supplemental_file, :with_caption_file, tags: ['machine_generated'], label: 'Test') } it 'includes machine_generated in json' do - expect(subject[:machine_generated]).to eq '1' + expect(subject[:machine_generated]).to eq true end end @@ -205,7 +191,7 @@ context 'as transcript' do let(:supplemental_file) { FactoryBot.create(:supplemental_file, :with_caption_file, tags: ['caption', 'transcript'], label: 'Test') } it 'includes treat_as_transcript in JSON' do - expect(subject[:treat_as_transcript]).to eq '1' + expect(subject[:treat_as_transcript]).to eq true end end end From dc7bcb0656855d1b6d400f9c5fb54eb04144303b Mon Sep 17 00:00:00 2001 From: cjcolvar Date: Wed, 12 Jun 2024 11:30:26 -0400 Subject: [PATCH 079/152] New ramp build --- package.json | 2 +- yarn.lock | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 08d62cc3c3..af93d90620 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "@babel/plugin-proposal-object-rest-spread": "^7.20.7", "@babel/preset-react": "^7.0.0", "@babel/runtime": "7", - "@samvera/ramp": "https://github.com/samvera-labs/ramp.git#25a519f3f78dd8579ff8b8b96e59c846879628c8", + "@samvera/ramp": "https://github.com/samvera-labs/ramp.git", "babel-plugin-macros": "^3.1.0", "babel-plugin-transform-react-remove-prop-types": "^0.4.24", "buffer": "^6.0.3", diff --git a/yarn.lock b/yarn.lock index 9431fdb12b..308b057ed7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1477,12 +1477,13 @@ estree-walker "^2.0.2" picomatch "^2.3.1" -"@samvera/ramp@https://github.com/samvera-labs/ramp.git#25a519f3f78dd8579ff8b8b96e59c846879628c8": +"@samvera/ramp@https://github.com/samvera-labs/ramp.git": version "3.1.2" - resolved "https://github.com/samvera-labs/ramp.git#25a519f3f78dd8579ff8b8b96e59c846879628c8" + resolved "https://github.com/samvera-labs/ramp.git#836f27f2225fbb4406b412603a1ca124d4563430" dependencies: "@rollup/plugin-json" "^6.0.1" "@silvermine/videojs-quality-selector" "^1.3.1" + classnames "^2.5.1" mammoth "^1.4.19" manifesto.js "^4.1.0" mime-db "^1.52.0" @@ -2534,6 +2535,11 @@ classnames@^2.3.1: resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e" integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA== +classnames@^2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.5.1.tgz#ba774c614be0f016da105c858e7159eae8e7687b" + integrity sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow== + clean-css@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.2.3.tgz#507b5de7d97b48ee53d84adb0160ff6216380f78" From 821f212ebaeb4567f99ae32d459d547020eb4214 Mon Sep 17 00:00:00 2001 From: cjcolvar Date: Wed, 12 Jun 2024 11:33:43 -0400 Subject: [PATCH 080/152] Enable playback rate control in all ramp instances --- app/javascript/components/MediaObjectRamp.jsx | 2 +- app/javascript/components/PlaylistRamp.jsx | 2 +- app/javascript/components/embeds/EmbeddedRamp.jsx | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/javascript/components/MediaObjectRamp.jsx b/app/javascript/components/MediaObjectRamp.jsx index f8476ef071..250e4d3ef1 100644 --- a/app/javascript/components/MediaObjectRamp.jsx +++ b/app/javascript/components/MediaObjectRamp.jsx @@ -155,7 +155,7 @@ const Ramp = ({ : ( {sections_count > 0 && - +
    {
    }
    diff --git a/app/javascript/components/PlaylistRamp.jsx b/app/javascript/components/PlaylistRamp.jsx index d478bd8b70..722a88d9c3 100644 --- a/app/javascript/components/PlaylistRamp.jsx +++ b/app/javascript/components/PlaylistRamp.jsx @@ -101,7 +101,7 @@ const Ramp = ({ startCanvasId={startCanvasId}>