Compare commits

...

459 Commits
master ... 2.x

Author SHA1 Message Date
Rosie Le Faive b8f0b9c966
Allow NodeHasMediaUse view filter to work on views with node relationships. (#1010) 4 months ago
Akanksha Singh da47bcfb08
Node has Parent context does not explicitly check if the field exists. (#1019) 4 months ago
Rosie Le Faive 5e958a5e10 Allow application/xml in OCR Action. 4 months ago
Rosie Le Faive 3902cce0ac
Allow filehash 3 (#1016) 5 months ago
Aron Novak c80769580c
Do not have a fatal error on a missing action (#1014) 5 months ago
Adam 54206de712
Fix Functional Javascript CI tests (#1004) 5 months ago
Joe Corall 9b2661696d
Add hOCR functionality (#1006) 5 months ago
Alexander O'Neill 089a3654ba
Merge pull request #1007 from rosiel/2.x 5 months ago
Rosie Le Faive 263666f5fc Remove new_storage_type from form part 2. 6 months ago
Rosie Le Faive 95c2d6c0c9 Syntax. 6 months ago
Rosie Le Faive cde2c133e1 Update tests for D10.3's new field selector form. 6 months ago
Rosie Le Faive a2c31fcaad @adam-vessey's fix for drupalGet headers. 6 months ago
Rosie Le Faive 9ed3637339
Update islandora.module (#1008) 6 months ago
Rosie Le Faive 13bc15ea43 Last attempt to not pass translatables as button labels. 7 months ago
Rosie Le Faive 9f2277fc51 Exclude PHP 8.3 with Drupal 10.1. 7 months ago
Rosie Le Faive 89261c17ae Add some forgotten translateable markup. 7 months ago
Rosie Le Faive 3784def287 Tests use strings not translateable markup to select interface buttons. 7 months ago
Rosie Le Faive e30cdbf681 Update testing drupal and php versions 7 months ago
Rosie Le Faive 3065c87874
Remove Feature-ness of Islandora Core Feature. (#968) 9 months ago
Alan Stanley d0e0c29921
Issue 1000 (#1001) 9 months ago
Alan Stanley 28174c3ce4
Change to Boolean logic (#999) 10 months ago
ajstanley 4404dff246 reverting accidental commit 10 months ago
ajstanley c93c1ff940 change to Boolean logic 10 months ago
ajstanley b65881625a change to Boooean logic 10 months ago
ajstanley c149781da0 Merge branch '2.x' of https://github.com/Islandora/islandora into 2.x 10 months ago
Seth Shaw c6341649ca
Use FileUrlGeneratorInterface (#996) 10 months ago
Annie Oelschlager 4630439760
Merge pull request #986 from rosiel/coi 10 months ago
Rosie Le Faive 056695c79c
8.2 deprecations from tests (#995) 11 months ago
Rosie Le Faive 4b2b9b221b
Fix tests (#991) 11 months ago
Joe Corall f29fef2bac
Do not render the pdf_url metatag if there is no value (#985) 11 months ago
Adam 095e0ecf67
Update to use the new hook. (#992) 11 months ago
Rosie Le Faive d5556f445d
Add PHP 8.2 <strike>and Drupal 10.2</strike> to testing matrix (#987) 11 months ago
Rosie Le Faive 2c91dc6f58 syntax. 11 months ago
Rosie Le Faive 16617a9dd7 Composer suggest COI. 11 months ago
Rosie Le Faive c05236ac8c Add COI integration to IIIF module. 11 months ago
Rosie Le Faive 6cfaca36e7
Update src/Form/IslandoraSettingsForm.php 11 months ago
Rosie Le Faive f077af677b Add COI integration to islandora settings form. 11 months ago
Adam 572ffcf2e1
Fix up typo. (#984) 11 months ago
Alexander O'Neill f7a77820d3
Merge pull request #980 from rosiel/issue-975 12 months ago
Annie Oelschlager 58d1b37f11
Merge pull request #982 from Islandora/rosiel-patch-1 12 months ago
Rosie Le Faive 76eb4717a2
Specify fetch-depth during mirroring to gitlab. 12 months ago
Annie Oelschlager e2ec673017
Merge pull request #971 from rosiel/media-redirect 1 year ago
Rosie Le Faive d6e07491d2 UI text improvement. 1 year ago
Rosie Le Faive c2cd14cfd5 Add config option to redirect after media add. 1 year ago
Rosie Le Faive fdfdd87472 Add media save redirect. 1 year ago
Rosie Le Faive 84c6ca85d8 Remove extra file. 1 year ago
Rosie Le Faive 91253bef14 Declare httpClient variable. 1 year ago
dannylamb e3399d3968
Stripping out json metadata in the queue messages except for the ones… (#973) 1 year ago
aOelschlager fd8319b7b2
Merge pull request #977 from Islandora/revert-976-rosiel-patch-1 1 year ago
Rosie Le Faive 5d83504778
Revert "Revert gitlab mirror to checkout v1." 1 year ago
aOelschlager 33340c2722
Merge pull request #976 from Islandora/rosiel-patch-1 1 year ago
Rosie Le Faive d1357d347d
Revert gitlab mirror to checkout v1. 1 year ago
Rosie Le Faive 0408edb93f
Push tags to gitlab. (#974) 1 year ago
Alexander O'Neill 71f0945e3c
959 Use image dimension properties in IIIF Manifest if they exist… (#969) 1 year ago
Alexander O'Neill 11afd42c8a
Issue #964: Allow relative paths in IIIF manifests. (#965) 1 year ago
Rosie Le Faive 4eef5f566d Deprecate advanced_search. 1 year ago
Rosie Le Faive 5331b0b7d5
Add push to Gitlab action. (#966) 1 year ago
Alexander O'Neill 8f1537670d
Merge pull request #960 from rosiel/drupal10 1 year ago
Rosie Le Faive 0fe2a8f559 Update features spec. 1 year ago
Alexander O'Neill ac818a0f27
Issue #961: Put back accidentally-removed IIIF Manifest alter hooks. (#962) 1 year ago
Rosie Le Faive 408776437b phpcs. 1 year ago
Rosie Le Faive 6b05ff5f99 typo in permission name. 1 year ago
Rosie Le Faive 5c09a1e3f4 Use a better version compare call. 1 year ago
Rosie Le Faive 6d59c526d3 In Drupal 10.1, include new file delete permission. 1 year ago
Rosie Le Faive 91016fd237 Don't delete files in the thumbnail field. 1 year ago
Rosie Le Faive 9ef509b0ad Fix merge conflict. 1 year ago
Rosie Le Faive 621b7a2c7d Remove duplicate line. 1 year ago
Rosie Le Faive aec8178846 Stop using deprecated FILE_STATUS_PERMANENT. 1 year ago
Rosie Le Faive 8adc44859c Update fixtures to have config UUIDs. 1 year ago
Rosie Le Faive d1861de270 Test on 8.1. 1 year ago
Rosie Le Faive d293d7702a Change to check access (true). 1 year ago
Rosie Le Faive a88486ca28 Add accessCheck FALSE to all queries. 1 year ago
Rosie Le Faive 8ef277527b Fix tests. 1 year ago
Rosie Le Faive e67e8e5f25 Remove problematic comments. 1 year ago
Rosie Le Faive 52947f3f96 Use phpcs friendly comment... 1 year ago
Rosie Le Faive e4dc48fca2 Tests were not finding the media use field. 1 year ago
Rosie Le Faive 7470327871 Inject fileUrlGenerator into Image Field formatter. 1 year ago
Rosie Le Faive 8f8e6a3c35 Test: Breadcrumbs config dependencies missing schema. 1 year ago
Jordan Dukart 9cabfc2e23 Fix a typo. (#958) 1 year ago
Rosie Le Faive ffd128db80 Typo prevented submodule functional tests from running. 1 year ago
Rosie Le Faive 2c332348dc Undo overzealous Rector. 1 year ago
Rosie Le Faive 7d7f97746a Drupal Rector. 1 year ago
Seth Shaw 91490ddbe2 bump jwt version (#952) 1 year ago
Alexander O'Neill e492b92d9f Remove Islandora Utils from Islandora IIIF. 1 year ago
Alexander O'Neill d4cac72993 Fix PHPCS errors. 1 year ago
Alexander O'Neill 9f5eceea07 Fix PHPCS errors. 1 year ago
Alexander O'Neill cf243f368d Fix PHPCS errors. 1 year ago
Alexander O'Neill f41dc59f1b Remove term-based hOCR configuration since we can just use Views. 1 year ago
Alexander O'Neill 7527b1fa6f Address PHPCS errors. 1 year ago
Alexander O'Neill 723f102365 Update Islandora IIIF README. 1 year ago
Alexander O'Neill 9ef3bcf440 Refactor IIIF Manifest Views Style plugin. 1 year ago
Alexander O'Neill 622eaab6a0 Issue 944: Pull hOCR from separate media in IIIF manifest. 1 year ago
Rosie Le Faive 374ab02d07 phpcs 1 year ago
Alexander O'Neill a7eaacc1d5 Issue #947 Add tokens for Original File filename, extension. 1 year ago
Alexander O'Neill 61c6e737c1 Fix PHPCS errors. 1 year ago
Alexander O'Neill 17b5049578 Issue #944: Un-hide arguments field in Text Extraction action. 1 year ago
Alexander O'Neill 5bc1584dd7 Issue 937: More PHPCS fixes. 1 year ago
Alexander O'Neill 43f32d1bcf Issue #937: Fix PHPCS issues. 1 year ago
Alexander O'Neill 78baec07e8 Issue #973 Add hooks to IIIF manifest Views Style plugin. 1 year ago
kstapelfeldt 0bd05b6c44 Update README.md 1 year ago
Alexander O'Neill 06dd1651ac Issue #939: Fix incorrect IIIF Manifest canvas Ids. 1 year ago
Alexander O'Neill 138eab2016 Issue #941: Only add <br/> tags to plain text extracted text fields. (#942) 1 year ago
Jared Whiklo 7b0ff739cd Update dependencies to tagged versions 1 year ago
Jared Whiklo ff4e0cafc4 More code style 1 year ago
Jared Whiklo ba93ad35a3 Fix tests 1 year ago
Jared Whiklo 860abf3c06 code style 1 year ago
Jared Whiklo 5dd96b8f22 Use new chullo static methods 1 year ago
Jared Whiklo 2c1d88f400 Use new package 1 year ago
Jared Whiklo 8ce1ad2cda Remove deprecate MimeTypeGuesser 1 year ago
Noah W. Smith 58da2a6af1 Missed one link; corrected TAG link 1 year ago
Noah W. Smith 7d54a42d48 Update maintainer and sponsor info 1 year ago
Lucas van Schaik ee451667d4 Revert "Check if action is appropriate for entity before executing" 1 year ago
Lucas van Schaik 4bcc7d4417 Revert "Make sure that the action is appropriate: either system or with same entity type" 1 year ago
Lucas van Schaik b82accf763 Revert "Comment too long" 1 year ago
Lucas van Schaik 1bbb48f70f Be consistent with context module, issue 3177007 1 year ago
Lucas van Schaik 088f1fcdd0 Comment too long 1 year ago
Lucas van Schaik 50685aebe6 Make sure that the action is appropriate: either system or with same entity type 1 year ago
Lucas van Schaik 2c48c8795f Check if conditions exist before applying contexts to them 1 year ago
Lucas van Schaik 9f83322902 Check if action is appropriate for entity before executing 1 year ago
Lucas van Schaik 709938cf29 Implement solution for drupal issues 3089660 and 3045666 1 year ago
Rosie Le Faive c67f3185ec Update Crayfish Commons dependency 1 year ago
Jordan Dukart 46cd2f9950 Reset contexts before evaluation. (#932) 1 year ago
Seth Shaw 4e091e524f fix for deprecated Symfony Event class 1 year ago
Ant Brown ee2b964a07 Fix deprecated File::url(), use createFileUrl() instead (#855) 1 year ago
JojoVes 2376f77831 Replace deprecated 'context' condition annotation with 'context_definitions' (#925) 1 year ago
Rosie Le Faive 8686dbf74b Avoid duplicate counts of the same file being deleted. 1 year ago
Rosie Le Faive a77bd2d949 Return array not string. 1 year ago
Rosie Le Faive cb2e1c4809 Remove 8 for consistency. 1 year ago
Rosie Le Faive 2040952740 Integer weight selector test module to D10. 1 year ago
Rosie Le Faive 8f77733c84 Allow jsonld 3.x. 1 year ago
Rosie Le Faive 0665310346 Remove unused use statements. 1 year ago
Rosie Le Faive bf17ed9bbc Allow contexts module in RC. 1 year ago
Rosie Le Faive 354341988b Drupal 10 Compatibility from Upgrade Status 1 year ago
Jordan Dukart 8502a347ff
Merge pull request #957 from rosiel/get-tests-passing 1 year ago
Rosie Le Faive f474f7b745 Remove duplicate line. 1 year ago
Jordan Dukart ece94a24f5
Fix a typo. (#958) 1 year ago
Rosie Le Faive 58ab9a3b70 Stop using deprecated FILE_STATUS_PERMANENT. 1 year ago
Rosie Le Faive 0d7f5d927f Update fixtures to have config UUIDs. 1 year ago
Rosie Le Faive 05fc3f9b88 Test on 8.1. 1 year ago
Rosie Le Faive 4eae636383 Change to check access (true). 1 year ago
Rosie Le Faive dd514a3eb0 Add accessCheck FALSE to all queries. 1 year ago
Rosie Le Faive c49c131ed8 Fix tests. 1 year ago
Rosie Le Faive 760593b4e0 Remove problematic comments. 1 year ago
Rosie Le Faive 54116efbab Use phpcs friendly comment... 1 year ago
Rosie Le Faive 41e4dc6fff Tests were not finding the media use field. 1 year ago
Rosie Le Faive 8ee4fb5aff Inject fileUrlGenerator into Image Field formatter. 1 year ago
Rosie Le Faive 1f09439e1e Test: Breadcrumbs config dependencies missing schema. 1 year ago
Jordan Dukart d1ac274543
Merge pull request #955 from rosiel/ci 1 year ago
Rosie Le Faive b3f2c006b1 Typo prevented submodule functional tests from running. 1 year ago
Alexander O'Neill c41f574268
Merge pull request #943 from rosiel/drupal10 1 year ago
Seth Shaw cc5b5f838d
bump jwt version (#952) 1 year ago
Alexander O'Neill da3311825c Remove Islandora Utils from Islandora IIIF. 1 year ago
Alexander O'Neill 6fe405ee93 Fix PHPCS errors. 1 year ago
Alexander O'Neill 30296b4566 Fix PHPCS errors. 1 year ago
Alexander O'Neill 97f3b2daf1 Fix PHPCS errors. 1 year ago
Alexander O'Neill 4ca6a0c88a Remove term-based hOCR configuration since we can just use Views. 1 year ago
Alexander O'Neill e1fde43e21 Address PHPCS errors. 1 year ago
Alexander O'Neill cf7b09f097 Update Islandora IIIF README. 1 year ago
Alexander O'Neill 2307dc6936 Refactor IIIF Manifest Views Style plugin. 1 year ago
Alexander O'Neill 8f5154c24e Issue 944: Pull hOCR from separate media in IIIF manifest. 1 year ago
Rosie Le Faive aa4d10649b phpcs 1 year ago
Willow Gillingham b0057d1895
Merge pull request #948 from Islandora/947-filename-tokens 1 year ago
Rosie Le Faive 879dc2091d Undo overzealous Rector. 1 year ago
Willow Gillingham 7a57d2dfc8
Merge pull request #945 from Islandora/944-text-extraction-args 1 year ago
Alexander O'Neill c1c0f21cb5 Issue #947 Add tokens for Original File filename, extension. 1 year ago
Alexander O'Neill 2e1df20b0c Fix PHPCS errors. 1 year ago
Alexander O'Neill 1bdb7323e3 Issue #944: Un-hide arguments field in Text Extraction action. 1 year ago
Rosie Le Faive 06f2a5754e Drupal Rector. 1 year ago
Willow Gillingham 48b73c562d
Merge pull request #938 from Islandora/issue-973-islandora-iiif-hooks 1 year ago
kstapelfeldt c80e687168
Update README.md 1 year ago
Willow Gillingham 66401baec9
Merge pull request #940 from Islandora/issue-939-iiif-manifest-canvas-ids 1 year ago
Alexander O'Neill e4fbbb375a
Issue #941: Only add <br/> tags to plain text extracted text fields. (#942) 1 year ago
Alexander O'Neill e8712d85f7 Issue #939: Fix incorrect IIIF Manifest canvas Ids. 1 year ago
Jared Whiklo 3ef2f1038e Update dependencies to tagged versions 1 year ago
Jared Whiklo 8370383e83 More code style 1 year ago
Jared Whiklo a02738bd3f Fix tests 1 year ago
Jared Whiklo 492338c653 code style 1 year ago
Jared Whiklo 97c3ddbdd1 Use new chullo static methods 1 year ago
Jared Whiklo a4b9f7fc4e Use new package 1 year ago
Jared Whiklo 7e09750dee Remove deprecate MimeTypeGuesser 1 year ago
Alexander O'Neill 994545798b Issue 937: More PHPCS fixes. 1 year ago
Alexander O'Neill 8286dfe423 Issue #937: Fix PHPCS issues. 1 year ago
Alexander O'Neill 8bc98e062f Issue #973 Add hooks to IIIF manifest Views Style plugin. 1 year ago
Don Richards e5b223a7a1
Merge pull request #935 from noahwsmith/patch-3 1 year ago
Noah W. Smith 718af168f4
Missed one link; corrected TAG link 1 year ago
Noah W. Smith 539952e89c
Update maintainer and sponsor info 1 year ago
Jordan Dukart e366da3257
Merge pull request #931 from LeidenUniversityLibrary/fix_documentation_2069 2 years ago
Lucas van Schaik d041ec3bf5 Revert "Check if action is appropriate for entity before executing" 2 years ago
Lucas van Schaik 233a65d871 Revert "Make sure that the action is appropriate: either system or with same entity type" 2 years ago
Lucas van Schaik ee425d2c1f Revert "Comment too long" 2 years ago
Rosie Le Faive bb06d8143c Update Crayfish Commons dependency 2 years ago
Jordan Dukart c721f9ba07
Reset contexts before evaluation. (#932) 2 years ago
Jordan Dukart db85922765
Merge pull request #928 from seth-shaw-asu/issue-927 2 years ago
Lucas van Schaik b89da473f1 Be consistent with context module, issue 3177007 2 years ago
Lucas van Schaik aba5052308 Comment too long 2 years ago
Lucas van Schaik a409d402aa Make sure that the action is appropriate: either system or with same entity type 2 years ago
Lucas van Schaik 4250109c63 Check if conditions exist before applying contexts to them 2 years ago
Lucas van Schaik 87f475d81c Check if action is appropriate for entity before executing 2 years ago
Lucas van Schaik 74755f8074 Implement solution for drupal issues 3089660 and 3045666 2 years ago
Seth Shaw b57f8ff64d fix for deprecated Symfony Event class 2 years ago
Ant Brown 2794f01164
Fix deprecated File::url(), use createFileUrl() instead (#855) 2 years ago
Simon Hieu Mai 488a82b741
Update islandora_advanced_search.module 2 years ago
Simon Hieu Mai 71c720736f
Update islandora_advanced_search.module 2 years ago
JojoVes c36f7d9978
Replace deprecated 'context' condition annotation with 'context_definitions' (#925) 2 years ago
Simon Hieu Mai da35fb8950
Delete soft-limit.js 2 years ago
Simon Hieu Mai af224e42cf
Delete facets-views-ajax.js 2 years ago
Simon Hieu Mai 0d2e584316
Update islandora_advanced_search.module 2 years ago
Adam fe7e450a51
Index `field_weight`'s value. (#924) 2 years ago
Alexander O'Neill 4f4e661e38
Merge pull request #918 from Islandora/issues-917-ocr-action-defaults 2 years ago
Adam 6f2955b061
Avoid ::referencedEntities() call when it is not expected to exist. (#923) 2 years ago
Rosie Le Faive cefee615c0
Warn re. tiffs and jp2s in image file derivatives. (#921) 2 years ago
Jordan Dukart 4ec340744c
Merge pull request #922 from rosiel/update-deps 2 years ago
Rosie Le Faive 12e28f1284 Sort dependencies. 2 years ago
Rosie Le Faive b326d967a6 Update dependencies. 2 years ago
Nigel Banks dfa095951e
Merge pull request #892 from rosiel/update-image-file-derivs 2 years ago
Nigel Banks f780c69556
Merge pull request #908 from rosiel/add-form-improvements-2 2 years ago
Nigel Banks db31d1438d
Merge pull request #920 from rosiel/infinite-derivs 2 years ago
Nigel Banks f63dce64ce
Merge pull request #912 from rosiel/fix-checksum-view 2 years ago
Rosie Le Faive 7df45a083a Use new syntax for filehash. 2 years ago
Rosie Le Faive 665abfbd6c phpcs. 2 years ago
Rosie Le Faive 41f8710122 Use a proper exception. 2 years ago
Jordan Dukart 5472f6d7e1
Merge pull request #916 from rosiel/fix-iiif 2 years ago
dannylamb f86f2bedb1
Updating default config for GenerateOCRDerivative.php 2 years ago
Jordan Dukart 33965b4ca6
Merge pull request #911 from rosiel/eva 2 years ago
Rosie Le Faive 0b7f12d3ba No infinite derivatives. 2 years ago
Rosie Le Faive b47d37b1b6 Fix errors when OCR field not set. 2 years ago
Jordan Dukart 023b24b5d3
Merge pull request #905 from shriram1056/dev 2 years ago
shriram1056 ee85472dc8 minor changes and post_update for delete_media_and_files 2 years ago
Rosie Le Faive 6c582a8702
Permit newer version of migrate_plus. 2 years ago
Jason Hildebrand f71f6dc2e8
Fix warning by checking whether key is set. (#909) 2 years ago
Jason Hildebrand 5f4a6ab3ae
Eliminate warnings when using NodeHasMediaUse views filter. (#914) 2 years ago
Rosie Le Faive def4fda5b6 Include original hash, and re-hash. 2 years ago
Rosie Le Faive 541620493b Updates settings and view for filehash^2. 2 years ago
shriram1056 e15b6322ff fixed log message 2 years ago
shriram1056 48b5333b2d skip entity types protected by entity integrity reference and updated test cases for toggle feature 2 years ago
Rosie Le Faive 74dcfd0fa4 Improve wording on multi-file derivative Action forms. 2 years ago
Rosie Le Faive 72eaaf659a Add Image fields only to Image derivative code. 2 years ago
Rosie Le Faive 4bed36dede Revert "Allowing Image fields for multi-file media (#860)" 2 years ago
Rosie Le Faive b0c43accb8 Upgrade the EVA to 3.0. 2 years ago
shriram 5c24c19018 added feature toggle for the behavior 2 years ago
shriram 9b58fc9ecb added islandora.libraries.yml 2 years ago
shriram ef1f36f283 Updated test cases to include file deletion 2 years ago
shriram 7ef1afffa2 delete media with files and translations 2 years ago
Rosie Le Faive 386ba0ceb1 Detect access before showing manage links. 2 years ago
Rosie Le Faive 7eebb65c2b Clarify wording and add manage link. 2 years ago
Rosie Le Faive e3c7e6edda Document the add members and add media pages. 2 years ago
Adam 3f7ca2ca10
Fix/batch upload children, with validation according to default widget (#896) 2 years ago
shriram fd5c38a107 added test cases for deleting node with media 2 years ago
shriram 5bd2cdd851 check if the entity is a node 2 years ago
shriram 3602bb441b fixed failing coding standard checks 2 years ago
shriram 33ce9e4e13 list media associated with a Islandora object 2 years ago
Willow Gillingham bdbef45baa
Merge pull request #897 from Islandora/hocr 2 years ago
Alexander O'Neill 2e4780163e Add check for falsity in IIIF Manifest along with 'isset()' 2 years ago
shriram aa3c71893e delete media associated with an islandora object 2 years ago
Jared Whiklo 0948436395
Unset pseudo field in display modes automatically (#899) 2 years ago
Mark Jordan ca1d9f6f60
Issue #2170: Sort "manage members" View results by field_weight (#900) 2 years ago
Adam a250c2ac78
Fix/schemas (#898) 2 years ago
Islandora Foundation Community 0e8c05cc7b
Update PULL_REQUEST_TEMPLATE.md 2 years ago
Alexander O'Neill c07d1f6540 Fix PHPCS Errors. 2 years ago
Alexander O'Neill a41ecaa754 Fix PHPCS errors. 2 years ago
Alexander O'Neill 78cee0a35a Fix PHPCS errors. 2 years ago
Alexander O'Neill bf25e2447a Fix error caused by rebase. 2 years ago
Alexander O'Neill 5e1d53d377 Add empty check when adding hOCR to IIIF manifest. 2 years ago
Alexander O'Neill 49c48a1493 WIP: Add hOCR file stream to IIIF Manifest. 2 years ago
Alexander O'Neill 4179f5cee7 WIP get hocr field in iiif view. 2 years ago
Alexander O'Neill 0644795c54 Skip empty image fields when constructing IIIF manifest. 2 years ago
Alexander O'Neill bd17a381ea Add Structured OCR field to IIIF Manifest view. 2 years ago
Alexander O'Neill 0bea8da572 WIP Modify GenerateOCRDerivativeFile to support hOCR 2 years ago
Adam 725b559280
Add file access check to IIIF manifest generation. (#884) 2 years ago
Rosie Le Faive 3048594a8b
Allow media to use integer weight selector. (#894) 2 years ago
Alan Stanley 62fbc6d288
Merge pull request #895 from seth-shaw-asu/issue-2152 2 years ago
Seth Shaw d405a2f14f
throw error instead of returning null data 2 years ago
Rosie Le Faive 7bca3d5675
IsIslandora views filter and context condition use Islandora Utils. (#881) 2 years ago
Seth Shaw 3c194cc7b7
Merge pull request #886 from rosiel/no-drupal-10 2 years ago
dannylamb a297796f47
Allowing Image fields for multi-file media (#860) 2 years ago
Rosie Le Faive eb53ff474e revert setting up chromedriver. 2 years ago
Rosie Le Faive 07e3c49ecc Add webdriver setting in phpunit.xml. 2 years ago
Rosie Le Faive 87231dc5c0 try now 2 years ago
Rosie Le Faive 705f623fdb chromedriver. 2 years ago
Rosie Le Faive 1415bd509b chromedriver. 2 years ago
Rosie Le Faive cebeeaec5c typo. 2 years ago
Rosie Le Faive 724d0845f4 typo. 2 years ago
Rosie Le Faive dd58302b98 Try to get FunctionalJavascript working. 2 years ago
Jordan Dukart 573d6878ed
Merge pull request from GHSA-m58q-qq5h-mgqq 2 years ago
Rosie Le Faive 704405e3da no newline is one newline. 2 years ago
Rosie Le Faive 2d8df5a226 Remove future versions we're not ready for. 2 years ago
Rosie Le Faive cdb83ece92 Update matrix. 2 years ago
Rosie Le Faive f4e91b20a3 Fix matrix. 2 years ago
Rosie Le Faive 551a6673bf Add back drupal 9.3. 2 years ago
Rosie Le Faive 5644a68a06 Try again to use matrix overrides. 2 years ago
Rosie Le Faive 98c9ba4c63 simplify matrix. 2 years ago
Rosie Le Faive 85cf0822f5 Allow failure on php 8.1 and add back drupal 10. 2 years ago
Rosie Le Faive 4d565164d7 Update stomp. 2 years ago
Rosie Le Faive 887cd8791e Update php version in README. 2 years ago
Rosie Le Faive 19db152531 Test matrix: remove php7.3, mysql5.7. 2 years ago
Alan Stanley 72c7dff3e8
Merge pull request #880 from seth-shaw-unlv/has_media_view_filter 2 years ago
Seth Shaw 39c7b3180a remove ability to expose form; todo: bring back some day 2 years ago
Seth Shaw 352631099e paranoid checking 2 years ago
Seth Shaw f6a66fe082 use term label for filter summary 2 years ago
Seth Shaw 472f487b35 Merge branch 'has_media_view_filter' of github.com:seth-shaw-unlv/islandora into has_media_view_filter 2 years ago
Seth Shaw a90630d976 turn textfield into a select 2 years ago
Seth Shaw cc958f4164
Merge branch 'Islandora:2.x' into has_media_view_filter 2 years ago
Seth Shaw 62211ff909 add NodeHasMediaUse views filter 2 years ago
Alexander O'Neill 491631c4db
Merge pull request #877 from adam-vessey/fix/filehash-dedup 2 years ago
Jordan Dukart 019572a778
Use the interface not the class. (#879) 2 years ago
Adam 3d122af5d6
Avoid attempting to refer to an unknown index. (#876) 2 years ago
Adam Vessey 61f9ec9106 That was a string, whoops... 2 years ago
Adam Vessey 63a77bd834 Move to int for config. 2 years ago
Rosie Le Faive 222c9601c1
Rename multifile media ocr derivative type. (#875) 2 years ago
Jordan Dukart ba74759f03
Merge pull request #874 from seth-shaw-unlv/issue-2110 2 years ago
Jordan Dukart 93c19b6c6e
Merge pull request #869 from Islandora/iiif_title 2 years ago
Seth Shaw b38f195a50 Produce error if viewing a media without a source file 2 years ago
Alexander O'Neill e5a1f99c57 IIIF Manifest: Use dependency injection for EntityTypeManager service. 2 years ago
Rosie Le Faive 032280827f
Support multiple tracks. (#871) 2 years ago
Jordan Dukart 1a13b3e713
Merge pull request #873 from seth-shaw-unlv/multiple-parents 2 years ago
Seth Shaw e1428bb13a PR recommendations; clarify max-depth config 2 years ago
Seth Shaw 73d0d66402 maxDepth counting should start at 1, not zero 2 years ago
Seth Shaw 9c283ea0c0 add breadcrumbs form; fix includeSelf error 2 years ago
Seth Shaw ed0979f97c make breadcrumbs referenceField an array 2 years ago
Alexander O'Neill f6fa77984b Islandora Image schema inherits from parent. 2 years ago
Alexander O'Neill 11bc7886ea Update islandora_image schema to fix failing test. 2 years ago
Alexander O'Neill 52d3df1462 Suppress 'Schema incomplete' error in Functional test. 2 years ago
Alexander O'Neill 92d5a7fbbd Fix Coder errors in IIIF views style plugin. 2 years ago
Jared Whiklo d8d101e571
Ensure we can connect to the JMS Broker everytime (#868) 2 years ago
Alexander O'Neill 71b1cb5d64 Add try() wrapper to IIIF manifest title generate function. 2 years ago
Alexander O'Neill e9f9aad49c Set IIIF Manifest title based on content title. 2 years ago
Simon Hieu Mai a04a72c483
The "Node has ancestor" condition shouldn't be required (#867) 2 years ago
Jordan Dukart e0152eaa8c
Per step not job. (#866) 2 years ago
Jordan Dukart a7e4c1659e
Add ancestor helper and condition. (#865) 2 years ago
Ant Brown bd98028f00
Fill in blanks for IntegerWeightSelector (Islandora#2065) (#863) 3 years ago
Jordan Dukart 4c439d4817
Ensure node exists before using it. (#864) 3 years ago
Jordan Dukart 6d752e479e
Merge pull request #862 from Islandora/jsonld-cache-tags 3 years ago
Alexander O'Neill 4c08d5a274 islandora:862 Remove white space at end of line causing failing tests. 3 years ago
Alexander O'Neill ac749ce3b5 Fix syntax error. 3 years ago
Alexander O'Neill f7287be012 islandora:862 Add Use statement to go with last commit. 3 years ago
Alexander O'Neill 9c8193b75a
islandora:862 Incorporate PR review suggestion 3 years ago
Islandora Foundation Community 081183bc71
Update README.md 3 years ago
Alexander O'Neill 20f7ebb332 Maintain backward compatibility with File Hash v. 1.x 3 years ago
Alexander O'Neill 1a61b17875 Update to File Hash 2.x due to failing tests. 3 years ago
Alexander O'Neill 2199336446 Add further check to jsonld context cacheability to avoid white screen. 3 years ago
Alexander O'Neill 7709425358 Add check for cache tags in JSON-LD alter hook. 3 years ago
Jared Whiklo c1aa0a5f2f
Update testing (#861) 3 years ago
Seth Shaw 90d6795172
Purge Migrate Tools (Issue 1994) (#859) 3 years ago
Alan Stanley 4f45cb8c06
added additional iiif config exception (#857) 3 years ago
ajstanley b733713610 added additional exception 3 years ago
Jordan Dukart adbfea79a4
Check if the cue has changed before updating the HTML. (#853) 3 years ago
Seth Shaw 01f22b717f
Explicitly close fedora flysystem connections (#852) 3 years ago
Rosie Le Faive e9448b0b00 Continue branch renaming. 3 years ago
Rosie Le Faive 4b9493210e Start fixing branches to semver. 3 years ago
Jared Whiklo 2923a1a8b9
Add missing use statement (#851) 3 years ago
Adam 05c0d1cc58
Make messages persistent by default. (#840) 3 years ago
Adam d800748653
Event-based STOMP header generation (#839) 3 years ago
Seth Shaw 588760b57d
Issues 1888 & 1886: fix broken islandora_core_feature_update_8001 (#850) 3 years ago
Rosie Le Faive 9ada5d678f
Add day as valid interval for JWT (#848) 3 years ago
Rosie Le Faive aa94c9afa5
Add "Documentation" section to pull request template (#846) 3 years ago
dannylamb 714d9b632f
Preparing for next deveopment iteration 3 years ago
dannylamb b53ba8e62c
Setting 2.0.0 dependency 3 years ago
Nigel Banks 0c0dd67334
Allow page view derived Advanced Search blocks on any page (#843) 3 years ago
Willow Gillingham 4a20d4b5e4
Islandora tokens 1171 pt2 (#845) 3 years ago
Alan Stanley d291c628bd
Merge pull request #842 from asulibraries/audio_notracks_hotfix_1871 3 years ago
dannylamb 75215caabb
Less gemini (#841) 3 years ago
Willow Gillingham 52033e8788 check that there are audio textTracks before trying to use them for #1871 3 years ago
dannylamb 85dae5c9d8
Reverting test for packagist auto-updating 3 years ago
dannylamb daebe15c59
Update README.md 3 years ago
dannylamb 77bf39b2bc
Update README.md 3 years ago
dannylamb 4918284296
Update README.md 3 years ago
Willow Gillingham 8a28ea28e2
added tokens to handle islandora > media operation (#836) 3 years ago
Alexander O'Neill 492be9fdef
Issue #1848 EmitFileEvent.php: Call to undefined function getFileUri() (#838) 3 years ago
Alexander O'Neill cf82e8f392
In media migrate, error "Call to function getMimeType() on null" (1825) (#837) 3 years ago
dannylamb 5127e7f3ab
Upload form (#754) 3 years ago
dannylamb 07178e9f6f
Mjordan issue 1521 redo (#835) 3 years ago
Eli Zoller 5bc9d04c0d
audio captions part 2 (#830) 3 years ago
Seth Shaw e57c82b888
fix WSOD when media source file is missing (#834) 3 years ago
Seth Shaw 56444554ef
Move rdf namespaces into jsonld config (Issue-1520) (#832) 3 years ago
Eli Zoller e5310bfef6
replace travis badge with github actions badge (#833) 3 years ago
Alan Stanley 6d2ad0ecf4
Merge pull request #824 from nigelgbanks/8.x-1.x-advanced-search 3 years ago
Nigel Banks 5b969790db Fixed issues causing travis build failures 4 years ago
Nigel Banks 84de729285 Added islandora_advanced_search module. 4 years ago
Willow Gillingham d76a6644c9
added custom module dependencies enforced section under install/config for #1707 (#812) 4 years ago
Mark Jordan 47213e2fd0
Merge pull request #829 from seth-shaw-unlv/issue-1771 4 years ago
Seth Shaw 4a0a47c802 drop renamed tables *after* we are done copying 4 years ago
Seth Shaw a8f8e40371 Batch the update 4 years ago
Seth Shaw a52617e99d Rename, recreate, and reload tables instead of using temporary files. 4 years ago
Eli Zoller bdf99377e1
captions v2 (#826) 4 years ago
Seth Shaw f0d0d909a4 Use temporary files for update and schema::changeField() 4 years ago
Seth Shaw 781d0c4e3e coding style 4 years ago
Seth Shaw 2bed2bceb4 Increase size of media.field_file_size. 4 years ago
dannylamb 1b6bea7902
Removing gemini (#822) 4 years ago
Jordan Dukart cb52ddd902
Merge pull request #825 from asulibraries/filesystem_fix 4 years ago
elizoller 71cbda2a9d update drupal core to 9.1.5 4 years ago
elizoller 301e1a0cc2 remove unreferenced class 4 years ago
elizoller a36629890b fix for filesysystem interface 4 years ago
Nigel Banks cfa48a9db7
Add null check prior to using object. (#791) 4 years ago
Eli Zoller 986a3d4ae9
Migrate fix (#818) 4 years ago
Nigel Banks ae2aeccfbb
Adding default value for URI for node_has_term condition 4 years ago
Eli Zoller 5e3233f914
multifile media d9 fixes (#820) 4 years ago
Jared Whiklo 76fea69d34
Switch Travis to Github Actions (#823) 4 years ago
Eli Zoller 7f0d54e1eb
D9 islandora (#809) 4 years ago
Alan Stanley 792b3d3ae2
Merge pull request #816 from Islandora/fix-multifile-media 4 years ago
Daniel Aitken e57b9e709a
manually implementing postsave (#815) 4 years ago
dannylamb 9ff811a3b1 Coding standards 4 years ago
dannylamb a1958113eb Refactoring multi-file media to create files like we do for other derivatives 4 years ago
Mark Jordan 2a2fbe7f3f
Merge pull request #808 from seth-shaw-unlv/issue-1694 4 years ago
Noah W. Smith 17711f4c24
Add "islandora_models" terms for Compound and Newspaper (#810) 4 years ago
Seth Shaw 1b3338e47e
hide node_referenced_by_node in block placement UI 4 years ago
Seth Shaw 69326432ec
coding style 4 years ago
Daniel Aitken 6d11796be8
getFilename as function instead of property (#814) 4 years ago
Seth Shaw 4b1103e05f
Update and rename NodeReferencedByField.php to NodeReferencedByNode.php 4 years ago
Seth Shaw 7de7a08ac1 node_referenced_by_field condition plugin 4 years ago
Nigel Banks 836d521273
Fix for media_source_mimetype to allow entities to display when not specified. (#803) 4 years ago
dannylamb 4f2c58e4ad
Use islandora_ci for scripts (#806) 4 years ago
Jared Whiklo 174cd0f0c9
Switch to using PHPUnit directly and splitting into testsuites. (#805) 4 years ago
dannylamb d7fb47add4
Use continue 2 in switch inside for loop (for php 7.3) (#802) 4 years ago
Rosie Le Faive ca73b271fd
Spaces needed in error messages. (#801) 4 years ago
Melissa Anez 7095680382
Fix installation documentation link in README.md (#798) 4 years ago
Alan Stanley a3d7a55bdf
coder_fixes (#797) 4 years ago
Alan Stanley ad05b37d08
Media multifile (#756) 4 years ago
Jordan Dukart 1be998fd20
Merge pull request #795 from seth-shaw-unlv/issue-1367 4 years ago
Seth Shaw 4bb18d8855
remove travis debugging 4 years ago
Seth Shaw 54ff6e0566
debug travis build failure 4 years ago
Jordan Dukart 047d62a53f
Merge pull request #796 from rosiel/arcane-backend-process 4 years ago
Rosie Le Faive 26f8cfb7ef Remove swp files. 4 years ago
Rosie Le Faive fca4e244aa Actually add documentation. 4 years ago
Rosie Le Faive 5ade4bf5dc Use new title in test. Document that strings allowed. 4 years ago
Rosie Le Faive 925f1520c9
Improve wording of jsonld type source field. 4 years ago
Rosie Le Faive a37eeea138 Explain what this means. 4 years ago
Seth Shaw c4c602a9c9 add deleting node to test 4 years ago
Seth Shaw 3d2eb69736 remove requirement for a value in the configured reference field (for collections) 4 years ago
Mark Jordan 173483ef52
Merge pull request #793 from asulibraries/version_files 4 years ago
elizoller df6e70e69e use existing has function 4 years ago
elizoller dea2d6dda0 fix phpcbf 4 years ago
elizoller 81c1ccc09c update composer.json to include the file_replace module 4 years ago
Mark Jordan d5f88d6602
Work on #1491. (#783) 4 years ago
elizoller 316917a12c improve header check in the case that it fails and update tests since they now make another fedora call 4 years ago
Melissa Anez 76f30ab3df
Update CONTRIBUTING.md (#794) 4 years ago
elizoller d1d15fe993 more cleanup of logging 4 years ago
elizoller 219925831d clean up logger 4 years ago
elizoller ea1383e499 add dependency on file_replace 4 years ago
Alan Stanley 3f58dfd583
Merge pull request #792 from qadan/delete-confirm 4 years ago
Eli Zoller 9a16c4373b php cleanup 4 years ago
Eli Zoller c1d719bc02 merge 8.x-1.x 4 years ago
qadan 0719944549 dont mind me just sprinkling semicolons everywhere 4 years ago
qadan 65bea6b0e7 update tests 4 years ago
qadan c33503e02c should probably include the heckin form 4 years ago
qadan 1cccac33ef permission doesnt exist 4 years ago
qadan 599ff072d3 move to a confirmation action form 4 years ago
Adam bfa2c39f1f
Skip link headers for non-canonicalizable entities (#781) 4 years ago
Don Richards ef98cdaee7
Merge pull request #787 from dannylamb/iiif-manifest-fix-no-cantaloupe 4 years ago
dannylamb 0097808bdd
Removing isladora_version_count table (#788) 4 years ago
dannylamb 8ff6112045 coding standards and removing dsm 4 years ago
dannylamb 22134df96f Handling case where IIIF manifest generation fails if cantaloupe is down 4 years ago
Seth Shaw bfab343d32
verify entity before walking breadcrumb (#786) 4 years ago
J Hunt 75ae02b65a
#1560 Update JWT to 8.x-1.0-beta5 (#785) 4 years ago
J Hunt 2ce358484b
#1544 Update Migrate Plus to 8.x-5.1, Migrate Tools to 8.x-5.0 (#784) 4 years ago
J Hunt a51db93ec1
#1534 Change config permission to 'administer site configuration' (#780) 4 years ago
Jared Whiklo bbcd28ccb5
Validate JWT expiry (#776) 4 years ago
Jordan Dukart bc6e5a3f27
Add configuration for a user and password to the broker. (#779) 4 years ago
J Hunt 057bc5cc3a
#1497 Update drupal/jwt to 8.x-1.0-beta2 (#775) 4 years ago
Eli Zoller 9b6a701a4a version a replaced file 5 years ago
  1. 10
      .github/PULL_REQUEST_TEMPLATE.md
  2. 125
      .github/workflows/build-2.x.yml
  3. 26
      .github/workflows/gitlab-mirror.yml
  4. 1
      .gitignore
  5. 53
      .travis.yml
  6. 4
      CONTRIBUTING.md
  7. 42
      README.md
  8. 34
      composer.json
  9. 2
      config/install/islandora.settings.yml
  10. 97
      config/schema/islandora.schema.yml
  11. 3
      css/islandora.css
  12. 1
      drush.services.yml
  13. 52
      islandora.info.yml
  14. 210
      islandora.install
  15. 5
      islandora.libraries.yml
  16. 16
      islandora.links.action.yml
  17. 7
      islandora.links.menu.yml
  18. 418
      islandora.module
  19. 16
      islandora.post_update.php
  20. 52
      islandora.routing.yml
  21. 37
      islandora.services.yml
  22. 192
      islandora.tokens.inc
  23. 51
      islandora.views.inc
  24. 2
      migrate/tags.csv
  25. 73
      modules/islandora_advanced_search/CONTRIBUTING.md
  26. 339
      modules/islandora_advanced_search/LICENSE
  27. 261
      modules/islandora_advanced_search/README.md
  28. 37
      modules/islandora_advanced_search/css/islandora_advanced_search.form.css
  29. 111
      modules/islandora_advanced_search/css/islandora_advanced_search.pager.css
  30. BIN
      modules/islandora_advanced_search/docs/advanced_search_block_settings.png
  31. BIN
      modules/islandora_advanced_search/docs/basic-input.png
  32. BIN
      modules/islandora_advanced_search/docs/contextual_filter_settings.png
  33. BIN
      modules/islandora_advanced_search/docs/demo.gif
  34. BIN
      modules/islandora_advanced_search/docs/enable_index_hierarchy.png
  35. BIN
      modules/islandora_advanced_search/docs/enable_index_hierarchy_processor.png
  36. BIN
      modules/islandora_advanced_search/docs/exclude_facet_settings_exclude.png
  37. BIN
      modules/islandora_advanced_search/docs/exclude_facet_settings_url_alias.png
  38. BIN
      modules/islandora_advanced_search/docs/facet_block_settings.png
  39. BIN
      modules/islandora_advanced_search/docs/field_decedent_of.png
  40. BIN
      modules/islandora_advanced_search/docs/include_exclude_facets.png
  41. BIN
      modules/islandora_advanced_search/docs/include_exclude_facets_settings.png
  42. BIN
      modules/islandora_advanced_search/docs/islandora_advanced_search_settings.png
  43. BIN
      modules/islandora_advanced_search/docs/pager.png
  44. BIN
      modules/islandora_advanced_search/docs/pager_settings.png
  45. BIN
      modules/islandora_advanced_search/docs/sort_criteria.png
  46. BIN
      modules/islandora_advanced_search/docs/view_advanced_setting.png
  47. 13
      modules/islandora_advanced_search/islandora_advanced_search.info.yml
  48. 17
      modules/islandora_advanced_search/islandora_advanced_search.libraries.yml
  49. 6
      modules/islandora_advanced_search/islandora_advanced_search.links.menu.yml
  50. 89
      modules/islandora_advanced_search/islandora_advanced_search.module
  51. 17
      modules/islandora_advanced_search/islandora_advanced_search.routing.yml
  52. 113
      modules/islandora_advanced_search/js/islandora_advanced_search.admin.js
  53. 124
      modules/islandora_advanced_search/js/islandora_advanced_search.form.js
  54. 254
      modules/islandora_advanced_search/src/AdvancedSearchQuery.php
  55. 294
      modules/islandora_advanced_search/src/AdvancedSearchQueryTerm.php
  56. 165
      modules/islandora_advanced_search/src/Controller/AjaxBlocksController.php
  57. 423
      modules/islandora_advanced_search/src/Form/AdvancedSearchForm.php
  58. 122
      modules/islandora_advanced_search/src/Form/SettingsForm.php
  59. 24
      modules/islandora_advanced_search/src/GetConfigTrait.php
  60. 394
      modules/islandora_advanced_search/src/Plugin/Block/AdvancedSearchBlock.php
  61. 17
      modules/islandora_advanced_search/src/Plugin/Block/AdvancedSearchBlockDeriver.php
  62. 97
      modules/islandora_advanced_search/src/Plugin/Block/SearchApiDisplayBlockDeriver.php
  63. 314
      modules/islandora_advanced_search/src/Plugin/Block/SearchResultsPagerBlock.php
  64. 17
      modules/islandora_advanced_search/src/Plugin/Block/SearchResultsPagerBlockDeriver.php
  65. 30
      modules/islandora_advanced_search/src/Plugin/Block/ViewAndDisplayIdentifiersTrait.php
  66. 63
      modules/islandora_advanced_search/src/Plugin/Field/FieldFormatter/EntityReferenceCountFormatter.php
  67. 84
      modules/islandora_advanced_search/src/Plugin/facets/widget/IncludeExcludeLinksWidget.php
  68. 42
      modules/islandora_advanced_search/src/Plugin/facets_summary/processor/ResetRemovePage.php
  69. 47
      modules/islandora_advanced_search/src/Plugin/facets_summary/processor/ShowActiveExcludedFacets.php
  70. 89
      modules/islandora_advanced_search/src/Plugin/facets_summary/processor/ShowActiveFacets.php
  71. 67
      modules/islandora_advanced_search/src/Plugin/facets_summary/processor/ShowFacetsTrait.php
  72. 47
      modules/islandora_advanced_search/src/Plugin/facets_summary/processor/ShowMissingFacets.php
  73. 101
      modules/islandora_advanced_search/src/Plugin/facets_summary/processor/ShowSearchQueryProcessor.php
  74. 63
      modules/islandora_advanced_search/src/Utilities.php
  75. 58
      modules/islandora_advanced_search/templates/facets/facets-item-list--include-exclude-links.html.twig
  76. 33
      modules/islandora_advanced_search/templates/facets/facets-result-item--include-exclude-links.html.twig
  77. 20
      modules/islandora_advanced_search/templates/facets/facets-result-item--summary.html.twig
  78. 6
      modules/islandora_audio/CONTRIBUTING.md
  79. 4
      modules/islandora_audio/README.md
  80. 3
      modules/islandora_audio/config/schema/islandora_audio.schema.yml
  81. 4
      modules/islandora_audio/islandora_audio.info.yml
  82. 7
      modules/islandora_audio/islandora_audio.libraries.yml
  83. 15
      modules/islandora_audio/islandora_audio.module
  84. 47
      modules/islandora_audio/js/audio.js
  85. 6
      modules/islandora_audio/src/Plugin/Action/GenerateAudioDerivative.php
  86. 28
      modules/islandora_audio/src/Plugin/Field/FieldFormatter/IslandoraFileAudioFormatter.php
  87. 31
      modules/islandora_audio/templates/islandora-file-audio.html.twig
  88. 9
      modules/islandora_audio/tests/src/Functional/GenerateAudioDerivativeTest.php
  89. 6
      modules/islandora_breadcrumbs/CONTRIBUTING.md
  90. 3
      modules/islandora_breadcrumbs/config/install/islandora.breadcrumbs.yml
  91. 4
      modules/islandora_breadcrumbs/config/install/islandora_breadcrumbs.breadcrumbs.yml
  92. 11
      modules/islandora_breadcrumbs/config/schema/islandora_breadcrumbs.schema.yml
  93. 4
      modules/islandora_breadcrumbs/islandora_breadcrumbs.info.yml
  94. 18
      modules/islandora_breadcrumbs/islandora_breadcrumbs.install
  95. 5
      modules/islandora_breadcrumbs/islandora_breadcrumbs.links.menu.yml
  96. 7
      modules/islandora_breadcrumbs/islandora_breadcrumbs.routing.yml
  97. 2
      modules/islandora_breadcrumbs/islandora_breadcrumbs.services.yml
  98. 132
      modules/islandora_breadcrumbs/src/Form/IslandoraBreadcrumbsSettingsForm.php
  99. 70
      modules/islandora_breadcrumbs/src/IslandoraBreadcrumbBuilder.php
  100. 16
      modules/islandora_breadcrumbs/tests/src/Functional/BreadcrumbsTest.php
  101. Some files were not shown because too many files have changed in this diff Show More

10
.github/PULL_REQUEST_TEMPLATE.md

@ -15,7 +15,6 @@ A in-depth description of the changes made by this PR. Technical details and
* Changes x feature to such that y
* Added x
* Removed y
* Does this change require documentation to be updated?
* Does this change add any new dependencies?
* Does this change require any other modifications to be made to the repository
(i.e. Regeneration activity, etc.)?
@ -29,9 +28,16 @@ A description of what steps someone could take to:
* Please be as detailed as possible.
* Good testing instructions help get your PR completed faster.
# Documentation Status
* Does this change existing behaviour that's currently documented?
* Does this change require new pages or sections of documentation?
* Who does this need to be documented for?
* Associated documentation pull request(s): ___ or documentation issue ___
# Additional Notes:
Any additional information that you think would be helpful when reviewing this
PR.
# Interested parties
Tag (@ mention) interested parties or, if unsure, @Islandora/8-x-committers
Tag (@ mention) interested parties or, if unsure, @Islandora/committers

125
.github/workflows/build-2.x.yml

@ -0,0 +1,125 @@
name: CI
on:
push:
branches: [ 2.x ]
pull_request:
branches: [ 2.x ]
workflow_dispatch:
jobs:
build:
env:
DRUPAL_VERSION: ${{ matrix.drupal-version }}
SCRIPT_DIR: ${{ github.workspace }}/islandora_ci
DRUPAL_DIR: /opt/drupal
PHPUNIT_FILE: ${{ github.workspace }}/build_dir/phpunit.xml
runs-on: ubuntu-latest
continue-on-error: ${{ matrix.allowed_failure }}
strategy:
fail-fast: false
matrix:
php-versions: ["8.1", "8.2", "8.3"]
test-suite: ["kernel", "functional", "functional-javascript"]
drupal-version: ["10.1.x", "10.2.x", "10.3.x-dev"]
mysql: ["8.0"]
allowed_failure: [false]
exclude:
- php-versions: "8.3"
drupal-version: "10.1.x"
name: PHP ${{ matrix.php-versions }} | drupal ${{ matrix.drupal-version }} | mysql ${{ matrix.mysql }} | test-suite ${{ matrix.test-suite }}
services:
mysql:
image: mysql:${{ matrix.mysql }}
env:
MYSQL_ALLOW_EMPTY_PASSWORD: yes
MYSQL_DATABASE: drupal
ports:
- 3306:3306
activemq:
image: webcenter/activemq:5.14.3
ports:
- 8161:8161
- 61616:61616
- 61613:61613
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
path: build_dir
- name: Checkout islandora_ci
uses: actions/checkout@v4
with:
repository: islandora/islandora_ci
ref: github-actions
path: islandora_ci
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-versions }}
tools: composer:v2
- name: Setup Mysql client
run: |
sudo apt-get update
sudo apt-get remove -y mysql-client mysql-common
sudo apt-get install -y mysql-client
- name: Cache Composer dependencies
uses: actions/cache@v3
with:
path: /tmp/composer-cache
key: ${{ runner.os }}-${{ hashFiles('**/composer.lock') }}
- name: Setup Drupal
run: |
mkdir $DRUPAL_DIR
$SCRIPT_DIR/travis_setup_drupal.sh
cd $DRUPAL_DIR
chmod -R u+w web/sites/default
mkdir -p web/sites/simpletest/browser_output
- name: Setup composer paths
run: |
git -C "$GITHUB_WORKSPACE/build_dir" checkout -b github-testing
cd $DRUPAL_DIR
composer config repositories.local path "$GITHUB_WORKSPACE/build_dir"
composer config minimum-stability dev
composer require "islandora/islandora:dev-github-testing as dev-2.x"
- name: Install modules
run: |
cd $DRUPAL_DIR/web
drush --uri=127.0.0.1:8282 en -y islandora_audio islandora_breadcrumbs islandora_iiif islandora_image islandora_video islandora_text_extraction_defaults
- name: Copy PHPunit file
run: cp $PHPUNIT_FILE $DRUPAL_DIR/web/core/phpunit.xml
- name: Test scripts
run: $SCRIPT_DIR/travis_scripts.sh
- name: Start chromedriver
if: matrix.test-suite == 'functional-javascript'
run: |-
/usr/local/share/chromedriver-linux64/chromedriver \
--log-path=/tmp/chromedriver.log \
--verbose \
--allowed-ips= \
--allowed-origins=* &
- name: PHPUNIT tests
run: |
cd $DRUPAL_DIR/web/core
$DRUPAL_DIR/vendor/bin/phpunit --verbose --testsuite "${{ matrix.test-suite }}"
- name: Print chromedriver logs
if: matrix.test-suite == 'functional-javascript'
run: cat /tmp/chromedriver.log

26
.github/workflows/gitlab-mirror.yml

@ -0,0 +1,26 @@
name: Mirror and run GitLab CI
on:
push:
branches: [2.x]
tags: '*'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Mirror + trigger CI
uses: SvanBoxel/gitlab-mirror-and-ci-action@master
with:
args: "https://git.drupalcode.org/project/islandora"
env:
FOLLOW_TAGS: "true"
FORCE_PUSH: "false"
GITLAB_HOSTNAME: "git.drupal.org"
GITLAB_USERNAME: "project_34868_bot"
GITLAB_PASSWORD: ${{ secrets.GITLAB_PASSWORD }}
GITLAB_PROJECT_ID: "34868"
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

1
.gitignore vendored

@ -1,2 +1,3 @@
vendor
/.idea/
/.vscode

53
.travis.yml

@ -1,53 +0,0 @@
sudo: true
language: php
php:
- 7.2
- 7.3
matrix:
fast_finish: true
allow_failures:
- php: 7.3
services:
- mysql
branches:
only:
- /^8.x/
- /master/
before_install:
- export SCRIPT_DIR=$HOME/islandora/.scripts
- export DRUPAL_DIR=/opt/drupal
- export COMPOSER_PATH="/home/travis/.phpenv/versions/$TRAVIS_PHP_VERSION/bin/composer"
install:
- git clone https://github.com/Islandora/documentation.git $HOME/islandora
- $SCRIPT_DIR/travis_setup_drupal.sh
- git -C "$TRAVIS_BUILD_DIR" checkout -b travis-testing
- cd $DRUPAL_DIR;
- chmod -R u+w web/sites/default
- COMPOSER_MEMORY_LIMIT=-1 php -d memory_limit=-1 $COMPOSER_PATH config repositories.local path "$TRAVIS_BUILD_DIR"
- COMPOSER_MEMORY_LIMIT=-1 php -d memory_limit=-1 $COMPOSER_PATH require "islandora/islandora:dev-travis-testing as dev-8.x-1.x" --prefer-source --update-with-dependencies
- cd web
- drush --uri=127.0.0.1:8282 en -y islandora_audio islandora_breadcrumbs islandora_iiif islandora_image islandora_video islandora_text_extraction_defaults
- drush --uri=127.0.0.1:8282 fim -y islandora_core_feature,islandora_text_extraction_defaults
script:
- $SCRIPT_DIR/travis_scripts.sh
- $SCRIPT_DIR/run-tests.sh "islandora"
- $SCRIPT_DIR/run-tests.sh "islandora_breadcrumbs"
- $SCRIPT_DIR/run-tests.sh "islandora_image"
- $SCRIPT_DIR/run-tests.sh "islandora_audio"
- $SCRIPT_DIR/run-tests.sh "islandora_video"
- $SCRIPT_DIR/run-tests.sh "islandora_text_extraction"
after_success:
- bash <(curl -s https://codecov.io/bash)
notifications:
slack:
on_success: change
on_failure: always
secure: $SLACK_NOTIFICATION_KEY

4
CONTRIBUTING.md

@ -8,7 +8,7 @@ Please note that this project operates under the [Islandora Community Code of Co
## Workflows
The group meets each Wednesday at 1:00 PM Eastern. Meeting notes and announcements are posted to the [Islandora community list](https://groups.google.com/forum/#!forum/islandora) and the [Islandora developers list](https://groups.google.com/forum/#!forum/islandora-dev). You can view meeting agendas, notes, and call-in information [here](https://github.com/Islandora/documentation/wiki#islandora-8-tech-calls). Anybody is welcome to join the calls, and add items to the agenda.
The Islandora 8 Tech Call meets each Wednesday at 1:00 PM Eastern, and the Islandora 8 User Call meets every second Thursday at 1:00 PM Eastern. Meeting notes and announcements are posted to the [Islandora community list](https://groups.google.com/forum/#!forum/islandora) and the [Islandora developers list](https://groups.google.com/forum/#!forum/islandora-dev). You can view meeting agendas, notes, and call-in information [here](https://github.com/Islandora/documentation/wiki#islandora-8-tech-calls). Anybody is welcome to join the calls, and add items to the agenda.
### Use cases
@ -62,7 +62,7 @@ Contributions to the Islandora codebase should be sent as GitHub pull requests.
Take a look at [Creating a pull request](https://help.github.com/articles/creating-a-pull-request). In a nutshell you need to:
1. [Fork](https://help.github.com/articles/fork-a-repo) this repository to your personal or institutional GitHub account (depending on the CLA you are working under). Be cautious of which branches you work from though (you'll want to base your work off master, or for Drupal modules use the most recent version branch). See [Fork a repo](https://help.github.com/articles/fork-a-repo) for detailed instructions.
1. [Fork](https://help.github.com/articles/fork-a-repo) this repository to your personal or institutional GitHub account (depending on the CLA you are working under). Be cautious of which branches you work from though (you'll want to base your work off the default branch). See [Fork a repo](https://help.github.com/articles/fork-a-repo) for detailed instructions.
2. Commit any changes to your fork.
3. Send a [pull request](https://help.github.com/articles/creating-a-pull-request) using the [pull request template](https://github.com/Islandora/documentation/blob/master/.github/PULL_REQUEST_TEMPLATE.md) to the Islandora GitHub repository that you forked in step 1. If your pull request is related to an existing issue -- for instance, because you reported a [bug/issue](https://github.com/Islandora/documentation/issues) earlier -- prefix the title of your pull request with the corresponding issue number (e.g. `issue-123: ...`). Please also include a reference to the issue in the description of the pull. This can be done by using '#' plus the issue number like so '#123', also try to pick an appropriate name for the branch in which you're issuing the pull request from.

42
README.md

@ -1,10 +1,10 @@
# ![Islandora](https://cloud.githubusercontent.com/assets/2371345/25624809/f95b0972-2f30-11e7-8992-a8f135402cdc.png) Islandora
# Islandora
[![Minimum PHP Version](https://img.shields.io/badge/php-%3E%3D%207.2-8892BF.svg?style=flat-square)](https://php.net/)
[![Build Status](https://travis-ci.org/Islandora/islandora.png?branch=8.x-1.x)](https://travis-ci.com/Islandora/islandora)
[![Minimum PHP Version](https://img.shields.io/badge/php-%3E%3D%207.4-8892BF.svg?style=flat-square)](https://php.net/)
[![Build Status](https://github.com/islandora/islandora/actions/workflows/build-2.x.yml/badge.svg)](https://github.com/Islandora/islandora/actions)
[![Contribution Guidelines](http://img.shields.io/badge/CONTRIBUTING-Guidelines-blue.svg)](./CONTRIBUTING.md)
[![LICENSE](https://img.shields.io/badge/license-GPLv2-blue.svg?style=flat-square)](./LICENSE)
[![codecov](https://codecov.io/gh/Islandora/islandora/branch/8.x-1.x/graph/badge.svg)](https://codecov.io/gh/Islandora/islandora)
[![codecov](https://codecov.io/gh/Islandora/islandora/branch/2.x/graph/badge.svg)](https://codecov.io/gh/Islandora/islandora)
## Introduction
@ -40,7 +40,6 @@ Installing via composer will download all required libraries and modules. Howev
- [eva](http://drupal.org/project/eva)
- [features](http://drupal.org/project/features)
- [migrate_plus](http://drupal.org/project/migrate_plus)
- [migrate_tools](http://drupal.org/project/migrate_tools)
- [migrate_source_csv](http://drupal.org/project/migrate_source_csv)
- [flysystem](http://drupal.org/project/flysystem)
@ -49,9 +48,11 @@ It also requires the following PHP libraries:
- [Crayfish Commons](https://packagist.org/packages/islandora/crayfish-commons)
- [Stomp PHP](http://drupal.org/project/)
If you are using a Drush version less than 10.4 you will also need to install and enable [migrate_tools](http://drupal.org/project/migrate_tools) separately.
## Installation
For a full digital repository solution, see our [installation documentation](https://islandora.github.io/documentation/installation/).
For a full digital repository solution, see our [installation documentation](https://islandora.github.io/documentation/installation/component_overview/).
To download/enable just this module, use the following from the command line:
@ -90,30 +91,31 @@ Having problems or solved a problem? Check out the Islandora google groups for a
Current maintainers:
* [Danny Lamb](https://github.com/dannylamb)
* [Islandora Technical Advisory Group](https://github.com/Islandora/islandora-community/wiki/Technical-Advisory-Group-%28TAG%29)
## Sponsors
* UPEI
* discoverygarden inc.
* LYRASIS
* McMaster University
* University of Limerick
* York University
* University of Manitoba
* Simon Fraser University
* PALS
* American Philosophical Society
* Common Media Inc.
* [American Philosophical Society](https://www.amphilsoc.org/)
* [Born-Digital, Inc.](https://www.born-digital.com/)
* [discoverygarden inc.](https://www.discoverygarden.ca/)
* [LYRASIS](https://www.lyrasis.org/)
* [McMaster University](https://www.mcmaster.ca/)
* [PALS](https://www.mnpals.org/)
* [University of Limerick](https://www.ul.ie/)
* [University of Manitoba](https://umanitoba.ca/)
* [UPEI](https://www.upei.ca/)
* [Simon Fraser University](https://www.sfu.ca/)
* [York University](https://www.yorku.ca/)
## Development
If you would like to contribute, please get involved by attending our weekly [Tech Call](https://github.com/Islandora/documentation/wiki). We love to hear from you!
If you would like to contribute, please get involved by attending our weekly [Tech Call](https://github.com/Islandora/islandora-community/wiki/Weekly-Open-Tech-Call). We love to hear from you!
If you would like to contribute code to the project, you need to be covered by an Islandora Foundation [Contributor License Agreement](http://islandora.ca/sites/default/files/islandora_cla.pdf) or [Corporate Contributor License Agreement](http://islandora.ca/sites/default/files/islandora_ccla.pdf). Please see the [Contributors](http://islandora.ca/resources/contributors) pages on Islandora.ca for more information.
If you would like to contribute code to the project, you need to be covered by an Islandora Foundation [Contributor License Agreement](https://github.com/Islandora/islandora-community/wiki/Onboarding-Checklist#contributor-license-agreements) or [Corporate Contributor License Agreement](https://github.com/Islandora/islandora-community/wiki/Onboarding-Checklist#contributor-license-agreements). Please see the [Contributor License Agreements](https://github.com/Islandora/islandora-community/wiki/Contributor-License-Agreements) page on the islandora-community wiki for more information.
We recommend using the [islandora-playbook](https://github.com/Islandora-Devops/islandora-playbook) to get started.
## License
[GPLv2](http://www.gnu.org/licenses/gpl-2.0.txt)

34
composer.json

@ -14,32 +14,34 @@
}
],
"require": {
"drupal/context": "^4.0",
"drupal/search_api": "~1.8",
"islandora/jsonld": "dev-8.x-1.x",
"stomp-php/stomp-php": "4.*",
"drupal/jwt": "1.0.0-beta1",
"drupal/filehash": "^1.1",
"drupal/context": "^4 || ^5@RC",
"drupal/ctools": "^3.8 || ^4",
"drupal/eva" : "^3.0",
"drupal/file_replace": "^1.1",
"drupal/filehash": "^2 || ^3",
"drupal/flysystem" : "^2.0@alpha",
"drupal/jwt": "^1.1 || ^2",
"drupal/migrate_plus" : "^5.1 || ^6",
"drupal/migrate_source_csv" : "^3.4",
"drupal/prepopulate" : "^2.2",
"drupal/eva" : "^2.0",
"drupal/features" : "^3.7",
"drupal/migrate_plus" : "^4.1",
"drupal/migrate_tools" : "^4.1",
"drupal/migrate_source_csv" : "^2.1",
"drupal/search_api": "^1.8",
"drupal/token" : "^1.3",
"drupal/flysystem" : "^1.0",
"islandora/crayfish-commons": "dev-dev"
"islandora/chullo": "^2.0",
"islandora/fedora-entity-mapper": "^1.0",
"islandora/jsonld": "^2 || ^3",
"stomp-php/stomp-php": "4.* || ^5"
},
"require-dev": {
"phpunit/phpunit": "^6",
"squizlabs/php_codesniffer": "2.7.1",
"squizlabs/php_codesniffer": "^2.7.1",
"drupal/coder": "*",
"sebastian/phpcpd": "*"
},
"suggest": {
"drupal/transliterate_filenames": "Sanitizes filenames when they are uploaded so they don't break your repository."
"drupal/transliterate_filenames": "Sanitizes filenames when they are uploaded so they don't break your repository.",
"drupal/coi": "Some configuration fields work with Config Override Inspector."
},
"license": "GPL-2.0+",
"license": "GPL-2.0-or-later",
"authors": [
{
"name": "Islandora Foundation",

2
config/install/islandora.settings.yml

@ -1,4 +1,4 @@
broker_url: 'tcp://localhost:61613'
jwt_expiry: '+2 hour'
gemini_url: ''
delete_media_and_files: TRUE
gemini_pseudo_bundles: []

97
config/schema/islandora.schema.yml

@ -14,9 +14,18 @@ islandora.settings:
jwt_expiry:
type: string
label: 'How long JWTs should last before expiring.'
gemini_url:
type: uri
label: 'Url to Gemini microservice'
delete_media_and_files:
type: boolean
label: 'Node Delete with Media and Files'
redirect_after_media_save:
type: boolean
label: 'Redirect to node after media save.'
upload_form_location:
type: string
label: 'Upload Form Location'
upload_form_allowed_mimetypes:
type: string
label: 'Upload Form Allowed Extensions'
gemini_pseudo_bundles:
type: sequence
label: 'List of node, media and taxonomy terms that should include the linked Fedora URI'
@ -81,6 +90,14 @@ condition.plugin.node_has_term:
logic:
type: string
label: 'Logic (AND or OR)'
tids:
type: sequence
sequence:
type: mapping
mapping:
target_id:
type: integer
label: The target taxonomy term IDs
condition.plugin.node_has_parent:
type: condition.plugin
@ -154,12 +171,74 @@ condition.plugin.node_had_namespace:
label: 'PID field'
field.formatter.settings.islandora_image:
type: mapping
label: 'Image field display format settings'
type: field.formatter.settings.image
label: 'Islandora image field display format settings'
condition.plugin.islandora_entity_bundle:
type: condition.plugin
mapping:
bundles:
type: sequence
sequence:
type: string
condition.plugin.media_source_mimetype:
type: condition.plugin
mapping:
mimetype:
type: string
reaction.plugin.alter_jsonld_type:
type: islandora.reaction_plugin_with_saved
mapping:
source_field:
type: string
islandora.reaction_plugin_with_saved:
type: reaction.plugin
mapping:
saved:
type: boolean
label: Default config upstream; however, left undefined in the schema.
reaction.plugin.islandora_map_uri_predicate:
type: islandora.reaction_plugin_with_saved
mapping:
image_link:
drupal_uri_predicate:
type: string
label: 'Link image to'
image_style:
reaction.plugin.view_mode_alter:
type: islandora.reaction_plugin_with_saved
mapping:
mode:
type: string
label: The view mode to which to switch
islandora.reaction.actions:
type: islandora.reaction_plugin_with_saved
mapping:
actions:
type: sequence
sequence:
type: string
reaction.plugin.index:
type: islandora.reaction.actions
reaction.plugin.delete:
type: islandora.reaction.actions
reaction.plugin.derivative:
type: islandora.reaction.actions
field.widget.settings.media_track:
type: field.widget.settings.file_generic
field.field_settings.media_track:
type: field.field_settings.file
mapping:
languages:
type: string
label: 'Image style'
field.storage_settings.media_track:
type: field.storage_settings.file

3
css/islandora.css

@ -0,0 +1,3 @@
.container .islandora-media-items {
margin: 0;
}

1
drush.services.yml

@ -1,5 +1,6 @@
services:
islandora.commands:
class: \Drupal\islandora\Commands\IslandoraCommands
arguments: ['@entity_type.manager', '@current_user', '@account_switcher']
tags:
- { name: drush.command }

52
islandora.info.yml

@ -4,30 +4,30 @@ name: 'islandora'
description: "Islandora Core"
type: module
package: Islandora
core: 8.x
core_version_requirement: ^9 || ^10
dependencies:
- block
- node
- path
- text
- options
- link
- jsonld
- search_api
- jwt
- rest
- filehash
- basic_auth
- context_ui
- action
- eva
- taxonomy
- views_ui
- media
- prepopulate
- features_ui
- migrate_tools
- migrate_source_csv
- content_translation
- flysystem
- token
- context:context_ui
- ctools:ctools
- drupal:action
- drupal:basic_auth
- drupal:block
- drupal:content_translation
- drupal:link
- drupal:media
- drupal:node
- drupal:options
- drupal:path
- drupal:rest
- drupal:taxonomy
- drupal:text
- drupal:views_ui
- eva:eva
- file_replace:file_replace
- filehash:filehash
- flysystem:flysystem
- jsonld:jsonld
- jwt:jwt
- migrate_source_csv:migrate_source_csv
- prepopulate:prepopulate
- search_api:search_api
- token:token

210
islandora.install

@ -5,40 +5,15 @@
* Install/update hook implementations.
*/
use Drupal\Core\Extension\ExtensionNameLengthException;
use Drupal\Core\Extension\MissingDependencyException;
use Drupal\Core\Utility\UpdateException;
/**
* Implements hook_schema().
* Adds common namespaces to jsonld.settings.
*/
function islandora_schema() {
$schema = [];
$schema['islandora_version_count'] = [
'description' => 'Keeps track of the number of changes to an entity',
'fields' => [
'id' => [
'description' => 'Autoincrementing id for record',
'type' => 'serial',
'unsigned' => TRUE,
'not null' => TRUE,
],
'uuid' => [
'description' => 'UUID for an entity',
'type' => 'varchar',
'length' => 128,
'not null' => TRUE,
'unique' => TRUE,
],
'count' => [
'description' => 'Number of times an entity has been updated.',
'type' => 'int',
'unsigned' => TRUE,
'default' => 0,
],
],
'primary key' => ['id'],
'unique keys' => [
'uuid' => ['uuid'],
],
];
return $schema;
function islandora_install() {
update_jsonld_included_namespaces();
}
/**
@ -80,3 +55,174 @@ function islandora_update_8002(&$sandbox) {
// Force drupal to reload the config.
\Drupal::service('plugin.manager.condition')->clearCachedDefinitions();
}
/**
* Deletes the islandora_version_count table.
*
* We never implemented the functionality.
*/
function islandora_update_8003(&$sandbox) {
\Drupal::service('database')
->schema()
->dropTable('islandora_version_count');
}
/**
* Renames migration source keys -> ids.
*/
function islandora_update_8004() {
$config_factory = \Drupal::configFactory();
$config = $config_factory->getEditable('migrate_plus.migration.islandora__tags');
if ($config) {
if (!$config->get('source.ids')) {
$config->set('source.ids', $config->get('source.keys'));
$config->clear('source.keys');
$config->save(TRUE);
}
}
}
/**
* Makes migrate_tags an array.
*/
function islandora_update_8005() {
$config_factory = \Drupal::configFactory();
$config_factory->getEditable('migrate_plus.migration.islandora__tags')->delete();
$config = $config_factory->getEditable('migrate_plus.migration.islandora_tags');
if ($config) {
if (!is_array($config->get('migration_tags'))) {
$config->set('migration_tags', [$config->get('migration_tags')]);
$config->save(TRUE);
}
if (!$config->get('source.ids')) {
$config->set('source.ids', $config->get('source.keys'));
$config->clear('source.keys');
$config->save(TRUE);
}
}
}
/**
* Adds adds previously hardcoded namespaces to configuration.
*/
function islandora_update_8006() {
update_jsonld_included_namespaces();
}
/**
* Used by install and update_8006 to add namespaces to jsonld.settings.yml.
*/
function update_jsonld_included_namespaces() {
$namespaces = [
[
'prefix' => 'ldp',
'namespace' => 'http://www.w3.org/ns/ldp#',
], [
'prefix' => 'dc11',
'namespace' => 'http://purl.org/dc/elements/1.1/',
], [
'prefix' => 'dcterms',
'namespace' => 'http://purl.org/dc/terms/',
], [
'prefix' => 'nfo',
'namespace' => 'http://www.semanticdesktop.org/ontologies/2007/03/22/nfo/v1.1/',
], [
'prefix' => 'ebucore',
'namespace' => 'http://www.ebu.ch/metadata/ontologies/ebucore/ebucore#',
], [
'prefix' => 'fedora',
'namespace' => 'http://fedora.info/definitions/v4/repository#',
], [
'prefix' => 'owl',
'namespace' => 'http://www.w3.org/2002/07/owl#',
], [
'prefix' => 'ore',
'namespace' => 'http://www.openarchives.org/ore/terms/',
], [
'prefix' => 'rdf',
'namespace' => 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
], [
'prefix' => 'rdau',
'namespace' => 'http://rdaregistry.info/Elements/u/',
], [
'prefix' => 'islandora',
'namespace' => 'http://islandora.ca/',
], [
'prefix' => 'pcdm',
'namespace' => 'http://pcdm.org/models#',
], [
'prefix' => 'use',
'namespace' => 'http://pcdm.org/use#',
], [
'prefix' => 'iana',
'namespace' => 'http://www.iana.org/assignments/relation/',
], [
'prefix' => 'premis',
'namespace' => 'http://www.loc.gov/premis/rdf/v1#',
], [
'prefix' => 'premis3',
'namespace' => 'http://www.loc.gov/premis/rdf/v3/',
], [
'prefix' => 'co',
'namespace' => 'http://purl.org/co/',
],
];
$config = \Drupal::configFactory()->getEditable('jsonld.settings');
if ($config && !is_array($config->get('rdf_namespaces'))) {
$config->set('rdf_namespaces', $namespaces);
$config->save(TRUE);
}
else {
\Drupal::logger('islandora')
->warning("Could not find required jsonld.settings to add default RDF namespaces.");
}
}
/**
* Ensure that ctools is enabled.
*/
function islandora_update_8007() {
$module_handler = \Drupal::moduleHandler();
if ($module_handler->moduleExists('ctools')) {
return t('The "@module_name" module is already enabled, no action necessary.', [
'@module_name' => 'ctools',
]);
}
/** @var \Drupal\Core\Extension\ModuleInstallerInterface $installer */
$installer = \Drupal::service('module_installer');
try {
if ($installer->install(['ctools'], TRUE)) {
return t('The "@module_name" module was enabled successfully.', [
'@module_name' => 'ctools',
]);
}
}
catch (ExtensionNameLengthException | MissingDependencyException $e) {
throw new UpdateException('Failed; ensure that the ctools module is available in the Drupal installation.', 0, $e);
}
catch (\Exception $e) {
throw new UpdateException('Failed; encountered an exception while trying to enable ctools.', 0, $e);
}
// Theoretically impossible to hit, as ModuleInstaller::install() only returns
// TRUE (or throws/propagates an exception), but... probably a good idea to
// have the here, just in case?
throw new UpdateException('Failed; hit the end of the update hook implementation, which is not expected.');
}
/**
* Set config to no redirect after media save.
*/
function islandora_update_8008() {
$config = \Drupal::configFactory()->getEditable('islandora.settings');
if ($config) {
$config->set('redirect_after_media_save', FALSE);
$config->save(TRUE);
return t('A new configuration option, "Redirect after media save" is now available.
It has been turned off to preserve existing behaviour. To enable this setting visit
Configuration > Islandora > Core Settings.');
}
}

5
islandora.libraries.yml

@ -0,0 +1,5 @@
islandora:
version: VERSION
css:
theme:
css/islandora.css: {}

16
islandora.links.action.yml

@ -1,12 +1,24 @@
islandora.upload_media:
route_name: islandora.upload_media
title: Batch Upload Media
appears_on:
- view.media_of.page_1
islandora.add_media_to_node:
route_name: islandora.add_media_to_node_page
title: Add media
title: Add Media
appears_on:
- view.media_of.page_1
islandora.upload_children:
route_name: islandora.upload_children
title: Batch Upload Children
appears_on:
- view.manage_members.page_1
islandora.add_member_to_node:
route_name: islandora.add_member_to_node_page
title: Add child
title: Add Child
appears_on:
- view.manage_members.page_1

7
islandora.links.menu.yml

@ -11,3 +11,10 @@ system.islandora_settings:
parent: system.admin_config_islandora
route_name: system.islandora_settings
description: 'Confgure core Islandora settings'
# RDF property mappings
system.islandora_rdf_mappings:
title: 'Field and term RDF mappings'
parent: system.admin_reports
description: 'List of configured Drupal field to RDF property mappings and taxonomy term linked data URIs.'
route_name: system.islandora_rdf_mappings

418
islandora.module

@ -18,14 +18,17 @@ use Drupal\Component\Plugin\Exception\PluginNotFoundException;
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Site\Settings;
use Drupal\Core\Url;
use Drupal\islandora\Form\IslandoraSettingsForm;
use Drupal\islandora\GeminiLookup;
use Drupal\node\NodeInterface;
use Drupal\media\MediaInterface;
use Drupal\file\FileInterface;
use Drupal\taxonomy\TermInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\serialization\Normalizer\CacheableNormalizerInterface;
use Drupal\Core\Entity\EntityForm;
use Drupal\file\Entity\File;
/**
* Implements hook_help().
@ -43,31 +46,6 @@ function islandora_help($route_name, RouteMatchInterface $route_match) {
}
}
/**
* Implements hook_rdf_namespaces().
*/
function islandora_rdf_namespaces() {
// Yes, it's amazing, rdf is not registered by default!
return [
'ldp' => 'http://www.w3.org/ns/ldp#',
'dc11' => 'http://purl.org/dc/elements/1.1/',
'dcterms' => 'http://purl.org/dc/terms/',
'nfo' => 'http://www.semanticdesktop.org/ontologies/2007/03/22/nfo/v1.1/',
'ebucore' => 'http://www.ebu.ch/metadata/ontologies/ebucore/ebucore#',
'fedora' => 'http://fedora.info/definitions/v4/repository#',
'owl' => 'http://www.w3.org/2002/07/owl#',
'ore' => 'http://www.openarchives.org/ore/terms/',
'rdf' => 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
'islandora' => 'http://islandora.ca/',
'pcdm' => 'http://pcdm.org/models#',
'use' => 'http://pcdm.org/use#',
'iana' => 'http://www.iana.org/assignments/relation/',
'premis' => 'http://www.loc.gov/premis/rdf/v1#',
'premis3' => 'http://www.loc.gov/premis/rdf/v3/',
'co' => 'http://purl.org/co/',
];
}
/**
* Implements hook_node_insert().
*/
@ -107,7 +85,6 @@ function islandora_node_delete(NodeInterface $node) {
*/
function islandora_media_insert(MediaInterface $media) {
$utils = \Drupal::service('islandora.utils');
// Execute index reactions.
$utils->executeMediaReactions('\Drupal\islandora\Plugin\ContextReaction\IndexReaction', $media);
@ -121,6 +98,8 @@ function islandora_media_insert(MediaInterface $media) {
$media
);
}
// Wait until the media insert is complete, then fire file derivatives.
drupal_register_shutdown_function('_islandora_fire_media_file_derivative_reaction', $media);
}
/**
@ -137,7 +116,6 @@ function islandora_media_update(MediaInterface $media) {
// Execute index reactions.
$utils->executeMediaReactions('\Drupal\islandora\Plugin\ContextReaction\IndexReaction', $media);
// Does it have a source field?
$source_field = $media_source_service->getSourceFieldName($media->bundle());
if (empty($source_field)) {
@ -148,7 +126,6 @@ function islandora_media_update(MediaInterface $media) {
if ($media->get($source_field)->equals($media->original->get($source_field))) {
return;
}
// If it has a parent node...
$node = $utils->getParentNode($media);
if ($node) {
@ -158,6 +135,7 @@ function islandora_media_update(MediaInterface $media) {
$node,
$media
);
$utils->executeMediaReactions('\Drupal\islandora\Plugin\ContextReaction\DerivativeFileReaction', $media);
}
}
@ -171,6 +149,25 @@ function islandora_media_delete(MediaInterface $media) {
$utils->executeMediaReactions('\Drupal\islandora\Plugin\ContextReaction\DeleteReaction', $media);
}
/**
* Helper to fire media derivative file reactions after a media 'insert'.
*
* This function should not be called on its own; it exists as a workaround to
* being unable to fire media events after a media insert operation. This
* behaviour will eventually be replaced by event listeners once these are
* implemented in Drupal 9.
*
* @param \Drupal\Core\Media\MediaInterface $media
* The media that was just inserted.
*
* @see https://www.drupal.org/project/drupal/issues/2551893
*/
function _islandora_fire_media_file_derivative_reaction(MediaInterface $media) {
$utils = \Drupal::service('islandora.utils');
// Execute derivative file reactions.
$utils->executeMediaReactions('\Drupal\islandora\Plugin\ContextReaction\DerivativeFileReaction', $media);
}
/**
* Implements hook_file_insert().
*/
@ -186,7 +183,8 @@ function islandora_file_insert(FileInterface $file) {
*/
function islandora_file_update(FileInterface $file) {
// Exit early if unchanged.
if ($file->filehash['sha1'] == $file->original->filehash['sha1']) {
if ($file->hasField('sha1') && $file->original->hasField('sha1')
&& $file->sha1->getString() == $file->original->sha1->getString()) {
return;
}
@ -257,8 +255,8 @@ function islandora_jsonld_alter_normalized_array(EntityInterface $entity, array
$reaction->execute($entity, $normalized, $context);
foreach ($context_manager->getActiveContexts() as $context_config) {
try {
if ($context_config->getReaction($reaction->getPluginId())) {
$context['cacheability']->addCacheTags($context_config->getCacheTags());
if ($context_config->getReaction($reaction->getPluginId()) && isset($context[CacheableNormalizerInterface::SERIALIZATION_CONTEXT_CACHEABILITY])) {
$context[CacheableNormalizerInterface::SERIALIZATION_CONTEXT_CACHEABILITY]->addCacheableDependency($context_config);
};
}
catch (PluginNotFoundException $e) {
@ -274,10 +272,11 @@ function islandora_jsonld_alter_normalized_array(EntityInterface $entity, array
function islandora_entity_view_mode_alter(&$view_mode, EntityInterface $entity) {
// Change the view mode based on user input from a 'view_mode_alter'
// ContextReaction.
$entity_type = $entity->getEntityType()->id();
$storage = \Drupal::service('entity_type.manager')->getStorage('entity_view_mode');
$context_manager = \Drupal::service('context.manager');
$current_entity = \Drupal::routeMatch()->getParameter('node');
$current_id = ($current_entity instanceof NodeInterface) ? $current_entity->id() : NULL;
$current_entity = \Drupal::routeMatch()->getParameter($entity_type);
$current_id = ($current_entity instanceof NodeInterface || $current_entity instanceof MediaInterface) ? $current_entity->id() : NULL;
if (isset($current_id) && $current_id == $entity->id()) {
foreach ($context_manager->getActiveReactions('\Drupal\islandora\Plugin\ContextReaction\ViewModeAlterReaction') as $reaction) {
// Construct the new view mode's machine name.
@ -316,6 +315,259 @@ function islandora_preprocess_node(&$variables) {
}
}
/**
* Implements hook_form_alter().
*/
function islandora_form_alter(&$form, FormStateInterface $form_state, $form_id) {
$media_add_forms = ['media_audio_add_form', 'media_document_add_form',
'media_extracted_text_add_form', 'media_file_add_form', 'media_image_add_form',
'media_fits_technical_metadata_add_form', 'media_video_add_form',
];
if (in_array($form['#form_id'], $media_add_forms)) {
$params = \Drupal::request()->query->all();
if (isset($params['edit'])) {
$media_of_nid = $params['edit']['field_media_of']['widget'][0]['target_id'];
$node = \Drupal::entityTypeManager()->getStorage('node')->load($media_of_nid);
if ($node) {
$form['name']['widget'][0]['value']['#default_value'] = $node->getTitle();
}
$form['actions']['submit']['#submit'][] = 'islandora_media_custom_form_submit';
}
}
$form_object = $form_state->getFormObject();
$utils = \Drupal::service('islandora.utils');
$config = \Drupal::config('islandora.settings')->get('delete_media_and_files');
if ($config == 1 && $form_object instanceof EntityForm) {
$entity = $form_object->getEntity();
if ($entity->getEntityTypeId() == "node" && $utils->isIslandoraType($entity->getEntityTypeId(), $entity->bundle()) && strpos($form['#form_id'], 'delete_form') !== FALSE) {
$medias = $utils->getMedia($form_state->getFormObject()->getEntity());
if (count($medias) != 0) {
$form['delete_associated_content'] = [
'#type' => 'checkbox',
'#title' => t('Delete all associated medias and nodes'),
];
$media_list = [];
foreach ($medias as $media) {
$media_list[] = $media->getName();
}
$form['container'] = [
'#type' => 'container',
'#states' => [
'visible' => [
':input[name="delete_associated_content"]' => ['checked' => TRUE],
],
],
];
$form['container']['media_items'] = [
'#theme' => 'item_list',
'#type' => 'ul',
'#items' => $media_list,
'#attributes' => ['class' => ['islandora-media-items']],
'#wrapper_attributes' => ['class' => ['container']],
'#attached' => [
'library' => [
'islandora/islandora',
],
],
];
$form['actions']['submit']['#submit'][] = 'islandora_object_delete_form_submit';
return $form;
}
}
}
return $form;
}
/**
* Redirect submit handler for media save.
*/
function islandora_media_custom_form_submit(&$form, FormStateInterface $form_state) {
// Check configuration to see whether a redirect is desired.
$redirect = \Drupal::config('islandora.settings')->get('redirect_after_media_save');
if ($redirect) {
$params = \Drupal::request()->query->all();
if (!empty($params)) {
$target_id = $params['edit']['field_media_of']['widget'][0]['target_id'];
$url = Url::fromRoute('view.media_of.page_1', ['node' => $target_id]);
$form_state->setRedirectUrl($url);
}
}
}
/**
* Implements a submit handler for the delete form.
*/
function islandora_object_delete_form_submit($form, FormStateInterface $form_state) {
$result = $form_state->getValues('delete_associated_content');
$utils = \Drupal::service('islandora.utils');
if ($result['delete_associated_content'] == 1) {
$node = $form_state->getFormObject()->getEntity();
$medias = $utils->getMedia($node);
$media_list = [];
$entity_field_manager = \Drupal::service('entity_field.manager');
$current_user = \Drupal::currentUser();
$logger = \Drupal::logger('logger.channel.islandora');
$messenger = \Drupal::messenger();
$delete_media = [];
$media_translations = [];
$media_files = [];
$entity_protected_medias = [];
$inaccessible_entities = [];
foreach ($medias as $id => $media) {
$lang = $media->language()->getId();
$selected_langcodes[$lang] = $lang;
if (!$media->access('delete', $current_user)) {
$inaccessible_entities[] = $media;
continue;
}
// Check for files.
$fields = $entity_field_manager->getFieldDefinitions('media', $media->bundle());
foreach ($fields as $field) {
$type = $field->getType();
if ($type == 'file' || $type == 'image') {
$target_id = $media->get($field->getName())->target_id;
$file = File::load($target_id);
if ($file) {
if (!$file->access('delete', $current_user)) {
$inaccessible_entities[] = $file;
continue;
}
$media_files[$id][$file->id()] = $file;
}
}
}
foreach ($selected_langcodes as $langcode) {
// We're only working with media, which are translatable.
$entity = $media->getTranslation($langcode);
if ($entity->isDefaultTranslation()) {
$delete_media[$id] = $entity;
unset($media_translations[$id]);
}
elseif (!isset($delete_media[$id])) {
$media_translations[$id][] = $entity;
}
}
}
if ($delete_media) {
foreach ($delete_media as $id => $media) {
try {
$media->delete();
$media_list[] = $id;
$logger->notice('The media %label has been deleted.', [
'%label' => $media->label(),
]);
}
catch (Exception $e) {
$entity_protected_medias[] = $id;
}
}
}
$delete_files = array_filter($media_files, function ($media) use ($entity_protected_medias) {
return !in_array($media, $entity_protected_medias);
}, ARRAY_FILTER_USE_KEY);
if ($delete_files) {
foreach ($delete_files as $files_array) {
foreach ($files_array as $file) {
$file->delete();
$logger->notice('The file %label has been deleted.', [
'%label' => $file->label(),
]);
}
}
}
$delete_media_translations = array_filter($media_translations, function ($media) use ($entity_protected_medias) {
return !in_array($media, $entity_protected_medias);
}, ARRAY_FILTER_USE_KEY);
if ($delete_media_translations) {
foreach ($delete_media_translations as $id => $translations) {
$media = $medias[$id];
foreach ($translations as $translation) {
$media->removeTranslation($translation->language()->getId());
}
$media->save();
foreach ($translations as $translation) {
$logger->notice('The media %label @language translation has been deleted', [
'%label' => $media->label(),
'@language' => $translation->language()->getName(),
]);
}
}
}
if ($inaccessible_entities) {
$messenger->addWarning("@count items have not been deleted because you do not have the necessary permissions.", [
'@count' => count($inaccessible_entities),
]);
}
$build = [
'heading' => [
'#type' => 'html_tag',
'#tag' => 'div',
'#value' => t("The repository item @node and @media", [
'@node' => $node->getTitle(),
'@media' => \Drupal::translation()->formatPlural(
count($media_list), 'the media with the id @media has been deleted.',
'the medias with the ids @media have been deleted.',
['@media' => implode(", ", $media_list)],
),
]),
],
];
$message = \Drupal::service('renderer')->renderPlain($build);
$messenger->deleteByType('status');
$messenger->addStatus($message);
}
}
/**
* Implements hook_field_widget_single_element_WIDGET_TYPE_form_alter().
*/
function islandora_field_widget_single_element_image_image_form_alter(&$element, $form_state, $context) {
$element['#process'][] = 'islandora_add_default_image_alt_text';
}
/**
* Callback for hook_field_widget_single_element_WIDGET_TYPE_form_alter().
*/
function islandora_add_default_image_alt_text($element, $form_state, $form) {
if ($element['alt']['#access']) {
$params = \Drupal::request()->query->all();
if (isset($params['edit'])) {
$media_of_nid = $params['edit']['field_media_of']['widget'][0]['target_id'];
$node = \Drupal::entityTypeManager()->getStorage('node')->load($media_of_nid);
if ($node) {
$element['alt']['#default_value'] = $node->getTitle();
}
}
}
return $element;
}
/**
* Implements hook_entity_form_display_alter().
*/
@ -357,16 +609,18 @@ function islandora_form_block_form_alter(&$form, FormStateInterface $form_state,
// /admin/structure/context instead if you want to use these conditions
// to alter block layout.
unset($form['visibility']['content_entity_type']);
unset($form['visibility']['parent_node_has_term']);
unset($form['visibility']['node_had_namespace']);
unset($form['visibility']['media_has_term']);
unset($form['visibility']['file_uses_filesystem']);
unset($form['visibility']['node_has_term']);
unset($form['visibility']['node_has_parent']);
unset($form['visibility']['media_uses_filesystem']);
unset($form['visibility']['media_has_mimetype']);
unset($form['visibility']['node_is_islandora_object']);
unset($form['visibility']['media_has_term']);
unset($form['visibility']['media_is_islandora_media']);
unset($form['visibility']['media_uses_filesystem']);
unset($form['visibility']['node_had_namespace']);
unset($form['visibility']['node_has_ancestor']);
unset($form['visibility']['node_has_parent']);
unset($form['visibility']['node_has_term']);
unset($form['visibility']['node_is_islandora_object']);
unset($form['visibility']['node_referenced_by_node']);
unset($form['visibility']['parent_node_has_term']);
}
/**
@ -380,8 +634,8 @@ function islandora_entity_extra_field_info() {
if (!empty($pseudo_bundles)) {
foreach ($pseudo_bundles as $key) {
list($bundle, $content_entity) = explode(":", $key);
$extra_field[$content_entity][$bundle]['display']['field_gemini_uri'] = [
[$bundle, $content_entity] = explode(":", $key);
$extra_field[$content_entity][$bundle]['display'][IslandoraSettingsForm::GEMINI_PSEUDO_FIELD] = [
'label' => t('Fedora URI'),
'description' => t('The URI to the persistent'),
'weight' => 100,
@ -400,27 +654,65 @@ function islandora_entity_view(array &$build, EntityInterface $entity, EntityVie
// Ensure the entity matches the route.
if ($entity === $route_match_item) {
if ($display->getComponent('field_gemini_uri')) {
$gemini = \Drupal::service('islandora.gemini.lookup');
if ($gemini instanceof GeminiLookup) {
$fedora_uri = $gemini->lookup($entity);
if (!is_null($fedora_uri)) {
$build['field_gemini_uri'] = [
'#type' => 'container',
'#attributes' => [
'id' => 'field-gemini-uri',
],
'internal_label' => [
'#type' => 'item',
'#title' => t('Fedora URI'),
'internal_uri' => [
'#type' => 'link',
'#title' => t("@url", ['@url' => $fedora_uri]),
'#url' => Url::fromUri($fedora_uri),
],
],
];
$mapper = \Drupal::service('islandora.entity_mapper');
$flysystem_config = Settings::get('flysystem');
$fedora_root = $flysystem_config['fedora']['config']['root'];
$fedora_root = rtrim($fedora_root, '/');
if ($entity->getEntityTypeId() == 'media') {
// Check if the source file is in Fedora or not.
$media_source_service = \Drupal::service('islandora.media_source_service');
$source_file = $media_source_service->getSourceFile($entity);
if (!$source_file) {
\Drupal::logger('islandora')->error(
\Drupal::service('string_translation')->translate(
"Can't get source file for @label (@id)", [
'@label' => $entity->label(),
"@id" => $entity->id(),
]
)
);
return;
}
$uri = $source_file->getFileUri();
$scheme = \Drupal::service('stream_wrapper_manager')->getScheme($uri);
$flysystem_config = Settings::get('flysystem');
// Use the file's path if it's in fedora.
// Otherwise do the UUID -> pair tree thang.
if (isset($flysystem_config[$scheme]) && $flysystem_config[$scheme]['driver'] == 'fedora') {
$parts = parse_url($uri);
$path = $parts['host'] . $parts['path'];
}
else {
$path = $mapper->getFedoraPath($source_file->uuid());
}
$path = trim($path, '/');
$fedora_uri = "$fedora_root/$path/fcr:metadata";
}
else {
// All non-media entities do the UUID -> pair tree thang.
$path = $mapper->getFedoraPath($entity->uuid());
$path = trim($path, '/');
$fedora_uri = "$fedora_root/$path";
}
// Stuff the fedora url into the pseudo field.
$build['field_gemini_uri'] = [
'#type' => 'container',
'#attributes' => [
'id' => 'field-gemini-uri',
],
'internal_label' => [
'#type' => 'item',
'#title' => t('Fedora URI'),
'internal_uri' => [
'#type' => 'link',
'#title' => t("@url", ['@url' => $fedora_uri]),
'#url' => Url::fromUri($fedora_uri),
],
],
];
}
}
}
@ -435,7 +727,7 @@ function islandora_preprocess_views_view_table(&$variables) {
// Check for a weight selector field.
foreach ($variables['view']->field as $field_key => $field) {
if ($field->options['plugin_id'] == 'integer_weight_selector') {
if ($field->getPluginId() == 'integer_weight_selector') {
// Check if the weight selector is on the first column.
$is_first_column = array_search($field_key, array_keys($variables['view']->field)) > 0 ? FALSE : TRUE;

16
islandora.post_update.php

@ -0,0 +1,16 @@
<?php
/**
* @file
* Post updates.
*/
/**
* Set default value for delete_media_and_files field in settings.
*/
function islandora_post_update_delete_media_and_files() {
$config_factory = \Drupal::configFactory();
$config = $config_factory->getEditable('islandora.settings');
$config->set('delete_media_and_files', TRUE);
$config->save(TRUE);
}

52
islandora.routing.yml

@ -16,6 +16,15 @@ system.islandora_settings:
requirements:
_permission: 'administer site configuration'
# RDF properties report
system.islandora_rdf_mappings:
path: '/admin/reports/islandora/rdf_mappings'
defaults:
_controller: '\Drupal\islandora\Controller\RdfMappingsReportController::main'
_title: 'Field and term RDF mappings'
requirements:
_permission: 'administer site configuration'
islandora.add_member_to_node_page:
path: '/node/{node}/members/add'
defaults:
@ -27,6 +36,17 @@ islandora.add_member_to_node_page:
requirements:
_entity_create_any_access: 'node'
islandora.upload_children:
path: '/node/{node}/members/upload/{step}'
defaults:
_wizard: '\Drupal\islandora\Form\AddChildrenWizard\ChildForm'
_title: 'Upload Children'
step: 'type_selection'
options:
_admin_route: 'TRUE'
requirements:
_custom_access: '\Drupal\islandora\Form\AddChildrenWizard\Access::childAccess'
islandora.add_media_to_node_page:
path: '/node/{node}/media/add'
defaults:
@ -38,6 +58,17 @@ islandora.add_media_to_node_page:
requirements:
_entity_create_any_access: 'media'
islandora.upload_media:
path: '/node/{node}/media/upload/{step}'
defaults:
_wizard: '\Drupal\islandora\Form\AddChildrenWizard\MediaForm'
_title: 'Add media'
step: 'type_selection'
options:
_admin_route: 'TRUE'
requirements:
_custom_access: '\Drupal\islandora\Form\AddChildrenWizard\Access::mediaAccess'
islandora.media_source_update:
path: '/media/{media}/source'
defaults:
@ -57,3 +88,24 @@ islandora.media_source_put_to_node:
_custom_access: '\Drupal\islandora\Controller\MediaSourceController::putToNodeAccess'
options:
_auth: ['basic_auth', 'cookie', 'jwt_auth']
islandora.attach_file_to_media:
path: '/media/add_derivative/{media}/{destination_field}'
defaults:
_controller: '\Drupal\islandora\Controller\MediaSourceController::attachToMedia'
methods: [GET, PUT]
requirements:
_custom_access: '\Drupal\islandora\Controller\MediaSourceController::attachToMediaAccess'
options:
_auth: ['basic_auth', 'cookie', 'jwt_auth']
no_cache: 'TRUE'
parameters:
media:
type: entity:media
islandora.confirm_delete_media_and_file:
path: '/media/delete_with_files'
defaults:
_form: 'Drupal\islandora\Form\ConfirmDeleteMediaAndFile'
requirements:
_permission: 'administer media+delete any media'

37
islandora.services.yml

@ -31,6 +31,9 @@ services:
logger.channel.islandora:
parent: logger.channel_base
arguments: ['islandora']
logger.channel.fedora_flysystem:
parent: logger.channel_base
arguments: ['fedora_flysystem']
islandora.media_route_context_provider:
class: Drupal\islandora\ContextProvider\MediaRouteContextProvider
arguments: ['@current_route_match']
@ -48,14 +51,30 @@ services:
- { name: 'context_provider' }
islandora.media_source_service:
class: Drupal\islandora\MediaSource\MediaSourceService
arguments: ['@entity_type.manager', '@current_user', '@language_manager', '@entity.query', '@file_system', '@islandora.utils']
arguments: ['@entity_type.manager', '@current_user', '@language_manager', '@file_system', '@islandora.utils']
islandora.utils:
class: Drupal\islandora\IslandoraUtils
arguments: ['@entity_type.manager', '@entity_field.manager', '@entity.query', '@context.manager', '@flysystem_factory', '@language_manager']
islandora.gemini.client:
class: Islandora\Crayfish\Commons\Client\GeminiClient
factory: ['Drupal\islandora\GeminiClientFactory', create]
arguments: ['@config.factory', '@logger.channel.islandora']
islandora.gemini.lookup:
class: Drupal\islandora\GeminiLookup
arguments: ['@islandora.gemini.client', '@jwt.authentication.jwt', '@islandora.media_source_service', '@http_client', '@logger.channel.islandora']
arguments: ['@entity_type.manager', '@entity_field.manager', '@context.manager', '@flysystem_factory', '@language_manager']
islandora.entity_mapper:
class: Islandora\EntityMapper\EntityMapper
islandora.stomp.auth_header_listener:
class: Drupal\islandora\EventSubscriber\StompHeaderEventSubscriber
arguments: ['@jwt.authentication.jwt']
tags:
- { name: event_subscriber }
islandora.upload_children.batch_processor:
class: Drupal\islandora\Form\AddChildrenWizard\ChildBatchProcessor
arguments:
- '@entity_type.manager'
- '@database'
- '@current_user'
- '@messenger'
- '@date.formatter'
islandora.upload_media.batch_processor:
class: Drupal\islandora\Form\AddChildrenWizard\MediaBatchProcessor
arguments:
- '@entity_type.manager'
- '@database'
- '@current_user'
- '@messenger'
- '@date.formatter'

192
islandora.tokens.inc

@ -0,0 +1,192 @@
<?php
/**
* @file
* Contains islandora.tokens.inc.
*
* This file provides islandora tokens.
*/
use Drupal\Core\Render\BubbleableMetadata;
use Drupal\media\Entity\Media;
use Drupal\file\Entity\File;
/**
* Implements hook_token_info().
*/
function islandora_token_info() {
$type = [
'name' => t('Islandora Tokens'),
'description' => t('Tokens for Islandora objects.'),
];
$node['media-original-file:filename'] = [
'name' => t('Media: Original File filename without extension.'),
'description' => t('File name without extension of original uploaded file associated with Islandora Object via Media.'),
];
$node['media-original-file:basename'] = [
'name' => t('Media: Original File filename with extension.'),
'description' => t('File name with extension of original uploaded file associated with Islandora Object via Media.'),
];
$node['media-original-file:extension'] = [
'name' => t('Media: Original File extension.'),
'description' => t('File extension of original uploaded file associated with Islandora Object via Media.'),
];
$node['media-thumbnail-image:url'] = [
'name' => t('Media: Thumbnail Image URL.'),
'description' => t('URL of Thumbnail Image associated with Islandora Object via Media.'),
];
$node['media-thumbnail-image:alt'] = [
'name' => t('Alternative text for Media: Thumbnail Image.'),
'description' => t('Alternative text for Thumbnail Image associated with Islandora Object via Media.'),
];
// Deprecated in favour if hyphenated version.
$node['media_thumbnail_image:url'] = [
'name' => t('Media: Thumbnail Image URL.'),
'description' => t('Deprecated: URL of Thumbnail Image associated with Islandora Object via Media.'),
];
// Deprecated in favour if hyphenated version.
$node['media_thumbnail_image:alt'] = [
'name' => t('Alternative text for Media: Thumbnail Image.'),
'description' => t('Deprecated: Alternative text for Thumbnail Image associated with Islandora Object via Media.'),
];
$node['pdf_url'] = [
'name' => t("PDF Url"),
'description' => t('URL to related media file if "Original file" is a PDF file'),
];
return [
'types' => ['islandoratokens' => $type],
'tokens' => ['islandoratokens' => $node],
];
}
/**
* Implements hook_tokens().
*/
function islandora_tokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) {
$replacements = [];
if ($type == 'islandoratokens' && !empty($data['node'])) {
if (!is_array($tokens) || empty($tokens)) {
\Drupal::logger('islandora')
->alert(
'Tokens not correct format: @tokens', [
'@tokens' => print_r($tokens, 1),
]
);
return;
}
$islandoraUtils = \Drupal::service('islandora.utils');
foreach ($tokens as $name => $original) {
switch ($name) {
case 'media-original-file:basename':
case 'media-original-file:filename':
case 'media-original-file:extension':
$term = $islandoraUtils->getTermForUri('http://pcdm.org/use#OriginalFile');
$media = $islandoraUtils->getMediaWithTerm($data['node'], $term);
// Is there media?
if ($media) {
$file = \Drupal::service('islandora.media_source_service')->getSourceFile($media);
if (!empty($file)) {
$path_info = pathinfo($file->createFileUrl());
$key = explode(':', $name)[1];
if (array_key_exists($key, $path_info)) {
$replacements[$original] = $path_info[$key];
}
}
}
break;
case 'media-thumbnail-image:url':
case 'media_thumbnail_image:url':
$term = $islandoraUtils->getTermForUri('http://pcdm.org/use#ThumbnailImage');
$media = $islandoraUtils->getMediaWithTerm($data['node'], $term);
// Is there media?
// @todo is this single or multiple?
if ($media) {
$file = \Drupal::service('islandora.media_source_service')->getSourceFile($media);
if (!empty($file)) {
$url = $file->createFileUrl();
$replacements[$original] = $url;
}
}
break;
case 'media-thumbnail-image:alt':
case 'media_thumbnail_image:alt':
$alt = '';
$term = $islandoraUtils->getTermForUri('http://pcdm.org/use#ThumbnailImage');
$media = $islandoraUtils->getMediaWithTerm($data['node'], $term);
// Is there media?
// @todo is this single or multiple?
if ($media) {
// Is the media an image?
if (isset($media->field_media_image)) {
$alt = $media->field_media_image[0]->alt;
}
}
// @todo get alt from original or service file, if thumbnail
// alt is empty.
$replacements[$original] = $alt;
break;
case 'pdf_url':
$replacements[$original] = islandora_url_to_service_file_media_by_mimetype($data['node'], 'application/pdf');
break;
}
}
}
return $replacements;
}
/**
* Gets Original File PDF file URL.
*
* @param object $node
* A core drupal node object.
* @param string $mime_type
* The name of the node's field to check for the specific relationship.
*
* @return string
* The tokenized value for the given data.
*/
function islandora_url_to_service_file_media_by_mimetype($node, $mime_type) {
$islandora_utils = \Drupal::service('islandora.utils');
$origfile_term = $islandora_utils->getTermForUri('http://pcdm.org/use#OriginalFile');
$origfile_media = $islandora_utils->getMediaWithTerm($node, $origfile_term);
// Get the media file's mime_type value.
if (is_object($origfile_media)) {
$origfile_mime_type = ($origfile_media->hasField('field_mime_type')) ?
$origfile_media->get('field_mime_type')->getValue() : NULL;
$origfile_mime_type = (is_array($origfile_mime_type) &&
array_key_exists(0, $origfile_mime_type) &&
is_array($origfile_mime_type[0]) &&
array_key_exists('value', $origfile_mime_type[0])) ?
$origfile_mime_type[0]['value'] : '';
// Compare the media file's mime_type to the given value.
if ($origfile_mime_type == $mime_type) {
$vid = $origfile_media->id();
if (!is_null($vid)) {
$media = Media::load($vid);
$bundle = $media->bundle();
// Since this is Islandora and we assume the Original File is a
// Document type... but doing it dynamically.
$fid = $media->get('field_media_' . $bundle)->getValue();
$fid_value = (is_array($fid) && array_key_exists(0, $fid) &&
array_key_exists('target_id', $fid[0])) ?
$fid[0]['target_id'] : NULL;
if (!is_null($fid_value)) {
$file = File::load($fid_value);
if ($file) {
$url = $islandora_utils->getDownloadUrl($file);
return $url;
}
}
}
}
}
return '';
}

51
islandora.views.inc

@ -9,17 +9,46 @@
* Implements hook_views_data_alter().
*/
function islandora_views_data_alter(&$data) {
// For now only support Nodes.
$fields = \Drupal::service('entity_field.manager')->getFieldStorageDefinitions('node');
foreach ($fields as $field => $field_storage_definition) {
if ($field_storage_definition->getType() == 'integer' && strpos($field, "field_") === 0) {
$data['node__' . $field][$field . '_value']['field'] = $data['node__' . $field][$field]['field'];
$data['node__' . $field][$field]['title'] = t('Integer Weight Selector (@field)', [
'@field' => $field,
]);
$data['node__' . $field][$field]['help'] = t('Provides a drag-n-drop reordering of integer-based weight fields.');
$data['node__' . $field][$field]['title short'] = t('Integer weight selector');
$data['node__' . $field][$field]['field']['id'] = 'integer_weight_selector';
// For now only support Nodes and Media.
foreach (['node', 'media'] as $entity_type) {
$fields = \Drupal::service('entity_field.manager')->getFieldStorageDefinitions($entity_type);
foreach ($fields as $field => $field_storage_definition) {
if ($field_storage_definition->getType() == 'integer' && strpos($field, "field_") === 0) {
$prefixed_field = $entity_type . '__' . $field;
if (isset($data[$prefixed_field])) {
$data[$prefixed_field][$field . '_value']['field'] = $data[$prefixed_field][$field]['field'];
$data[$prefixed_field][$field]['title'] = t('Integer Weight Selector (@field)', [
'@field' => $field,
]);
$data[$prefixed_field][$field]['help'] = t('Provides a drag-n-drop reordering of integer-based weight fields.');
$data[$prefixed_field][$field]['title short'] = t('Integer weight selector');
$data[$prefixed_field][$field]['field']['id'] = 'integer_weight_selector';
}
}
}
}
// Add Has Media filter.
$data['node_field_data']['islandora_has_media'] = [
'title' => t('Node has Media Use'),
'group' => t('Content'),
'filter' => [
'title' => t('Node has media use filter'),
'help' => t('Provides a custom filter for nodes that do or do not have media with a given use.'),
'field' => 'nid',
'id' => 'islandora_node_has_media_use',
],
];
// Add Is Islandora filter.
$data['node_field_data']['islandora_node_is_islandora'] = [
'title' => t('Node is Islandora'),
'group' => t('Content'),
'filter' => [
'title' => t('Node is Islandora'),
'help' => t('Node has a content type that possesses the mandatory Islandora fields.'),
'field' => 'nid',
'id' => 'islandora_node_is_islandora',
],
];
}

2
migrate/tags.csv

@ -15,3 +15,5 @@ islandora_models,"Digital Document","An electronic file or document.",https://sc
islandora_models,"Paged Content","An Electronic Book, object with pages",https://schema.org/Book
islandora_models,"Page","A page in an Electronic Paged Content Object",http://id.loc.gov/ontologies/bibframe/part
islandora_models,"Publication Issue","A part of a successively published publication such as a periodical or publication volume, often numbered, usually containing a grouping of works such as articles.",https://schema.org/PublicationIssue
islandora_models,"Compound Object","A special type of collection where the parent item may also have complex metadata",http://vocab.getty.edu/aat/300242735
islandora_models,"Newspaper","A special type of collection which only has Newspaper Issues for children.",https://schema.org/Newspaper

1 vid name description external_uri
15 islandora_models Paged Content An Electronic Book, object with pages https://schema.org/Book
16 islandora_models Page A page in an Electronic Paged Content Object http://id.loc.gov/ontologies/bibframe/part
17 islandora_models Publication Issue A part of a successively published publication such as a periodical or publication volume, often numbered, usually containing a grouping of works such as articles. https://schema.org/PublicationIssue
18 islandora_models Compound Object A special type of collection where the parent item may also have complex metadata http://vocab.getty.edu/aat/300242735
19 islandora_models Newspaper A special type of collection which only has Newspaper Issues for children. https://schema.org/Newspaper

73
modules/islandora_advanced_search/CONTRIBUTING.md

@ -0,0 +1,73 @@
# Welcome!
If you are reading this document then you are interested in contributing to Islandora 8. All contributions are welcome: use-cases, documentation, code, patches, bug reports, feature requests, etc. You do not need to be a programmer to speak up!
We also have an IRC channel -- #islandora -- on freenode.net. Feel free to hang out there, ask questions, and help others out if you can.
Please note that this project operates under the [Islandora Community Code of Conduct](http://islandora.ca/codeofconduct). By participating in this project you agree to abide by its terms.
## Workflows
The group meets each Wednesday at 1:00 PM Eastern. Meeting notes and announcements are posted to the [Islandora community list](https://groups.google.com/forum/#!forum/islandora) and the [Islandora developers list](https://groups.google.com/forum/#!forum/islandora-dev). You can view meeting agendas, notes, and call-in information [here](https://github.com/Islandora/documentation/wiki#islandora-8-tech-calls). Anybody is welcome to join the calls, and add items to the agenda.
### Use cases
If you would like to submit a use case to the Islandora 8 project, please submit an issue [here](https://github.com/Islandora/documentation/issues/new) using the [Use Case template](https://github.com/Islandora/documentation/wiki/Use-Case-template), prepending "Use Case:" to the title of the issue.
### Documentation
You can contribute documentation in two different ways. One way is to create an issue [here](https://github.com/Islandora/documentation/issues/new), prepending "Documentation:" to the title of the issue. Another way is by pull request, which is the same process as [Contribute Code](https://github.com/Islandora/documentation/blob/main/CONTRIBUTING.md#contribute-code). All documentation resides in [`docs`](https://github.com/Islandora/documentation/tree/main/docs).
### Request a new feature
To request a new feature you should [open an issue in the Islandora 8 repository](https://github.com/Islandora/documentation/issues/new) or create a use case (see the _Use cases_ section above), and summarize the desired functionality. Prepend "Enhancement:" if creating an issue on the project repo, and "Use Case:" if creating a use case.
### Report a bug
To report a bug you should [open an issue in the Islandora 8 repository](https://github.com/Islandora/documentation/issues/new) that summarizes the bug. Prepend the label "Bug:" to the title of the issue.
In order to help us understand and fix the bug it would be great if you could provide us with:
1. The steps to reproduce the bug. This includes information about e.g. the Islandora version you were using along with the versions of stack components.
2. The expected behavior.
3. The actual, incorrect behavior.
Feel free to search the issue queue for existing issues (aka tickets) that already describe the problem; if there is such a ticket please add your information as a comment.
**If you want to provide a pull along with your bug report:**
That is great! In this case please send us a pull request as described in the section _Create a pull request_ below.
### Contribute code
Before you set out to contribute code you will need to have completed a [Contributor License Agreement](http://islandora.ca/sites/default/files/islandora_cla.pdf) or be covered by a [Corporate Contributor License Agreement](http://islandora.ca/sites/default/files/islandora_ccla.pdf). The signed copy of the license agreement should be sent to <mailto:community@islandora.ca>
_If you are interested in contributing code to Islandora but do not know where to begin:_
In this case you should [browse open issues](https://github.com/Islandora/documentation/issues) and check out [use cases](https://github.com/Islandora/documentation/labels/use%20case).
If you are contributing Drupal code, it must adhere to [Drupal Coding Standards](https://www.drupal.org/coding-standards); Travis CI will check for this on pull requests.
Contributions to the Islandora codebase should be sent as GitHub pull requests. See section _Create a pull request_ below for details. If there is any problem with the pull request we can work through it using the commenting features of GitHub.
* For _small patches_, feel free to submit pull requests directly for those patches.
* For _larger code contributions_, please use the following process. The idea behind this process is to prevent any wasted work and catch design issues early on.
1. [Open an issue](https://github.com/Islandora/documentation/issues), prepending "Enhancement:" in the title if a similar issue does not exist already. If a similar issue does exist, then you may consider participating in the work on the existing issue.
2. Comment on the issue with your plan for implementing the issue. Explain what pieces of the codebase you are going to touch and how everything is going to fit together.
3. Islandora committers will work with you on the design to make sure you are on the right track.
4. Implement your issue, create a pull request (see below), and iterate from there.
### Create a pull request
Take a look at [Creating a pull request](https://help.github.com/articles/creating-a-pull-request). In a nutshell you need to:
1. [Fork](https://help.github.com/articles/fork-a-repo) this repository to your personal or institutional GitHub account (depending on the CLA you are working under). Be cautious of which branches you work from though (you'll want to base your work off the default branch). See [Fork a repo](https://help.github.com/articles/fork-a-repo) for detailed instructions.
2. Commit any changes to your fork.
3. Send a [pull request](https://help.github.com/articles/creating-a-pull-request) using the [pull request template](https://github.com/Islandora/documentation/blob/main/.github/PULL_REQUEST_TEMPLATE.md) to the Islandora GitHub repository that you forked in step 1. If your pull request is related to an existing issue -- for instance, because you reported a [bug/issue](https://github.com/Islandora/documentation/issues) earlier -- prefix the title of your pull request with the corresponding issue number (e.g. `issue-123: ...`). Please also include a reference to the issue in the description of the pull. This can be done by using '#' plus the issue number like so '#123', also try to pick an appropriate name for the branch in which you're issuing the pull request from.
You may want to read [Syncing a fork](https://help.github.com/articles/syncing-a-fork) for instructions on how to keep your fork up to date with the latest changes of the upstream (official) repository.
## License Agreements
The Islandora Foundation requires that contributors complete a [Contributor License Agreement](http://islandora.ca/sites/default/files/islandora_cla.pdf) or be covered by a [Corporate Contributor License Agreement](http://islandora.ca/sites/default/files/islandora_ccla.pdf). The signed copy of the license agreement should be sent to <a href="mailto:community@islandora.ca?Subject=Contributor%20License%20Agreement" target="_top">community@islandora.ca</a>. This license is for your protection as a contributor as well as the protection of the Foundation and its users; it does not change your rights to use your own contributions for any other purpose. A list of current CLAs is kept [here](https://github.com/Islandora/islandora/wiki/Contributor-License-Agreements).

339
modules/islandora_advanced_search/LICENSE

@ -0,0 +1,339 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.

261
modules/islandora_advanced_search/README.md

@ -0,0 +1,261 @@
# Islandora Advanced Search <!-- omit in toc -->
- [Introduction](#introduction)
- [Requirements](#requirements)
- [Installation](#installation)
- [Configuration](#configuration)
- [Configuring Solr](#configuring-solr)
- [Configure Collection Search](#configure-collection-search)
- [Configure Views](#configure-views)
- [Exposed Form](#exposed-form)
- [Collection Search](#collection-search)
- [Paging](#paging)
- [Sorting](#sorting)
- [Configure Facets](#configure-facets)
- [Include / Exclude Facets](#include--exclude-facets)
- [Configure Blocks](#configure-blocks)
- [Advanced Search Block](#advanced-search-block)
- [Documentation](#documentation)
- [Troubleshooting/Issues](#troubleshootingissues)
- [Maintainers](#maintainers)
- [Sponsors](#sponsors)
- [Development](#development)
- [License](#license)
## Introduction
This module creates several blocks to support searching. It also enables the use
of Ajax with search blocks, facets, and search results.
![image](./docs/demo.gif)
## Requirements
Use composer to download the required libraries and modules.
```bash
composer require drupal/facets "^1.3"
composer require drupal/search_api_solr "^4.1"
composer require drupal/search_api "^1.5"
```
However, for reference, `islandora_advanced_search` requires the following
drupal modules:
- [facets](https://www.drupal.org/project/facets)
- [search_api_solr](https://www.drupal.org/project/search_api_solr)
## Installation
To download/enable just this module, use the following from the command line:
```bash
composer require islandora/islandora
drush en islandora_advanced_search
```
## Configuration
You can set the following configuration at
`admin/config/islandora/advanced_search`:
![image](./docs/islandora_advanced_search_settings.png)
## Configuring Solr
Please review
[Islandora Documentation](https://islandora.github.io/documentation/user-documentation/searching/)
before continuing. The following assumes you already have a working Solr and the
Drupal Search API setup.
## Configure Collection Search
To support collection based searches you need to index the `field_member_of` for
every repository item as well define a new field that captures the full
hierarchy of `field_member_of` for each repository item.
Add a new `Content` solr field `field_decedent_of` to the solr index at
`admin/config/search/search-api/index/default_solr_index/fields`.
![image](./docs/field_decedent_of.png)
Then under `admin/config/search/search-api/index/default_solr_index/processors`
enable `Index hierarchy` and setup the new field to index the hierarchy.
![image](./docs/enable_index_hierarchy.png)
![image](./docs/enable_index_hierarchy_processor.png)
The field can now be used limit a search to all the decedents of a given object.
> N.B. You may have to re-index to make sure the field is populated.
## Configure Views
The configuration of views is outside of the scope of this document, please read
the [Drupal Documentation](https://www.drupal.org/docs/8/core/modules/views), as
well as the
[Search API Documentation](https://www.drupal.org/docs/contributed-modules/search-api).
### Exposed Form
Solr views allow the user to configure an exposed form (_optionally as a
block_). This form / block is **different** from the
[Advanced Search Block](#advanced-search-block). This module does not make any
changes to the form, but this form can cause the Advanced Search Block to not
function if configured incorrectly.
The Advanced Search Block requires that if present the Exposed forms
`Exposed form style` is set to `Basic` rather than `Input Required`. As
`Input Required` will prevent any search from occurring unless the user puts an
additional query in the Exposed form as well.
![Form Style](./docs/basic-input.png)
### Collection Search
That being said it will be typical that you require the following
`Relationships` and `Contextual Filters` when setting up a search view to enable
`Collection Search` searches.
![image](./docs/view_advanced_setting.png)
Here a relationship is setup with `Member Of` field and we have **two**
contextual filters:
1. `field_member_of` (Direct decedents of the Entity)
2. `field_decedent_of` (All decedents of the Entity)
Both of these filters are configured the exact same way.
![image](./docs/contextual_filter_settings.png)
These filters are toggled by the Advanced Search block to allow the search to
include all decedents or just direct decedents (*documented below*).
### Paging
The paging options specified here can have an affect on the pager block
(*documented below*).
![image](./docs/pager_settings.png)
### Sorting
Additional the fields listed as `Sort Criteria` as `Exposed` will be made
available in the pager block (*documented below*).
![image](./docs/sort_criteria.png)
## Configure Facets
The facets can be configured at `admin/config/search/facets`. Facets are linked
to a **Source** which is a **Search API View Display** so it will be typically
to have to duplicate your configuration for a given facet across each of the
displays where you want it to show up.
### Include / Exclude Facets
To be able to display exclude facet links as well as include links in the facets
block we have to duplicate the configuration for the facet like so.
![image](./docs/include_exclude_facets.png)
Both the include / exclude facets must use the widget
`List of links that allow the user to include / exclude facets`
![image](./docs/include_exclude_facets_settings.png)
The excluded facet also needs the following settings to appear and function
correctly.
The `URL alias` must match the same value as the include facet except it must be
prefixed with `~` character that is what links to the two facets to each other.
![image](./docs/exclude_facet_settings_url_alias.png)
And it must also explicitly be set to exclude:
![image](./docs/exclude_facet_settings_exclude.png)
You may also want to enable `Hide active items` and `Hide non-narrowing results`
for a cleaner presentation of facets.
## Configure Blocks
For each block type:
- Facet
- Pager
- Advanced Search
There will be **one block** per `View Display`. The block should be limited to
only appear when the view it was derived from is also being displayed on the
same page.
This requires configuring the `visibility` of the block as appropriate. For
collection based searches be sure to limit the display of the Facets block to
the models you want to display the search on, e.g:
![image](./docs/facet_block_settings.png)
### Advanced Search Block
For any valid search field, you can drag / drop and reorder the fields to
display in the advanced search form on. The configuration resides on the block
so this can differ across views / displays if need be. Additionally if the View
the block was derived from has multiple contextual filters you can choose which
one corresponds to direct children, this will enable the recursive search
checkbox.
![image](./docs/advanced_search_block_settings.png)
> N.B. Be aware that the Search views [Exposed Form](#exposed-form) can have an
> affect on the function of the
> [Advanced Search Block](#advanced-search-block). Please refer to that section
> to learn more.
## Documentation
Further documentation for this module is available on the
[Islandora 8 documentation site](https://islandora.github.io/documentation/).
## Troubleshooting/Issues
Having problems or solved a problem? Check out the Islandora google groups for
a solution.
- [Islandora Group](https://groups.google.com/forum/?hl=en&fromgroups#!forum/islandora)
- [Islandora Dev Group](https://groups.google.com/forum/?hl=en&fromgroups#!forum/islandora-dev)
## Maintainers
Current maintainers:
- [Nigel Banks](https://github.com/nigelgbanks)
## Sponsors
- LYRASIS
## Development
If you would like to contribute, please get involved by attending our weekly
[Tech Call](https://github.com/Islandora/documentation/wiki). We love to hear
from you!
If you would like to contribute code to the project, you need to be covered by
an Islandora Foundation
[Contributor License Agreement](http://islandora.ca/sites/default/files/islandora_cla.pdf)
or
[Corporate Contributor License Agreement](http://islandora.ca/sites/default/files/islandora_ccla.pdf).
Please see the [Contributors](http://islandora.ca/resources/contributors) pages
on Islandora.ca for more information.
We recommend using the
[islandora-playbook](https://github.com/Islandora-Devops/islandora-playbook) to
get started.
## License
[GPLv2](http://www.gnu.org/licenses/gpl-2.0.txt)

37
modules/islandora_advanced_search/css/islandora_advanced_search.form.css

@ -0,0 +1,37 @@
.islandora-advanced-search-form .form-type-select {
display: inline-block;
}
.islandora-advanced-search-form .form-type-select__select-wrapper {
width: auto;
}
.islandora-advanced-search-form .form-select {
margin-right: 0.25em;
}
input.islandora-advanced-search-form__add,
input.islandora-advanced-search-form__remove {
display: inline-block;
background: none !important;
border: none;
box-shadow: none;
color: #0c6170;
padding: 0 !important;
text-decoration: none;
margin: 0 0 1rem;
}
input.islandora-advanced-search-form__add:hover,
input.islandora-advanced-search-form__add:focus,
input.islandora-advanced-search-form__remove:hover,
input.islandora-advanced-search-form__remove:focus {
text-decoration: underline;
color: #0c6170;
outline: none;
}
input.islandora-advanced-search-form__reset,
input.islandora-advanced-search-form__search {
display: inline-block;
}

111
modules/islandora_advanced_search/css/islandora_advanced_search.pager.css

@ -0,0 +1,111 @@
.islandora_advanced_search_result_pager .pager__summary {
font-weight: 700;
}
.islandora_advanced_search_result_pager .pager__group {
margin: 1.25rem 0;
padding: 1rem 0;
border-top: 1px solid;
border-bottom: 1px solid;
border-color: #e5e5e5;
display: flex;
justify-content: flex-start;
align-items: center;
flex-flow: row wrap;
}
@media all and (min-width: 45.063em) {
.islandora_advanced_search_result_pager .pager__group {
justify-content: flex-end;
}
.islandora_advanced_search_result_pager .pager__group > * {
margin: 0.47214rem 0 0.47214rem 2.61803rem;
}
.islandora_advanced_search_result_pager .pager__group > *:first-child {
margin-left: 0;
}
}
.islandora_advanced_search_result_pager .pager__group > * {
margin: 0.47214rem 2rem 0.47214rem 0;
}
.islandora_advanced_search_result_pager .pager__group > *:last-child {
margin-right: 0;
}
.islandora_advanced_search_result_pager .pager__group .item-list__list,
.islandora_advanced_search_result_pager .pager__group .item-list__title,
.islandora_advanced_search_result_pager .pager__group .item-list__item {
display: inline;
}
.islandora_advanced_search_result_pager .pager__group .item-list__title {
font-size: initial;
margin: 0.25rem;
}
.pager {
margin: initial;
}
.pager__item {
margin: 0.125rem;
text-align: center;
}
.pager__items {
text-align: right;
}
@media all and (max-width: 45em) {
.pager__items {
text-align: center;
}
}
.pager__items__first-previous,
.pager__items__num-pages,
.pager__items__next-last {
display: inline;
}
.pager__items__first-previous,
.pager__items__next-last {
float: none;
}
.pager__items__first-previous .pager__item,
.pager__items__next-last .pager__item {
display: inline;
}
.pager .pager__link,
.pager__results .pager__link {
display: inline-block;
border-radius: 0.125em;
border: 1px solid;
transition: all, 0.2s, ease-in-out;
min-width: 1.75em;
padding: 0.125rem 0.4375rem 0;
}
.pager .pager__link:focus,
.pager .pager__link:hover,
.pager__results .pager__link:focus,
.pager__results .pager__link:hover {
text-decoration: underline;
}
.pager__display .pager__link {
background-color: #ffffff;
}
.pager__display .pager__link:hover,
.pager__display .pager__link:focus {
background-color: #ffffff;
}
.pager__link--is-active {
text-decoration: underline;
}

BIN
modules/islandora_advanced_search/docs/advanced_search_block_settings.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

BIN
modules/islandora_advanced_search/docs/basic-input.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

BIN
modules/islandora_advanced_search/docs/contextual_filter_settings.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

BIN
modules/islandora_advanced_search/docs/demo.gif

Binary file not shown.

After

Width:  |  Height:  |  Size: 645 KiB

BIN
modules/islandora_advanced_search/docs/enable_index_hierarchy.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

BIN
modules/islandora_advanced_search/docs/enable_index_hierarchy_processor.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
modules/islandora_advanced_search/docs/exclude_facet_settings_exclude.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

BIN
modules/islandora_advanced_search/docs/exclude_facet_settings_url_alias.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

BIN
modules/islandora_advanced_search/docs/facet_block_settings.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

BIN
modules/islandora_advanced_search/docs/field_decedent_of.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

BIN
modules/islandora_advanced_search/docs/include_exclude_facets.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
modules/islandora_advanced_search/docs/include_exclude_facets_settings.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
modules/islandora_advanced_search/docs/islandora_advanced_search_settings.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

BIN
modules/islandora_advanced_search/docs/pager.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
modules/islandora_advanced_search/docs/pager_settings.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

BIN
modules/islandora_advanced_search/docs/sort_criteria.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

BIN
modules/islandora_advanced_search/docs/view_advanced_setting.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

13
modules/islandora_advanced_search/islandora_advanced_search.info.yml

@ -0,0 +1,13 @@
# This .info.yml files provides the basic information about our module to Drupal
# More: https://www.drupal.org/node/2000204
name: 'Islandora Advanced Search'
description: "Creates an Advanced Search block and other enhancements to search."
type: module
package: Islandora
core_version_requirement: ^9 || ^10
dependencies:
- drupal:facets
- drupal:facets_summary
- drupal:search_api_solr
lifecycle: deprecated
lifecycle_link: https://groups.google.com/g/islandora/c/SEOAWJrfE_M

17
modules/islandora_advanced_search/islandora_advanced_search.libraries.yml

@ -0,0 +1,17 @@
advanced.search.admin:
js:
js/islandora_advanced_search.admin.js: {}
dependencies:
- core/drupal.tabledrag
advanced.search.form:
js:
js/islandora_advanced_search.form.js: {}
css:
component:
css/islandora_advanced_search.form.css: {}
advanced.search.pager:
css:
component:
css/islandora_advanced_search.pager.css: {}

6
modules/islandora_advanced_search/islandora_advanced_search.links.menu.yml

@ -0,0 +1,6 @@
islandora_advanced_search.settings:
title: 'Advanced Search Settings'
route_name: islandora_advanced_search.settings
description: 'Configure Islandora Advanced Search settings'
parent: system.admin_config_islandora
weight: 99

89
modules/islandora_advanced_search/islandora_advanced_search.module

@ -0,0 +1,89 @@
<?php
/**
* @file
* Contains islandora_advanced_search.module.
*
* This file is part of the Islandora Project.
*
* (c) Islandora Foundation
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
use Drupal\block\Entity\Block;
use Drupal\Core\Form\FormStateInterface;
use Drupal\islandora_advanced_search\AdvancedSearchQuery;
use Drupal\islandora_advanced_search\Utilities;
use Drupal\search_api\Query\QueryInterface as DrupalQueryInterface;
use Drupal\views\ViewExecutable;
use Solarium\Core\Query\QueryInterface as SolariumQueryInterface;
/**
* Implements hook_search_api_solr_converted_query_alter().
*/
function islandora_advanced_search_search_api_solr_converted_query_alter(SolariumQueryInterface $solarium_query, DrupalQueryInterface $search_api_query) {
// We must modify the query itself rather than the representation the
// search_api presents as it is not possible to use the 'OR' operator
// with it as it converts conditions into separate filter queries.
// Additionally filter queries do not affect the score so are not
// suitable for use in the advanced search queries.
$advanced_search_query = new AdvancedSearchQuery();
$advanced_search_query->alterQuery(\Drupal::request(), $solarium_query, $search_api_query);
}
/**
* Implements hook_form_form_id_alter().
*/
function islandora_advanced_search_form_block_form_alter(&$form, FormStateInterface $form_state, $form_id) {
// Islandora removes this condition from the form, but we require it.
// So we can show blocks for nodes which belong to specific models.
// Allowing us to add a block for collections only.
$visibility = [];
$entity_id = $form['id']['#default_value'];
$block = Block::load($entity_id);
if ($block) {
$visibility = $block->getVisibility();
}
$manager = \Drupal::getContainer()->get('plugin.manager.condition');
$condition_id = 'node_has_term';
/** @var \Drupal\Core\Condition\ConditionInterface $condition */
$condition = $manager->createInstance($condition_id, isset($visibility[$condition_id]) ? $visibility[$condition_id] : []);
$form_state->set(['conditions', $condition_id], $condition);
$condition_form = $condition->buildConfigurationForm([], $form_state);
$condition_form['#type'] = 'details';
$condition_form['#title'] = $condition->getPluginDefinition()['label'];
$condition_form['#group'] = 'visibility_tabs';
// Not all blocks are required to give this field.
$condition_form['term']['#required'] = FALSE;
$form['visibility'][$condition_id] = $condition_form;
}
/**
* Implements hook_preprocess_preprocess_views_view().
*/
function islandora_advanced_search_preprocess_views_view(&$variables) {
/** @var \Drupal\views\ViewExecutable $view */
$view = &$variables['view'];
$views = Utilities::getPagerViewDisplays();
// Only add the toggle class for view display on displays in which the pager
// has been created for.
if (in_array([$view->id(), $view->current_display], $views)) {
// Toggle between 'list' and 'grid' display depending on url parameter.
$format = \Drupal::request()->query->get('display') ?? 'list';
$variables['attributes']['class'][] = "view-{$format}";
$view->element['#attached']['library'][] = 'islandora_advanced_search/advanced.search.pager';
}
$view = &$variables['view'];
}
/**
* Implements hook_views_pre_view().
*/
function islandora_advanced_search_views_pre_view(ViewExecutable $view, $display_id, array &$args) {
// Allow for recursive searches by disabling contextual filter.
$advanced_search_query = new AdvancedSearchQuery();
$advanced_search_query->alterView(\Drupal::request(), $view, $display_id);
}

17
modules/islandora_advanced_search/islandora_advanced_search.routing.yml

@ -0,0 +1,17 @@
islandora_advanced_search.ajax.blocks:
path: '/islandora-advanced-search-ajax-blocks'
defaults:
_controller: '\Drupal\islandora_advanced_search\Controller\AjaxBlocksController::respond'
requirements:
# Allow public access to search blocks.
_access: 'TRUE'
islandora_advanced_search.settings:
path: '/admin/config/search/advanced'
defaults:
_form: '\Drupal\islandora_advanced_search\Form\SettingsForm'
_title: 'Islandora Advanced Search Settings'
requirements:
_permission: 'administer site configuration'
options:
_admin_route: TRUE

113
modules/islandora_advanced_search/js/islandora_advanced_search.admin.js

@ -0,0 +1,113 @@
//# sourceURL=modules/contrib/islandora_advanced_search/js/islandora-advanced-search.admin.js
/**
* @file
* Largely based on core/modules/blocks/js/blocks.js
*
* This file allows for moving rows between two regions in a table and have the
* 'region' field update appropriately.
*/
(function ($, window, Drupal) {
Drupal.behaviors.islandoraAdvancedSearchAdmin = {
attach: function attach(context, settings) {
if (typeof Drupal.tableDrag === 'undefined' || typeof Drupal.tableDrag['advanced-search-fields'] === 'undefined') {
return;
}
function checkEmptyRegions(table, rowObject) {
table.find('tr.region-message').each(function () {
var $this = $(this);
if ($this.prev('tr').get(0) === rowObject.element) {
if (rowObject.method !== 'keyboard' || rowObject.direction === 'down') {
rowObject.swap('after', this);
}
}
if ($this.next('tr').is(':not(.draggable)') || $this.next('tr').length === 0) {
$this.removeClass('region-populated').addClass('region-empty');
} else if ($this.is('.region-empty')) {
$this.removeClass('region-empty').addClass('region-populated');
}
});
}
function updateLastPlaced(table, rowObject) {
table.find('.color-success').removeClass('color-success');
var $rowObject = $(rowObject);
if (!$rowObject.is('.drag-previous')) {
table.find('.drag-previous').removeClass('drag-previous');
$rowObject.addClass('drag-previous');
}
}
function updateFieldWeights(table, region) {
var weight = -Math.round(table.find('.draggable').length / 2);
table.find('.region-' + region + '-message').nextUntil('.region-title').find('select.field-weight').val(function () {
return ++weight;
});
}
var table = $('#advanced-search-fields');
var tableDrag = Drupal.tableDrag['advanced-search-fields'];
tableDrag.row.prototype.onSwap = function (swappedRow) {
checkEmptyRegions(table, this);
updateLastPlaced(table, this);
};
tableDrag.onDrop = function () {
var dragObject = this;
var $rowElement = $(dragObject.rowObject.element);
var regionRow = $rowElement.prevAll('tr.region-message').get(0);
var regionName = regionRow.className.replace(/([^ ]+[ ]+)*region-([^ ]+)-message([ ]+[^ ]+)*/, '$2');
var regionField = $rowElement.find('select.field-display');
if (regionField.find('option[value=' + regionName + ']').length === 0) {
window.alert(Drupal.t('The field cannot be placed in this region.'));
regionField.trigger('change');
}
if (!regionField.is('.field-display-' + regionName)) {
var weightField = $rowElement.find('select.field-weight');
var oldRegionName = weightField[0].className.replace(/([^ ]+[ ]+)*field-weight-([^ ]+)([ ]+[^ ]+)*/, '$2');
regionField.removeClass('field-display-' + oldRegionName).addClass('field-display-' + regionName);
weightField.removeClass('field-weight-' + oldRegionName).addClass('field-weight-' + regionName);
regionField.val(regionName);
}
updateFieldWeights(table, regionName);
};
$(context).find('select.field-display').once('field-display').on('change', function (event) {
var row = $(this).closest('tr');
var select = $(this);
tableDrag.rowObject = new tableDrag.row(row[0]);
var regionMessage = table.find('.region-' + select[0].value + '-message');
var regionItems = regionMessage.nextUntil('.region-message, .region-title');
if (regionItems.length) {
regionItems.last().after(row);
} else {
regionMessage.after(row);
}
updateFieldWeights(table, select[0].value);
checkEmptyRegions(table, tableDrag.rowObject);
updateLastPlaced(table, row);
if (!tableDrag.changed) {
$(Drupal.theme('tableDragChangedWarning')).insertBefore(tableDrag.table).hide().fadeIn('slow');
tableDrag.changed = true;
}
select.trigger('blur');
});
}
};
})(jQuery, window, Drupal);

124
modules/islandora_advanced_search/js/islandora_advanced_search.form.js

@ -0,0 +1,124 @@
//# sourceURL=modules/contrib/islandora/modules/islandora_advanced_search/js/islandora-advanced-search.form.js
/**
* @file
* Handles Ajax submission / updating form action on url change, etc.
*/
(function ($, Drupal, drupalSettings) {
// Gets current parameters minus ones provided by the form.
function getParams(query_parameter, recurse_parameter) {
const url_search_params = new URLSearchParams(window.location.search);
const params = Object.fromEntries(url_search_params.entries());
// Remove Advanced Search Query Parameters.
const param_match = "query\\[\\d+\\]\\[.+\\]".replace("query", query_parameter);
const param_regex = new RegExp(param_match, "g");
for (const param in params) {
if (param.match(param_regex)) {
delete params[param];
}
}
// Remove Recurse parameter.
delete params[recurse_parameter];
// Remove the page if set as submitting the form should always take
// the user to the first page (facets do the same).
delete params["page"];
return params;
}
// Groups form inputs by search term.
function getTerms(inputs) {
const input_regex = /terms\[(?<index>\d+)\]\[(?<component>.*)\]/;
const terms = [];
for (const input in inputs) {
const name = inputs[input].name;
const value = inputs[input].value;
const found = name.match(input_regex);
if (found) {
const index = parseInt(found.groups.index);
const component = found.groups.component;
if (typeof terms[index] !== 'object') {
terms[index] = {};
}
terms[index][component] = value;
}
}
return terms;
}
// Checks if the form user has set recursive to true in the form.
function getRecurse(inputs) {
for (const input in inputs) {
const name = inputs[input].name;
const value = inputs[input].value;
if (name == "recursive" && value == "1") {
return true;
}
}
return false;
}
function url(inputs, settings) {
const terms = getTerms(inputs);
const recurse = getRecurse(inputs);
const params = getParams(settings.query_parameter, settings.recurse_parameter);
for (const index in terms) {
const term = terms[index];
// Do not include terms with no value.
if (term.value.length != 0) {
for (const component in term) {
const value = term[component];
const param = "query[index][component]"
.replace("query", settings.query_parameter)
.replace("index", index)
.replace("component", settings.mapping[component]);
params[param] = value;
}
}
}
if (recurse) {
params[settings.recurse_parameter] = '1';
}
return window.location.href.split("?")[0] + "?" + $.param(params);
}
Drupal.behaviors.islandora_advanced_search_form = {
attach: function (context, settings) {
if (settings.islandora_advanced_search_form.id !== 'undefined') {
const $form = $('form#' + settings.islandora_advanced_search_form.id).once();
if ($form.length > 0) {
window.addEventListener("pushstate", function (e) {
$form.attr('action', window.location.pathname + window.location.search);
});
window.addEventListener("popstate", function (e) {
if (e.state != null) {
$form.attr('action', window.location.pathname + window.location.search);
}
});
// Prevent form submission and push state instead.
//
// Logic server side / client side should match to generate the
// appropriate URL with javascript enabled or disable.
//
// If a route is set for the view display that this form is derived
// from, and we are not on the same page as that route, rely on the
// normal submit which will redirect to the appropriate page.
if (!settings.islandora_advanced_search_form.redirect) {
$form.submit(function (e) {
e.preventDefault();
e.stopPropagation();
const inputs = $form.serializeArray();
const href = url(inputs, settings.islandora_advanced_search_form);
window.history.pushState(null, document.title, href);
});
}
// Reset should trigger refresh of AJAX Blocks / Views.
$form.find('input[data-drupal-selector = "edit-reset"]').mousedown(function (e) {
const inputs = [];
const href = url(inputs, settings.islandora_advanced_search_form);
window.history.pushState(null, document.title, href);
});
}
}
}
};
})(jQuery, Drupal, drupalSettings);

254
modules/islandora_advanced_search/src/AdvancedSearchQuery.php

@ -0,0 +1,254 @@
<?php
namespace Drupal\islandora_advanced_search;
use Drupal\block\Entity\Block;
use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
use Drupal\Core\Form\FormBuilderInterface;
use Drupal\Core\Url;
use Drupal\islandora_advanced_search\Form\SettingsForm;
use Drupal\islandora_advanced_search\Plugin\Block\AdvancedSearchBlock;
use Drupal\search_api\Query\QueryInterface as DrupalQueryInterface;
use Drupal\views\ViewExecutable;
use Solarium\Core\Query\QueryInterface as SolariumQueryInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Alter current search query / view from using URL parameters.
*/
class AdvancedSearchQuery {
use GetConfigTrait;
// User can set this configuration for the module.
const DEFAULT_QUERY_PARAM = 'a';
const DEFAULT_RECURSE_PARAM = 'r';
/**
* The query parameter is how terms are passed to the query.
*
* @var string
*/
protected $queryParameter;
/**
* The recurse parameter indicates the search should be recursive or not.
*
* @var string
*/
protected $recurseParameter;
/**
* Constructs a FacetBlockAjaxController object.
*
* @param string $query_parameter
* The field to search against.
* @param string $recurse_parameter
* The field that signifies the search should be recursive.
*/
public function __construct(string $query_parameter = self::DEFAULT_QUERY_PARAM, string $recurse_parameter = self::DEFAULT_RECURSE_PARAM) {
$this->queryParameter = $query_parameter;
$this->recurseParameter = $recurse_parameter;
}
/**
* Gets the query parameter to use that stores the search terms.
*
* @return string
* The query parameter to use that stores the search terms.
*/
public static function getQueryParameter() {
return self::getConfig(SettingsForm::SEARCH_QUERY_PARAMETER, self::DEFAULT_QUERY_PARAM);
}
/**
* Gets the query parameter to use that stores the search terms.
*
* @return string
* The recurse parameter used to indicate that the search should be
* recursive.
*/
public static function getRecurseParameter() {
return self::getConfig(SettingsForm::SEARCH_RECURSIVE_PARAMETER, self::DEFAULT_RECURSE_PARAM);
}
/**
* Extracts a list of AdvancedSearchQueryTerms from the given request.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The request to parse terms from.
*
* @return \Drupal\islandora_advanced_search\AdvancedSearchQueryTerm[]
* A list of search terms.
*/
public function getTerms(Request $request) {
$terms = [];
if ($request->query->has($this->queryParameter)) {
$query_params = $request->query->get($this->queryParameter);
if (is_array($query_params)) {
foreach ($query_params as $params) {
$terms[] = AdvancedSearchQueryTerm::fromQueryParams($params);
}
}
}
return array_filter($terms);
}
/**
* Checks if the query should recursively include sub-collections.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The request to parse.
*
* @return bool
* TRUE if the search should recurse FALSE otherwise.
*/
public function shouldRecurse(Request $request) {
if ($request->query->has($this->recurseParameter)) {
$recurse_param = $request->query->get($this->recurseParameter);
return filter_var($recurse_param, FILTER_VALIDATE_BOOLEAN);
}
return FALSE;
}
/**
* Checks if the all of the given terms are negations or not.
*
* @param \Drupal\islandora_advanced_search\AdvancedSearchQueryTerm[] $terms
* The terms to search for.
*
* @return bool
* TRUE if all terms are to be excluded otherwise FALSE.
*/
protected function negativeQuery(array $terms) {
foreach ($terms as $term) {
if ($term->getInclude()) {
return FALSE;
}
}
return TRUE;
}
/**
* Alters the given query using search terms provided in the given request.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The request to parse terms from.
* @param \Solarium\Core\Query\QueryInterface $solarium_query
* The solr query to modify.
* @param \Drupal\search_api\Query\QueryInterface $search_api_query
* The search api query from which the solr query was build.
*/
public function alterQuery(Request $request, SolariumQueryInterface &$solarium_query, DrupalQueryInterface $search_api_query) {
// Only apply if a Advanced Search Query was made.
$terms = $this->getTerms($request);
if (!empty($terms)) {
$index = $search_api_query->getIndex();
/** @var \Drupal\search_api_solr\Plugin\search_api\backend\SearchApiSolrBackend $backend */
$backend = $index->getServerInstance()->getBackend();
$language_ids = $search_api_query->getLanguages();
$field_mapping = $backend->getSolrFieldNamesKeyedByLanguage($language_ids, $index);
$q[] = "{!boost b=boost_document}";
// To support negative queries we must first bring in all documents.
$q[] = $this->negativeQuery($terms) ? "*:*" : "";
$term = array_shift($terms);
$q[] = $term->toSolrQuery($field_mapping);
foreach ($terms as $term) {
$q[] = $term->getConjunction();
$q[] = $term->toSolrQuery($field_mapping);
}
$q = implode(' ', $q);
/** @var Solarium\QueryType\Select\Query\Query $solarium_query */
$solarium_query->setQuery($q);
}
}
/**
* Alters the given view to be recursive if applicable.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The request to parse terms from.
* @param \Drupal\views\ViewExecutable $view
* The view to modify.
* @param string $display_id
* The view display to potentially alter.
*/
public function alterView(Request $request, ViewExecutable $view, $display_id) {
$views = Utilities::getAdvancedSearchViewDisplays();
// Only specify contextual filters for views which the advanced search
// blocks are derived from.
$block_id = array_search([$view->id(), $display_id], $views);
if ($block_id !== FALSE) {
$block = Block::load($block_id);
$settings = $block->get('settings');
// Ignore the immediate children contextual filter in the query to allow
// for recursive search.
if (isset($settings[AdvancedSearchBlock::SETTING_CONTEXTUAL_FILTER])) {
$display = $view->getDisplay();
$display_arguments = $display->getOption('arguments');
$immediate_children_contextual_filter = $settings[AdvancedSearchBlock::SETTING_CONTEXTUAL_FILTER];
$index = array_search($immediate_children_contextual_filter, array_keys($display_arguments));
if ($this->shouldRecurse($request)) {
// Change the argument to the exception value which should cause the
// contextual filter to be ignored.
$view->args[$index] = $display_arguments[$immediate_children_contextual_filter]['exception']['value'];
}
else {
// Explicitly set the default argument for AJAX requests.
// We need to restore the default as that functionality is currently
// broken. @see https://www.drupal.org/project/drupal/issues/3173778
//
// We fake the current request from the refer only to set the default
// argument in case it is build from the URL. If this is not an AJAX
// request this logic can be ignored.
if ($request->isXmlHttpRequest()) {
$view->initHandlers();
$request_stack = \Drupal::requestStack();
$refer = Request::create($request->server->get('HTTP_REFERER'));
$refer->getPathInfo();
$refer->attributes->add(\Drupal::getContainer()->get('router')->matchRequest($refer));
$request_stack->push($refer);
$plugin = $view->argument[$immediate_children_contextual_filter]->getPlugin('argument_default');
if ($plugin) {
$view->args[$index] = $plugin->getArgument();
}
$request_stack->pop();
}
}
}
}
}
/**
* Get query parameter for all search terms.
*
* @return \Drupal\Core\Url
* Url for the given request combined with search query parameters.
*/
public function toUrl(Request $request, array $terms, bool $recurse, $route = NULL) {
$query_params = $request->query->all();
if ($route) {
$url = Url::fromRoute($route);
// The form that built the url may use AJAX, but we are redirecting to a
// new page, so it should be disabled.
unset($query_params[FormBuilderInterface::AJAX_FORM_REQUEST]);
unset($query_params[MainContentViewSubscriber::WRAPPER_FORMAT]);
}
else {
$url = Url::createFromRequest($request);
}
unset($query_params[$this->queryParameter]);
foreach ($terms as $term) {
$query_params[$this->queryParameter][] = $term->toQueryParams();
}
if ($recurse) {
$query_params[$this->recurseParameter] = '1';
}
else {
unset($query_params[$this->recurseParameter]);
}
$url->setOptions(['query' => $query_params]);
return $url;
}
}

294
modules/islandora_advanced_search/src/AdvancedSearchQueryTerm.php

@ -0,0 +1,294 @@
<?php
namespace Drupal\islandora_advanced_search;
use Drupal\islandora_advanced_search\Form\AdvancedSearchForm;
/**
* Defines a single search term.
*
* Used for parsing query parameters as well as form submission and generating
* search queries.
*/
class AdvancedSearchQueryTerm {
// Conjunctions.
// @see https://lucene.apache.org/solr/guide/7_1/the-standard-query-parser.html#TheStandardQueryParser-BooleanOperatorsSupportedbytheStandardQueryParser
const CONJUNCTION_AND = 'AND';
const CONJUNCTION_OR = 'OR';
// Used for serializing / deserializing query parameters.
// These are also hard-coded in islandora_advanced_search.form.js.
const CONJUNCTION_QUERY_PARAMETER = 'c';
const FIELD_QUERY_PARAMETER = 'f';
const INCLUDE_QUERY_PARAMETER = 'i';
const VALUE_QUERY_PARAMETER = 'v';
// Defaults.
const DEFAULT_CONJUNCTION = self::CONJUNCTION_AND;
const DEFAULT_INCLUDE = TRUE;
/**
* The field to search.
*
* @var string
*/
protected $field;
/**
* Include / exclude results where 'value' is in the 'search' term.
*
* @var bool
*/
protected $include = TRUE;
/**
* The value to filter with.
*
* @var string
*/
protected $value;
/**
* The conjunction to use for the condition group – either 'AND' or 'OR'.
*
* @var string
*/
protected $conjunction;
/**
* Constructs a FacetBlockAjaxController object.
*
* @param string $field
* The field to search against.
* @param string $value
* The value to search the field with.
* @param bool $include
* Limit results to records whose field contains or does not contain the
* given value.
* @param string $conjunction
* The conjunction to apply when combining this search term along with
* others.
*/
public function __construct(string $field, string $value, bool $include = self::DEFAULT_INCLUDE, string $conjunction = self::DEFAULT_CONJUNCTION) {
$this->field = $field;
$this->value = $value;
switch ($conjunction) {
case self::CONJUNCTION_AND:
case self::CONJUNCTION_OR:
$this->conjunction = $conjunction;
break;
default:
throw new \InvalidArgumentException('Invalid value given for argument "conjunction": $conjunction');
}
if ($this->conjunction == self::CONJUNCTION_OR && !$include) {
throw new \InvalidArgumentException('Excluding terms with the conjunction "OR" is not supported');
}
$this->include = $include;
}
/**
* Validate 'include' or fallback to default value.
*
* @param string $include
* The value to cast to a boolean if possible.
*
* @return bool
* The normalized input for 'include' or its default.
*/
protected static function normalizeInclude(string $include) {
switch (strtoupper($include)) {
case AdvancedSearchForm::IS_OP:
return TRUE;
case AdvancedSearchForm::NOT_OP:
return FALSE;
default:
$include = filter_var($include, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
// Ignore include parameter if invalid and fallback to the default.
return is_bool($include) ? $include : self::DEFAULT_INCLUDE;
}
}
/**
* Validate 'conjunction' or fallback to default value.
*
* @param string $conjunction
* The conjunction to validate.
*
* @return string
* The normalized input for 'include' or its default.
*/
protected static function normalizeConjunction(string $conjunction) {
switch (strtoupper($conjunction)) {
case self::CONJUNCTION_AND:
return self::CONJUNCTION_AND;
case self::CONJUNCTION_OR:
return self::CONJUNCTION_OR;
default:
return self::DEFAULT_CONJUNCTION;
}
}
/**
* Creates a AdvancedSearchQueryTerm from the given parameters if possible.
*
* @param array $params
* An array representing the query parameters for a single search term.
*
* @return \Drupal\islandora_advanced_search\AdvancedSearchQueryTerm|null
* An object which represents a valid search term.
*/
public static function fromQueryParams(array $params) {
// Field & value are required values. We do not check if field is a valid
// value only that it is non-empty. All other fields will be cast to
// defaults if they are not valid / missing.
$has_required_params = isset($params[self::FIELD_QUERY_PARAMETER], $params[self::VALUE_QUERY_PARAMETER]);
$search_value_empty = isset($params[self::VALUE_QUERY_PARAMETER]) && empty($params[self::VALUE_QUERY_PARAMETER]);
if (!$has_required_params || $search_value_empty) {
return NULL;
}
$field = $params[self::FIELD_QUERY_PARAMETER];
$value = $params[self::VALUE_QUERY_PARAMETER];
$include = isset($params[self::INCLUDE_QUERY_PARAMETER]) ?
$include = self::normalizeInclude($params[self::INCLUDE_QUERY_PARAMETER]) :
self::DEFAULT_INCLUDE;
$conjunction = isset($params[self::CONJUNCTION_QUERY_PARAMETER]) ?
self::normalizeConjunction($params[self::CONJUNCTION_QUERY_PARAMETER]) :
self::DEFAULT_CONJUNCTION;
return new self($field, $value, $include, $conjunction);
}
/**
* Creates a AdvancedSearchQueryTerm from user submitted form values.
*
* @param array $input
* An array representing the submitted form values for a single search term.
*
* @return \Drupal\islandora_advanced_search\AdvancedSearchQueryTerm|null
* An object which represents a valid search term.
*/
public static function fromUserInput(array $input) {
// Search field & value are required values we do not check if field is a
// valid value only that it is non-empty. All other fields will use
// defaults if they are not valid / missing.
$has_required_inputs = isset($input[AdvancedSearchForm::SEARCH_FORM_FIELD], $input[AdvancedSearchForm::VALUE_FORM_FIELD]);
$search_value_empty = isset($input[AdvancedSearchForm::VALUE_FORM_FIELD]) && empty($input[AdvancedSearchForm::VALUE_FORM_FIELD]);
if (!$has_required_inputs || $search_value_empty) {
return NULL;
}
$field = $input[AdvancedSearchForm::SEARCH_FORM_FIELD];
$value = $input[AdvancedSearchForm::VALUE_FORM_FIELD];
$include = self::DEFAULT_INCLUDE;
$conjunction = self::DEFAULT_CONJUNCTION;
if (isset($input[AdvancedSearchForm::CONJUNCTION_FORM_FIELD])) {
switch ($input[AdvancedSearchForm::CONJUNCTION_FORM_FIELD]) {
case AdvancedSearchForm::AND_OP:
$conjunction = self::CONJUNCTION_AND;
break;
case AdvancedSearchForm::OR_OP:
$conjunction = self::CONJUNCTION_OR;
break;
}
}
// Only allow users to specify include when using 'AND' conjunction.
if (
$conjunction == self::CONJUNCTION_AND
&& isset($input[AdvancedSearchForm::INCLUDE_FORM_FIELD])
) {
switch ($input[AdvancedSearchForm::INCLUDE_FORM_FIELD]) {
case AdvancedSearchForm::IS_OP:
$include = TRUE;
break;
case AdvancedSearchForm::NOT_OP:
$include = FALSE;
break;
}
}
return new self($field, $value, $include, $conjunction);
}
/**
* Get query parameter representation of this search term.
*
* @return array
* Representation of this search term which can be serialized to a query
* parameter.
*/
public function toQueryParams() {
$params = [
self::FIELD_QUERY_PARAMETER => $this->field,
self::VALUE_QUERY_PARAMETER => $this->value,
];
// No need to specify conjunction if it is equivalent to the default.
if ($this->conjunction != self::DEFAULT_CONJUNCTION) {
$params[self::CONJUNCTION_QUERY_PARAMETER] = $this->conjunction;
}
if ($this->include != self::DEFAULT_CONJUNCTION) {
$params[self::INCLUDE_QUERY_PARAMETER] = $this->include ? '1' : '0';
}
return $params;
}
/**
* Get user input of search form representation of this search term.
*
* @return array
* Representation of this search term which can be used as input to the
* advanced search form.
*/
public function toUserInput() {
return [
AdvancedSearchForm::SEARCH_FORM_FIELD => $this->field,
AdvancedSearchForm::VALUE_FORM_FIELD => $this->value,
AdvancedSearchForm::INCLUDE_FORM_FIELD => $this->include ? AdvancedSearchForm::IS_OP : AdvancedSearchForm::NOT_OP,
AdvancedSearchForm::CONJUNCTION_FORM_FIELD => $this->conjunction == self::CONJUNCTION_AND ? AdvancedSearchForm::AND_OP : AdvancedSearchForm::OR_OP,
];
}
/**
* Gets if this term should be included / excluded from results.
*
* @return bool
* TRUE if the term should be include in results, FALSE otherwise.
*/
public function getInclude() {
return $this->include;
}
/**
* Gets the conjunction for this term.
*
* @return string
* The conjunction to use for this term.
*/
public function getConjunction() {
return $this->conjunction;
}
/**
* Using the provided field mapping create a Solr Query string.
*
* @param array $solr_field_mapping
* An array that maps search api fields to one or more solr fields.
*
* @return string
* The conjunction to use for this term conjunction.
*/
public function toSolrQuery(array $solr_field_mapping) {
$terms = [];
$query_helper = \Drupal::service('solarium.query_helper');
$value = $query_helper->escapePhrase(trim($this->value));
foreach ($solr_field_mapping[$this->field] as $field) {
$terms[] = "$field:$value";
}
$terms = implode(' ', $terms);
return $this->include ? "($terms)" : "-($terms)";
}
}

165
modules/islandora_advanced_search/src/Controller/AjaxBlocksController.php

@ -0,0 +1,165 @@
<?php
namespace Drupal\islandora_advanced_search\Controller;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\ReplaceCommand;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Path\CurrentPathStack;
use Drupal\Core\PathProcessor\PathProcessorManager;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Routing\CurrentRouteMatch;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Routing\RouterInterface;
/**
* Defines a controller to load a facet via AJAX.
*/
class AjaxBlocksController extends ControllerBase {
/**
* The entity storage for block.
*
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
protected $storage;
/**
* The renderer.
*
* @var \Drupal\Core\Render\RendererInterface
*/
protected $renderer;
/**
* The current path.
*
* @var \Drupal\Core\Path\CurrentPathStack
*/
protected $currentPath;
/**
* The dynamic router service.
*
* @var \Symfony\Component\Routing\Matcher\RequestMatcherInterface
*/
protected $router;
/**
* The path processor service.
*
* @var \Drupal\Core\PathProcessor\InboundPathProcessorInterface
*/
protected $pathProcessor;
/**
* The current route match service.
*
* @var \Drupal\Core\Routing\CurrentRouteMatch
*/
protected $currentRouteMatch;
/**
* The service container this instance should use.
*
* @var \Symfony\Component\DependencyInjection\ContainerInterface
*/
protected $container;
/**
* Constructs a FacetBlockAjaxController object.
*
* @param \Drupal\Core\Render\RendererInterface $renderer
* The renderer service.
* @param \Drupal\Core\Path\CurrentPathStack $currentPath
* The current path service.
* @param \Symfony\Component\Routing\RouterInterface $router
* The router service.
* @param \Drupal\Core\PathProcessor\PathProcessorManager $pathProcessor
* The path processor manager.
* @param \Drupal\Core\Routing\CurrentRouteMatch $currentRouteMatch
* The current route match service.
* @param \Symfony\Component\DependencyInjection\ContainerInterface $container
* The drupal container.
*/
public function __construct(RendererInterface $renderer, CurrentPathStack $currentPath, RouterInterface $router, PathProcessorManager $pathProcessor, CurrentRouteMatch $currentRouteMatch, ContainerInterface $container) {
$this->storage = $this->entityTypeManager()->getStorage('block');
$this->renderer = $renderer;
$this->currentPath = $currentPath;
$this->router = $router;
$this->pathProcessor = $pathProcessor;
$this->currentRouteMatch = $currentRouteMatch;
$this->container = $container;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('renderer'),
$container->get('path.current'),
$container->get('router'),
$container->get('path_processor_manager'),
$container->get('current_route_match'),
$container
);
}
/**
* Loads and renders the facet blocks via AJAX.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The current request object.
*
* @return \Drupal\Core\Ajax\AjaxResponse
* The ajax response.
*
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
* Thrown when the view was not found.
*/
public function respond(Request $request) {
$response = new AjaxResponse();
// Rebuild the request and the current path, needed for facets.
$path = $request->request->get('link');
$blocks = $request->request->get('blocks');
// Make sure we are not updating blocks multiple times.
$blocks = array_unique($blocks);
if (empty($path) || empty($blocks)) {
throw new NotFoundHttpException('No facet link or facet blocks found.');
}
$new_request = Request::create($path);
$request_stack = new RequestStack();
$processed = $this->pathProcessor->processInbound($new_request->getPathInfo(), $new_request);
$this->currentPath->setPath($processed);
$request->attributes->add($this->router->matchRequest($new_request));
$this->currentRouteMatch->resetRouteMatch();
$request_stack->push($new_request);
$this->container->set('request_stack', $request_stack);
// Build the facets blocks found for the current request and update.
foreach ($blocks as $block_id => $block_selector) {
$block_entity = $this->storage->load($block_id);
if ($block_entity) {
// Render a block, then add it to the response as a replace command.
$block_view = $this->entityTypeManager
->getViewBuilder('block')
->view($block_entity);
$block_view = (string) $this->renderer->renderPlain($block_view);
$response->addCommand(new ReplaceCommand($block_selector, $block_view));
}
}
return $response;
}
}

423
modules/islandora_advanced_search/src/Form/AdvancedSearchForm.php

@ -0,0 +1,423 @@
<?php
namespace Drupal\islandora_advanced_search\Form;
use Drupal\Component\Utility\Html;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\islandora_advanced_search\AdvancedSearchQuery;
use Drupal\islandora_advanced_search\AdvancedSearchQueryTerm;
use Drupal\islandora_advanced_search\GetConfigTrait;
use Drupal\views\DisplayPluginCollection;
use Drupal\views\Entity\View;
use Drupal\views\Plugin\views\display\PathPluginBase;
use Drupal\views\Views;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Form for building and Advanced Search Query.
*/
class AdvancedSearchForm extends FormBase {
use GetConfigTrait;
// Users can customize the operator to use font-awesome or some other icons.
// Its a limitation in the use of `input type=submit` rather than buttons in
// Drupal that we couldn't just rely on CSS.
// This is exposed in the module settings.
// @see https://www.drupal.org/project/drupal/issues/1671190
const DEFAULT_ADD_OP = '+';
const DEFAULT_REMOVE_OP = '-';
const AND_OP = 'AND';
const IS_OP = 'IS';
const NOT_OP = 'NOT';
const OR_OP = 'OR';
// These are also hard-coded in islandora_advanced_search.form.js.
const CONJUNCTION_FORM_FIELD = 'conjunction';
const SEARCH_FORM_FIELD = 'search';
const INCLUDE_FORM_FIELD = 'include';
const VALUE_FORM_FIELD = 'value';
const AJAX_WRAPPER = 'advanced-search-ajax';
/**
* The current request.
*
* @var \Symfony\Component\HttpFoundation\Request
*/
protected $request;
/**
* The current route match.
*
* @var \Drupal\Core\Routing\RouteMatchInterface
*/
protected $currentRouteMatch;
/**
* Class constructor.
*/
public function __construct(Request $request, RouteMatchInterface $current_route_match) {
$this->request = $request;
$this->currentRouteMatch = $current_route_match;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('request_stack')->getMainRequest(),
$container->get('current_route_match')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'islandora_advanced_search_form';
}
/**
* Get the character to use for adding a facet to the query.
*
* @return string
* The character to use for adding an facet to the query.
*/
public static function getAddOperator() {
return self::getConfig(SettingsForm::SEARCH_ADD_OPERATOR, self::DEFAULT_ADD_OP);
}
/**
* Get the character to use for removing a facet from the query.
*
* @return string
* The character to use for removing an facet to the query.
*/
public static function getRemoveOperator() {
return self::getConfig(SettingsForm::SEARCH_REMOVE_OPERATOR, self::DEFAULT_REMOVE_OP);
}
/**
* Convert the list of fields to select options.
*
* @param \Drupal\search_api\Item\FieldInterface[] $fields
* The fields to convert to select options.
*
* @return array
* Array of fields which can be searched where the key is the search field
* identifier and the value is its human readable label.
*/
protected function fieldOptions(array $fields) {
$options = [];
foreach ($fields as $field) {
$id = $field->getFieldIdentifier();
$options[$id] = $field->getLabel();
}
return $options;
}
/**
* Gets possible include options for the given conjunction.
*/
protected function includeOptions(string $conjunction) {
switch ($conjunction) {
case self::AND_OP:
return;
case self::OR_OP:
return [
self::IS_OP => $this->t('is'),
];
}
}
/**
* Default values to for a term.
*/
protected function defaultTermValues(array $options) {
return [
self::CONJUNCTION_FORM_FIELD => self::AND_OP,
// First item in list is default.
self::SEARCH_FORM_FIELD => key($options),
self::INCLUDE_FORM_FIELD => self::IS_OP,
self::VALUE_FORM_FIELD => NULL,
];
}
/**
* Process input to the from either URL parameters or from the form input.
*/
protected function processInput(FormStateInterface $form_state, array $term_default_values) {
$input = $form_state->getUserInput();
$recursive = isset($input['recursive']) ? $input['recursive'] : NULL;
$term_values = isset($input['terms']) && is_array($input['terms']) ? $input['terms'] : [];
// Form was not submitted see if we can rebuild from query parameters.
$advanced_search_query = new AdvancedSearchQuery();
if (empty($term_values)) {
$terms = $advanced_search_query->getTerms($this->request);
foreach ($terms as $term) {
$term_values[] = $term->toUserInput();
}
}
if (!isset($input['recursive'])) {
$recursive = $advanced_search_query->shouldRecurse($this->request);
}
// Form was submitted via +/- operators.
$trigger = $form_state->getTriggeringElement();
if ($trigger != NULL) {
$term_index = $trigger['#term_index'] ?? 0;
$value = $trigger['#value'] instanceof TranslatableMarkup ?
$trigger['#value']->getUntranslatedString() :
$trigger['#value'];
switch ($value) {
case $this->getAddOperator():
// Insert after the term listed.
array_splice($term_values, $term_index + 1, 0, [$term_default_values]);
break;
case $this->getRemoveOperator():
array_splice($term_values, $term_index, 1);
break;
case "Reset":
$recursive = FALSE;
$term_values = [];
break;
// Ignore unknown value for trigger.
}
// Place user input with updated values.
$input['terms'] = $term_values;
$input['recursive'] = $recursive;
$form_state->setUserInput($input);
}
return [$recursive, $term_values];
}
/**
* Gets the route name for the view display used to derive this forms block.
*
* @return string|null
* The route name for the view display that was used to create this
* forms block.
*/
protected function getRouteName(FormStateInterface $form_state) {
$view = $form_state->get('view');
$display = $form_state->get('display');
$display_handlers = new DisplayPluginCollection($view->getExecutable(), Views::pluginManager('display'));
$display_handler = $display_handlers->get($display['id']);
if ($display_handler instanceof PathPluginBase) {
return $display_handler->getRouteName();
}
return NULL;
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, View $view = NULL, array $display = [], array $fields = [], string $context_filter = NULL) {
// Keep reference to view and display as the submit handler may use them
// to redirect the user to the search page.
$form_state->set('view', $view);
$form_state->set('display', $display);
$route_name = $this->getRouteName($form_state);
$requires_redirect = $route_name ? $this->currentRouteMatch->getRouteName() !== $route_name : FALSE;
$form['#attached']['library'][] = 'islandora_advanced_search/advanced.search.form';
$form['#attached']['drupalSettings']['islandora_advanced_search_form'] = [
'id' => Html::getId($this->getFormId()),
'redirect' => $requires_redirect,
'query_parameter' => AdvancedSearchQuery::getQueryParameter(),
'recurse_parameter' => AdvancedSearchQuery::getRecurseParameter(),
'mapping' => [
self::CONJUNCTION_FORM_FIELD => AdvancedSearchQueryTerm::CONJUNCTION_QUERY_PARAMETER,
self::SEARCH_FORM_FIELD => AdvancedSearchQueryTerm::FIELD_QUERY_PARAMETER,
self::INCLUDE_FORM_FIELD => AdvancedSearchQueryTerm::INCLUDE_QUERY_PARAMETER,
self::VALUE_FORM_FIELD => AdvancedSearchQueryTerm::VALUE_QUERY_PARAMETER,
],
];
$options = $this->fieldOptions($fields);
$term_default_values = $this->defaultTermValues($options);
list($recursive, $term_values) = $this->processInput($form_state, $term_default_values);
$i = 0;
$term_elements = [];
$total_terms = count($term_values);
$block_class_prefix = str_replace('_', '-', $this->getFormId());
do {
// Either specified by the user in the request or use the default.
$first = $i == 0;
$term_value = !empty($term_values) ? array_shift($term_values) : $term_default_values;
$conjunction = isset($term_value[self::CONJUNCTION_FORM_FIELD]) ? $term_value[self::CONJUNCTION_FORM_FIELD] : $term_default_values[self::CONJUNCTION_FORM_FIELD];
$term_elements[] = [
// Only show on terms after the first.
self::CONJUNCTION_FORM_FIELD => $first ? NULL : [
'#type' => 'select',
'#options' => [
self::AND_OP => $this->t('and'),
self::OR_OP => $this->t('or'),
],
'#default_value' => $conjunction,
],
self::SEARCH_FORM_FIELD => [
'#type' => 'select',
'#options' => $options,
'#default_value' => $term_value[self::SEARCH_FORM_FIELD],
],
self::INCLUDE_FORM_FIELD => [
'#type' => 'select',
'#options' => [
self::IS_OP => $this->t('is'),
self::NOT_OP => $this->t('is not'),
],
'#default_value' => $term_value[self::INCLUDE_FORM_FIELD],
// Show only when conjunction is 'AND' as 'OR NOT' is not supported
// by solr and will be converted to 'AND NOT'.
'#states' => [
'visible' => [
':input[name="terms[' . $i . '][' . self::CONJUNCTION_FORM_FIELD . ']"]' => ['value' => self::AND_OP],
],
],
],
// Just markup to show when 'include' is not alterable due to the
// selected 'conjunction'. Hide for the first term.
'is' => $first ? NULL : [
'#type' => 'container',
'#attributes' => ['style' => 'display:inline;'],
'#states' => [
'visible' => [
':input[name="terms[' . $i . '][' . self::CONJUNCTION_FORM_FIELD . ']"]' => ['value' => self::OR_OP],
],
],
'content' => [
'#markup' => $this->t('is'),
],
],
self::VALUE_FORM_FIELD => [
'#type' => 'textfield',
'#default_value' => $term_value[self::VALUE_FORM_FIELD],
],
'actions' => [
'#type' => 'container',
'add' => [
'#type' => 'button',
'#value' => $this->getAddOperator(),
'#name' => 'add-term-' . $i,
'#term_index' => $i,
'#attributes' => [
'class' => [$block_class_prefix . '__add', 'fa'],
],
'#ajax' => [
'callback' => [$this, 'ajaxCallback'],
'wrapper' => self::AJAX_WRAPPER,
'progress' => [
'type' => 'none',
],
],
],
'remove' => $total_terms <= 1 ? NULL : [
'#type' => 'button',
'#value' => $this->getRemoveOperator(),
'#name' => 'remove-term-' . $i,
'#term_index' => $i,
'#attributes' => [
'class' => [$block_class_prefix . '__remove', 'fa'],
],
'#ajax' => [
'callback' => [$this, 'ajaxCallback'],
'wrapper' => self::AJAX_WRAPPER,
'progress' => [
'type' => 'none',
],
],
],
],
];
$i++;
} while (!empty($term_values));
$form['ajax'] = [
'#type' => 'container',
'#attributes' => ['id' => self::AJAX_WRAPPER],
'terms' => array_merge([
'#tree' => TRUE,
'#type' => 'container',
], $term_elements),
];
if ($context_filter != NULL) {
$form['ajax']['recursive'] = [
'#type' => 'checkbox',
'#title' => $this->t('Include Sub-Collections'),
'#default_value' => $recursive,
];
}
$form['reset'] = [
'#type' => 'button',
'#value' => $this->t('Reset'),
'#attributes' => [
'class' => [$block_class_prefix . '__reset'],
],
'#ajax' => [
'callback' => [$this, 'ajaxCallback'],
'wrapper' => self::AJAX_WRAPPER,
'progress' => [
'type' => 'none',
],
],
];
$form['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Search'),
'#attributes' => [
'class' => [$block_class_prefix . '__search'],
],
];
return $form;
}
/**
* Builds an Advanced Search Query Url from the submitted form values.
*/
protected function buildUrl(FormStateInterface $form_state) {
$terms = [];
$values = $form_state->getValues();
foreach ($values['terms'] as $term) {
$terms[] = AdvancedSearchQueryTerm::fromUserInput($term);
}
$terms = array_filter($terms);
$recurse = filter_var(isset($values['recursive']) ? $values['recursive'] : FALSE, FILTER_VALIDATE_BOOLEAN);
$route = $this->getRouteName($form_state);
$advanced_search_query = new AdvancedSearchQuery();
return $advanced_search_query->toUrl($this->request, $terms, $recurse, $route);
}
/**
* Callback for adding / removing terms from the search.
*/
public function ajaxCallback(array &$form, FormStateInterface $form_state) {
return $form['ajax'];
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$trigger = (string) $form_state->getTriggeringElement()['#value'];
switch ($trigger) {
case $this->t('Search'):
$form_state->setRedirectUrl($this->buildUrl($form_state));
break;
default:
$form_state->setRebuild();
}
}
}

122
modules/islandora_advanced_search/src/Form/SettingsForm.php

@ -0,0 +1,122 @@
<?php
namespace Drupal\islandora_advanced_search\Form;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\islandora_advanced_search\AdvancedSearchQuery;
use Drupal\islandora_advanced_search\GetConfigTrait;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Config form for Islandora Advanced Search settings.
*/
class SettingsForm extends ConfigFormBase {
use GetConfigTrait;
const CONFIG_NAME = 'islandora_advanced_search.settings';
const SEARCH_QUERY_PARAMETER = 'search_query_parameter';
const SEARCH_RECURSIVE_PARAMETER = 'search_recursive_parameter';
const SEARCH_ADD_OPERATOR = 'search_add_operator';
const SEARCH_REMOVE_OPERATOR = 'search_remove_operator';
const FACET_TRUNCATE = 'facet_truncate';
/**
* Constructs a \Drupal\system\ConfigFormBase object.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The factory for configuration objects.
*/
public function __construct(ConfigFactoryInterface $config_factory) {
$this->setConfigFactory($config_factory);
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static($container->get('config.factory'));
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'islandora_advanced_search_settings_form';
}
/**
* {@inheritdoc}
*/
protected function getEditableConfigNames() {
return [
self::CONFIG_NAME,
];
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$form += [
'search' => [
'#type' => 'fieldset',
'#title' => $this->t('Advanced Search'),
self::SEARCH_QUERY_PARAMETER => [
'#type' => 'textfield',
'#title' => $this->t('Search Query Parameter'),
'#description' => $this->t('The url parameter in which the advanced search query is stored.'),
'#default_value' => AdvancedSearchQuery::getQueryParameter(),
],
self::SEARCH_RECURSIVE_PARAMETER => [
'#type' => 'textfield',
'#title' => $this->t('Recurse Query Parameter'),
'#description' => $this->t('The url parameter which can toggle recursive search.'),
'#default_value' => AdvancedSearchQuery::getRecurseParameter(),
],
self::SEARCH_ADD_OPERATOR => [
'#type' => 'textfield',
'#title' => $this->t('Facet Add Operator'),
'#description' => $this->t('Users can customize the operator for adding facets to use font-awesome or some other icon, etc.'),
'#default_value' => AdvancedSearchForm::getAddOperator(),
],
self::SEARCH_REMOVE_OPERATOR => [
'#type' => 'textfield',
'#title' => $this->t('Facet Remove Operator'),
'#description' => $this->t('Users can customize the operator for removing facets to use font-awesome or some other icon, etc.'),
'#default_value' => AdvancedSearchForm::getRemoveOperator(),
],
],
'facets' => [
'#type' => 'fieldset',
'#title' => $this->t('Facets'),
self::FACET_TRUNCATE => [
'#type' => 'number',
'#title' => $this->t('Truncate Facet'),
'#description' => $this->t('Optionally truncate the length of facets titles in the display. If unspecified they will not be truncated.'),
'#default_value' => self::getConfig(self::FACET_TRUNCATE, 32),
'#min' => 1,
],
],
];
return parent::buildForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$config = $this->configFactory->getEditable(self::CONFIG_NAME);
$config
->set(self::SEARCH_QUERY_PARAMETER, $form_state->getValue(self::SEARCH_QUERY_PARAMETER))
->set(self::SEARCH_RECURSIVE_PARAMETER, $form_state->getValue(self::SEARCH_RECURSIVE_PARAMETER))
->set(self::SEARCH_ADD_OPERATOR, $form_state->getValue(self::SEARCH_ADD_OPERATOR))
->set(self::SEARCH_REMOVE_OPERATOR, $form_state->getValue(self::SEARCH_REMOVE_OPERATOR))
->set(self::FACET_TRUNCATE, $form_state->getValue(self::FACET_TRUNCATE))
->save();
parent::submitForm($form, $form_state);
}
}

24
modules/islandora_advanced_search/src/GetConfigTrait.php

@ -0,0 +1,24 @@
<?php
namespace Drupal\islandora_advanced_search;
use Drupal\islandora_advanced_search\Form\SettingsForm;
/**
* Simple trait for accessing this modules configuration.
*/
trait GetConfigTrait {
/**
* Get a config setting or returns a default.
*
* @return string
* The config setting or default value.
*/
protected static function getConfig($config, $default) {
$settings = \Drupal::config(SettingsForm::CONFIG_NAME);
$value = $settings->get($config);
return !empty($value) ? $value : $default;
}
}

394
modules/islandora_advanced_search/src/Plugin/Block/AdvancedSearchBlock.php

@ -0,0 +1,394 @@
<?php
namespace Drupal\islandora_advanced_search\Plugin\Block;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Form\FormBuilderInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\search_api\Display\DisplayPluginManager;
use Drupal\views\Entity\View;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Provides an Islandora Advanced Search block.
*
* @Block(
* id = "islandora_advanced_search_block",
* deriver = "Drupal\islandora_advanced_search\Plugin\Block\AdvancedSearchBlockDeriver",
* admin_label = @Translation("Islandora Advanced Search"),
* category = @Translation("Islandora"),
* )
*/
class AdvancedSearchBlock extends BlockBase implements ContainerFactoryPluginInterface {
use ViewAndDisplayIdentifiersTrait;
// CSS classes used to bind table-drag behavior to.
const WEIGHT_FIELD_CLASS = 'field-weight';
const DISPLAY_FIELD_CLASS = 'field-display';
// Regions in the table which denote if a given field
// is visible in the Advanced Search Form or not.
const REGION_VISIBLE = 'visible';
const REGION_HIDDEN = 'hidden';
// Keys for settings.
const SETTING_FIELDS = 'fields';
const SETTING_CONTEXTUAL_FILTER = 'context_filter';
/**
* The display plugin manager.
*
* @var \Drupal\search_api\Display\DisplayPluginManager
*/
protected $displayPluginManager;
/**
* The clone of the current request object.
*
* @var \Symfony\Component\HttpFoundation\Request
*/
protected $request;
/**
* The view this block affects.
*
* @var \Drupal\views\Entity\View
*/
protected $view;
/**
* The view display this block affects.
*
* @var array
*/
protected $display;
/**
* Form Builder.
*
* @var \Drupal\Core\Form\FormBuilderInterface
*/
protected $formBuilder;
/**
* Construct a AdvancedSearchBlock instance.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param string $plugin_definition
* The plugin implementation definition.
* @param \Drupal\search_api\Display\DisplayPluginManager $display_plugin_manager
* The display plugin manager.
* @param \Drupal\Core\Form\FormBuilderInterface $form_builder
* The form builder service used to build the search form.
* @param \Symfony\Component\HttpFoundation\Request $request
* A request object for the current request.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, DisplayPluginManager $display_plugin_manager, FormBuilderInterface $form_builder, Request $request) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->displayPluginManager = $display_plugin_manager;
list($view_id, $display_id) = preg_split('/__/', $this->getDerivativeId(), 2);
$this->view = View::Load($view_id);
$this->display = $this->view->getDisplay($display_id);
$this->formBuilder = $form_builder;
$this->request = clone $request;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('plugin.manager.search_api.display'),
$container->get('form_builder'),
$container->get('request_stack')->getMainRequest()
);
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return [
self::SETTING_FIELDS => [],
self::SETTING_CONTEXTUAL_FILTER => NULL,
];
}
/**
* Fields which can be enabled / disabled for display in the search form.
*
* @return \Drupal\search_api\Item\FieldInterface[]
* The $fields sorted by label.
*/
protected function getFields() {
$fields = $this->getIndex()->getFields();
// First pass sort on label, secondary sort will be used
// when looking at existing configuration for this block.
uasort($fields, function ($a, $b) {
return strcmp($a->getLabel(), $b->getLabel());
});
return $fields;
}
/**
* Get regions of table to display.
*
* @return array
* The properties of each region used for building the table of fields.
*/
protected function getRegions() {
// Classes for select fields like 'weight' and 'display' are hard-coded
// and used in js/islandora-advanced-search.admin.js.
return [
'visible' => [
'title' => $this->t('Visible'),
'invisible' => TRUE,
'message' => $this->t('No search field is visible.'),
'weight' => self::WEIGHT_FIELD_CLASS . '-visible',
'display' => self::DISPLAY_FIELD_CLASS . '-visible',
],
'hidden' => [
'title' => $this->t('Hidden'),
'invisible' => FALSE,
'message' => $this->t('No search field is hidden.'),
'weight' => self::WEIGHT_FIELD_CLASS . '-hidden',
'display' => self::DISPLAY_FIELD_CLASS . '-hidden',
],
];
}
/**
* Options for field display derived from the available regions.
*
* @return array
* Display select field options.
*/
protected function getDisplayOptions() {
$options = [];
foreach ($this->getRegions() as $region => $settings) {
$options[$region] = $settings['title'];
}
return $options;
}
/**
* {@inheritdoc}
*/
public function blockForm($form, FormStateInterface $form_state) {
// At most we will have one row per field.
$fields = $this->getFields();
$weight_delta = round(count($fields) / 2);
// Group each field into a region given our current configuration.
$visible_fields = $this->configuration[self::SETTING_FIELDS];
$regions = $this->getRegions();
$display_options = $this->getDisplayOptions();
// Field rows are grouped by the region in which they are displayed.
$field_rows = array_fill_keys(array_keys($regions), []);
foreach ($fields as $field) {
// If a field exists in the blocks configuration than it is 'visible' and
// its weight is equivalent to its order in the configuration,
// i.e. its index.
$identifier = $field->getFieldIdentifier();
$weight = array_search($identifier, $visible_fields);
$visible = $weight !== FALSE;
$region = $visible ? self::REGION_VISIBLE : self::REGION_HIDDEN;
$field_rows[$region][$identifier] = [
'#attributes' => [
'class' => ['draggable'],
],
'label' => ['#plain_text' => $field->getLabel()],
'identifier' => ['#plain_text' => $identifier],
'weight' => [
'#type' => 'weight',
'#title' => $this->t('Weight'),
'#title_display' => 'invisible',
'#default_value' => $visible ? $weight : 0,
'#delta' => $weight_delta,
'#attributes' => [
'class' => [self::WEIGHT_FIELD_CLASS, $regions[$region]['weight']],
],
],
'display' => [
'#type' => 'select',
'#title' => $this->t('Display'),
'#title_display' => 'invisible',
'#options' => $display_options,
'#default_value' => $region,
'#attributes' => [
'class' => [self::DISPLAY_FIELD_CLASS, $regions[$region]['display']],
],
],
];
}
// Sort the visible rows by their weight.
uasort($field_rows[self::REGION_VISIBLE], function ($a, $b) {
$a = $a['weight']['#default_value'];
$b = $b['weight']['#default_value'];
if ($a == $b) {
return 0;
}
return ($a < $b) ? -1 : 1;
});
// Build Rows.
$rows = [];
$table_drag = [];
foreach ($regions as $region => $properties) {
$rows += [
// Conditionally display region title as a row.
"region-$region" => $properties['invisible'] ? NULL : [
'#attributes' => [
'class' => ['region-title', "region-title-$region"],
],
'label' => [
'#plain_text' => $properties['title'],
'#wrapper_attributes' => [
'colspan' => 4,
],
],
],
// Will dynamically display if the region has fields or not controlled
// by Drupal behaviors in js/islandora-advanced-search.admin.js.
"region-$region-message" => [
'#attributes' => [
'class' => [
'region-message',
"region-$region-message",
empty($field_rows[$region]) ? 'region-empty' : 'region-populated',
],
],
'message' => [
'#markup' => '<em>' . $properties['message'] . '</em>',
'#wrapper_attributes' => [
'colspan' => 4,
],
],
],
];
// Include field rows in this region.
$rows += $field_rows[$region];
// Configure order by weight field in region.
$table_drag[] = [
'action' => 'order',
'relationship' => 'sibling',
'group' => self::WEIGHT_FIELD_CLASS,
'subgroup' => $properties['weight'],
'source' => self::WEIGHT_FIELD_CLASS,
];
// Configure drag action for display field in region.
$table_drag[] = [
'action' => 'match',
'relationship' => 'sibling',
'group' => self::DISPLAY_FIELD_CLASS,
'subgroup' => $properties['display'],
'source' => self::DISPLAY_FIELD_CLASS,
];
}
$form[self::SETTING_FIELDS] = [
'#type' => 'table',
'#attributes' => [
// Identifier is hard-coded and used in
// js/islandora-advanced-search.admin.js.
'id' => 'advanced-search-fields',
],
'#header' => [
$this->t('Label'),
$this->t('Field'),
$this->t('Weight'),
$this->t('Display'),
],
'#empty' => $this->t('No search fields, please check search index configuration.'),
'#tabledrag' => $table_drag,
] + $rows;
// If there is contextual filters associated with the display that means
// we can filter on collection / sub-collection. Allow the user to choose
// which filters collections.
$id = NULL;
$field = NULL;
$options = [];
if (isset($this->display['display_options']['arguments'])) {
foreach ($this->display['display_options']['arguments'] as $context_filter) {
$id = $context_filter['id'];
$field = $context_filter['field'];
if (isset($fields[$field])) {
$options[$id] = $fields[$field]->getLabel() . ':' . $id;
}
}
}
if (count($options) > 1) {
$form[self::SETTING_CONTEXTUAL_FILTER] = [
'#type' => 'select',
'#title' => $this->t('Context Filter'),
'#description' => $this->t('If more than one <strong>Context Filter</strong> is defined, specify which is used to <strong>include</strong> only <strong>direct children</strong> of the Collection as it will disabled to allow recursive searching.'),
'#options' => $options,
'#default_value' => $this->configuration[self::SETTING_CONTEXTUAL_FILTER],
'#multiple' => FALSE,
'#required' => TRUE,
'#size' => count($options) + 1,
];
}
$form['#attributes']['class'][] = 'clearfix';
$form['#attached']['library'][] = 'islandora_advanced_search/advanced.search.admin';
return $form;
}
/**
* {@inheritdoc}
*/
public function blockSubmit($form, FormStateInterface $form_state) {
$values = $form_state->getValues();
$fields = array_filter($values[self::SETTING_FIELDS], function ($field) {
return $field['display'] == 'visible';
});
uasort($fields, '\Drupal\Component\Utility\SortArray::sortByWeightElement');
$this->configuration[self::SETTING_FIELDS] = array_keys($fields);
if (isset($values[self::SETTING_CONTEXTUAL_FILTER])) {
$this->configuration[self::SETTING_CONTEXTUAL_FILTER] = $values[self::SETTING_CONTEXTUAL_FILTER];
}
}
/**
* {@inheritdoc}
*/
public function build() {
$fields = $this->getIndex()->getFields();
$configured_fields = [];
foreach ($this->configuration[self::SETTING_FIELDS] as $identifier) {
$configured_fields[$identifier] = $fields[$identifier];
}
return $this->formBuilder->getForm('Drupal\islandora_advanced_search\Form\AdvancedSearchForm', $this->view, $this->display, $configured_fields, $this->configuration[self::SETTING_CONTEXTUAL_FILTER]);
}
/**
* {@inheritdoc}
*/
public function getCacheMaxAge() {
// The block cannot be cached, because it must always match the current
// search results.
return 0;
}
/**
* Get Search Index.
*/
protected function getIndex() {
$id = $this->getDerivativeId();
return $this->displayPluginManager->createInstance("views_{$this->display['display_plugin']}:{$id}")->getIndex();
}
}

17
modules/islandora_advanced_search/src/Plugin/Block/AdvancedSearchBlockDeriver.php

@ -0,0 +1,17 @@
<?php
namespace Drupal\islandora_advanced_search\Plugin\Block;
/**
* Deriver for AdvancedSearchBlock.
*/
class AdvancedSearchBlockDeriver extends SearchApiDisplayBlockDeriver {
/**
* {@inheritdoc}
*/
protected function label() {
return $this->t('Advanced Search');
}
}

97
modules/islandora_advanced_search/src/Plugin/Block/SearchApiDisplayBlockDeriver.php

@ -0,0 +1,97 @@
<?php
namespace Drupal\islandora_advanced_search\Plugin\Block;
use Drupal\Component\Plugin\PluginBase;
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* This deriver creates a block for every search_api.display.
*/
abstract class SearchApiDisplayBlockDeriver implements ContainerDeriverInterface {
use StringTranslationTrait;
/**
* List of derivative definitions.
*
* @var array
*/
protected $derivatives = [];
/**
* The entity storage for the view.
*
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
protected $storage;
/**
* The display manager for the search_api.
*
* @var \Drupal\search_api\Display\DisplayPluginManager
*/
protected $displayPluginManager;
/**
* Label for the SearchApiDisplayBlockDriver.
*/
abstract protected function label();
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, $base_plugin_id) {
$deriver = new static($container, $base_plugin_id);
$deriver->storage = $container->get('entity_type.manager')->getStorage('view');
$deriver->displayPluginManager = $container->get('plugin.manager.search_api.display');
return $deriver;
}
/**
* {@inheritdoc}
*/
public function getDerivativeDefinition($derivative_id, $base_plugin_definition) {
$derivatives = $this->getDerivativeDefinitions($base_plugin_definition);
return isset($derivatives[$derivative_id]) ? $derivatives[$derivative_id] : NULL;
}
/**
* {@inheritdoc}
*/
public function getDerivativeDefinitions($base_plugin_definition) {
$base_plugin_id = $base_plugin_definition['id'];
if (!isset($this->derivatives[$base_plugin_id])) {
$plugin_derivatives = [];
foreach ($this->displayPluginManager->getDefinitions() as $display_definition) {
$view_id = $display_definition['view_id'];
$view_display = $display_definition['view_display'];
// The derived block needs both the view / display identifiers to
// construct the pager.
$machine_name = "${view_id}__${view_display}";
/** @var \Drupal\views\ViewEntityInterface $view */
$view = $this->storage->load($view_id);
$display = $view->getDisplay($view_display);
$plugin_derivatives[$machine_name] = [
'id' => $base_plugin_id . PluginBase::DERIVATIVE_SEPARATOR . $machine_name,
'label' => $this->label(),
'admin_label' => $this->t(':view: :label for :display', [
':view' => $view->label(),
':label' => $this->label(),
':display' => $display['display_title'],
]),
] + $base_plugin_definition;
}
$this->derivatives[$base_plugin_id] = $plugin_derivatives;
}
return $this->derivatives[$base_plugin_id];
}
}

314
modules/islandora_advanced_search/src/Plugin/Block/SearchResultsPagerBlock.php

@ -0,0 +1,314 @@
<?php
namespace Drupal\islandora_advanced_search\Plugin\Block;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Render\Markup;
use Drupal\Core\Url;
use Drupal\islandora_advanced_search\AdvancedSearchQuery;
use Drupal\views\Entity\View;
use Drupal\views\Plugin\views\pager\SqlBase;
use Drupal\views\ViewExecutable;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Provides a 'AjaxViewBlock' block.
*
* @Block(
* id = "islandora_advanced_search_result_pager",
* deriver = "Drupal\islandora_advanced_search\Plugin\Block\SearchResultsPagerBlockDeriver",
* admin_label = @Translation("Search Results Pager"),
* category = @Translation("Islandora"),
* )
*/
class SearchResultsPagerBlock extends BlockBase implements ContainerFactoryPluginInterface {
use ViewAndDisplayIdentifiersTrait;
/**
* The clone of the current request object.
*
* @var \Symfony\Component\HttpFoundation\Request
*/
protected $request;
/**
* Construct a FacetBlock instance.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param string $plugin_definition
* The plugin implementation definition.
* @param \Symfony\Component\HttpFoundation\Request $request
* A request object for the current request.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, Request $request) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->request = clone $request;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('request_stack')->getMainRequest()
);
}
/**
* {@inheritdoc}
*/
public function build() {
$id = $this->getDerivativeId();
list($view_id, $display_id) = $this->getViewAndDisplayIdentifiers();
$view = View::Load($view_id);
$view_executable = $view->getExecutable();
$view_executable->setDisplay($display_id);
// Allow advanced search to alter the query.
$advanced_search_query = new AdvancedSearchQuery();
$advanced_search_query->alterView($this->request, $view_executable, $display_id);
$view_executable->execute();
$pager = $view_executable->getPager();
$exposed_input = $view_executable->getExposedInput();
$query_parameters = $this->request->query->all();
$build = [
'#attached' => [
'drupalSettings' => [
'islandora_advanced_search_pager_views_ajax' => [
$id => [
'view_id' => $view_id,
'current_display_id' => $display_id,
'ajax_path' => '/views/ajax',
],
],
],
],
'#attributes' => [
'class' => ['islandora_advanced_search_result_pager'],
'data-drupal-pager-id' => $id,
],
'result_summary' => $this->buildResultsSummary($view_executable),
'container' => [
'#prefix' => '<div class="pager__group">',
'#suffix' => '</div>',
'results_per_page_links' => $this->buildResultsPerPageLinks($pager, $query_parameters),
'display_links' => $this->buildDisplayLinks($query_parameters),
'sort_by' => $this->buildSortByForm($view_executable->sort, $query_parameters),
'pager' => array_merge($pager->render($exposed_input), ['#wrapper_attributes' => ['class' => ['container']]]),
],
];
return $build;
}
/**
* Build the results summary portion of the pager.
*
* @param Drupal\views\ViewExecutable $view_executable
* The view to build the summary for.
*
* @return array
* A renderable array that represents the current page, and number of
* results in the view.
*/
protected function buildResultsSummary(ViewExecutable $view_executable) {
$current_page = (int) $view_executable->getCurrentPage() + 1;
$per_page = (int) $view_executable->getItemsPerPage();
$total = isset($view_executable->total_rows) ? $view_executable->total_rows : count($view_executable->result);
// If there is no result the "start" and "current_record_count" should be
// equal to 0. To have the same calculation logic, we use a "start offset"
// to handle all the cases.
$start_offset = empty($total) ? 0 : 1;
if ($per_page === 0) {
$start = $start_offset;
$end = $total;
}
else {
$total_count = $current_page * $per_page;
if ($total_count > $total) {
$total_count = $total;
}
$start = ($current_page - 1) * $per_page + $start_offset;
$end = $total_count;
}
if (!empty($total)) {
// Return as render array.
return [
'#prefix' => '<div class="pager__summary">',
'#suffix' => '</div>',
'#markup' => $this->t('Displaying @start - @end of @total', [
'@start' => $start,
'@end' => $end,
'@total' => $total,
]),
];
}
return [];
}
/**
* Build the results per page portion of the pager.
*
* @param Drupal\views\Plugin\views\pager\SqlBase $pager
* The pager for the view.
* @param array $query_parameters
* The query parameters used to change the number of results per page.
*
* @return array
* A renderable array representing the results per page portion of pager.
*/
protected function buildResultsPerPageLinks(SqlBase $pager, array $query_parameters) {
$active_items_per_page = $query_parameters['items_per_page'] ?? $pager->options['items_per_page'];
$items_per_page_options = array_map(function ($value) {
return trim($value);
}, explode(',', $pager->options['expose']['items_per_page_options']));
$items = [];
foreach ($items_per_page_options as $items_per_page) {
$url = Url::fromRoute('<current>', [], [
// When changing the number of items displayed always return the user
// to the first page.
'query' => array_merge($query_parameters, [
'items_per_page' => $items_per_page,
'page' => 0,
]),
'absolute' => TRUE,
]);
$active = $items_per_page == $active_items_per_page;
$items[] = [
'#type' => 'link',
'#url' => $url,
'#title' => $items_per_page,
'#attributes' => [
'class' => $active ? ['pager__link', 'pager__link--is-active'] : ['pager__link'],
],
'#wrapper_attributes' => [
'class' => $active ? ['pager__item', 'is-active'] : ['pager__item'],
],
];
}
return [
'#theme' => 'item_list',
'#title' => $this->t('Results per page'),
'#list_type' => 'ul',
'#items' => $items,
'#attributes' => [],
'#wrapper_attributes' => ['class' => ['pager__results', 'container']],
];
}
/**
* Build the display links portion of the pager (list/grid).
*
* @param array $query_parameters
* The query parameters used to change the display format.
*
* @return array
* A renderable array representing the display links portion of pager.
*/
protected function buildDisplayLinks(array $query_parameters) {
$active_display = $query_parameters['display'] ?? 'list';
$display_options = [
'list' => [
'icon' => 'fa-list',
'title' => $this->t('List'),
],
'grid' => [
'icon' => 'fa-th',
'title' => $this->t('Grid'),
],
];
$items = [];
foreach ($display_options as $display => $options) {
$url = Url::fromRoute('<current>', [], [
'query' => array_merge($query_parameters, ['display' => $display]),
'absolute' => TRUE,
]);
$text = "<i class='fa {$options['icon']}' aria-hidden='true'>&nbsp;</i>{$options['title']}";
$active = $active_display == $display;
$items[] = [
'#type' => 'link',
'#url' => $url,
'#title' => Markup::create($text),
'#attributes' => [
'class' => $active ? ['pager__link', 'pager__link--is-active'] : ['pager__link'],
],
'#wrapper_attributes' => [
'class' => $active ? ['pager__item', 'is-active'] : ['pager__item'],
],
];
}
return [
'#theme' => 'item_list',
'#list_type' => 'ul',
'#items' => $items,
'#attributes' => [],
'#wrapper_attributes' => ['class' => ['pager__display', 'container']],
];
}
/**
* Build the sort by portion of the pager.
*
* @param array $sort_criteria
* The search fields which can be sorted.
* @param array $query_parameters
* The query parameters used to change the display format.
*
* @return array
* A renderable array representing the sort by portion of pager.
*/
protected function buildSortByForm(array $sort_criteria, array $query_parameters) {
$default_order = $query_parameters['sort_order'] ?? 'ASC';
$default_sort_by = $query_parameters['sort_by'] ?? 'search_api_relevance';
$default_value = $default_sort_by . '_' . strtolower($default_order);
$options = [];
$options_attributes = [];
// Not sure if this will work without defining a sort per direction.
foreach ($sort_criteria as $sort) {
if ($sort->options['exposed'] == TRUE) {
$id = $sort->options['id'];
// Label should be translated via views already.
$label = $sort->options['expose']['label'];
$asc = "{$id}_asc";
$desc = "{$id}_desc";
$options[$asc] = "{$label} ↑";
$options[$desc] = "{$label} ↓";
$options_attributes[$asc] = [
'data-sort_by' => $id,
'data-sort_order' => 'ASC',
];
$options_attributes[$desc] = [
'data-sort_by' => $id,
'data-sort_order' => 'DESC',
];
}
}
return [
'#type' => 'select',
'#title' => 'Sort',
'#title_display' => 'invisible',
'#options' => $options,
'#options_attributes' => $options_attributes,
'#attributes' => ['autocomplete' => 'off'],
'#wrapper_attributes' => ['class' => ['pager__sort', 'container']],
'#name' => 'order',
'#value' => $default_value,
];
}
/**
* {@inheritdoc}
*/
public function getCacheMaxAge() {
// The block cannot be cached, because it must always match the current
// search results.
return 0;
}
}

17
modules/islandora_advanced_search/src/Plugin/Block/SearchResultsPagerBlockDeriver.php

@ -0,0 +1,17 @@
<?php
namespace Drupal\islandora_advanced_search\Plugin\Block;
/**
* This deriver creates a block for every search_api.display.
*/
class SearchResultsPagerBlockDeriver extends SearchApiDisplayBlockDeriver {
/**
* {@inheritdoc}
*/
protected function label() {
return $this->t('Search Results Pager');
}
}

30
modules/islandora_advanced_search/src/Plugin/Block/ViewAndDisplayIdentifiersTrait.php

@ -0,0 +1,30 @@
<?php
namespace Drupal\islandora_advanced_search\Plugin\Block;
/**
* Gets the view and display identifiers used to create this block.
*
* @see Drupal\Component\Plugin\Discovery\DiscoveryInterface
*/
trait ViewAndDisplayIdentifiersTrait {
/**
* {@inheritdoc}
*/
abstract public function getDerivativeId();
/**
* Gets the View and View Display identifiers used to derive this block.
*
* @return string[]
* Returns an array of two strings where the first is the View identifier
* and the second is the View Display identifier associated with the view
* used to derive this block.
*/
public function getViewAndDisplayIdentifiers() {
$id = $this->getDerivativeId();
return preg_split('/__/', $id, 2);
}
}

63
modules/islandora_advanced_search/src/Plugin/Field/FieldFormatter/EntityReferenceCountFormatter.php

@ -0,0 +1,63 @@
<?php
namespace Drupal\islandora_advanced_search\Plugin\Field\FieldFormatter;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\Plugin\Field\FieldFormatter\EntityReferenceFormatterBase;
use Drupal\Core\Form\FormStateInterface;
/**
* Plugin implementation of the 'entity reference ID' formatter.
*
* @FieldFormatter(
* id = "entity_reference_url_title",
* label = @Translation("Children Entity Count, Label."),
* description = @Translation("Children Entity Count, Label."),
* field_types = {
* "entity_reference"
* }
* )
*/
class EntityReferenceCountFormatter extends EntityReferenceFormatterBase {
/**
* {@inheritdoc}
*/
public static function defaultSettings() {
return [
'label' => 'Items in Collection',
] + parent::defaultSettings();
}
/**
* {@inheritdoc}
*/
public function settingsForm(array $form, FormStateInterface $form_state) {
$elements['separator'] = [
'#title' => $this->t("Text to appear next to the children's count"),
'#type' => 'textfield',
'#default_value' => $this->getSetting('label'),
];
return $elements;
}
/**
* {@inheritdoc}
*/
public function settingsSummary(): array {
$summary = [];
$summary[] = $this->getSetting('label') ? 'Label : ' . $this->getSetting('label') : $this->t('No label');
return $summary;
}
/**
* {@inheritdoc}
*/
public function viewElements(FieldItemListInterface $items, $langcode): array {
$element = [];
$total_items = count($this->getEntitiesToView($items, $langcode));
$element[] = ['#markup' => "<span class='collection_children__total_count'>{$total_items}<span>" . ' ' . $this->formatPlural($total_items, '1 Item in Collection', '@count Items in Collection')];
return $element;
}
}

84
modules/islandora_advanced_search/src/Plugin/facets/widget/IncludeExcludeLinksWidget.php

@ -0,0 +1,84 @@
<?php
namespace Drupal\islandora_advanced_search\Plugin\facets\widget;
use Drupal\facets\Plugin\facets\widget\LinksWidget;
use Drupal\Core\Link;
use Drupal\facets\Result\ResultInterface;
/**
* The links widget.
*
* @FacetsWidget(
* id = "include_exclude_links",
* label = @Translation("List of links that allow the user to include / exclude facets."),
* description = @Translation("A simple widget that shows a list of +/- links."),
* )
*/
class IncludeExcludeLinksWidget extends LinksWidget {
/**
* {@inheritdoc}
*/
protected function prepareLink(ResultInterface $result) {
$facet = $result->getFacet();
$facet_source_id = $facet->getFacetSourceId();
$facet_manager = \Drupal::service('facets.manager');
$facets = $facet_manager->getFacetsByFacetSourceId($facet_source_id);
$raw_value = $result->getRawValue();
$count = $result->getCount();
$url = $result->getUrl();
$exclude_facet = $this->getExcludeFacet($facet, $facets);
$exclude_result = $this->getExcludeResult($exclude_facet, $raw_value);
$exclude_url = $exclude_result ? $exclude_result->getUrl() : NULL;
return [
'#theme' => 'facets_result_item',
'#is_active' => $result->isActive(),
'#value' => [
'text' => (new Link($result->getDisplayValue(), $url))->toRenderable(),
'include' => (new Link(' ', $url))->toRenderable() + [
'#attributes' => [
'class' => ['facet-item__include', 'fa', 'fa-plus'],
],
],
'exclude' => $exclude_url ? (new Link(' ', $exclude_url))->toRenderable() + [
'#attributes' => [
'class' => ['facet-item__exclude', 'fa', 'fa-minus'],
],
] : NULL,
],
'#show_count' => $this->getConfiguration()['show_numbers'] && ($count !== NULL),
'#count' => $count,
'#facet' => $facet,
'#raw_value' => $raw_value,
];
}
/**
* Looks for the excluded facet version of the included facet.
*/
protected function getExcludeResult($facet, $raw_value) {
if ($facet) {
foreach ($facet->getResults() as $result) {
if ($result->getRawValue() === $raw_value) {
return $result;
}
}
}
return NULL;
}
/**
* Looks for the excluded facet version of the included facet.
*/
protected function getExcludeFacet($include, $facets) {
$field_identifier = $include->getFieldIdentifier();
foreach ($facets as $facet) {
if ($field_identifier === $facet->getFieldIdentifier() && $facet->getExclude()) {
return $facet;
}
}
return NULL;
}
}

42
modules/islandora_advanced_search/src/Plugin/facets_summary/processor/ResetRemovePage.php

@ -0,0 +1,42 @@
<?php
namespace Drupal\islandora_advanced_search\Plugin\facets_summary\processor;
use Drupal\facets_summary\FacetsSummaryInterface;
use Drupal\facets_summary\Processor\BuildProcessorInterface;
/**
* Reset should also remove the page query attribute.
*
* @SummaryProcessor(
* id = "reset_remove_page",
* label = @Translation("Remove page from query when resetting facets/query."),
* description = @Translation("Remove page from query when resetting facets/query."),
* stages = {
* "build" = 45
* }
* )
*/
class ResetRemovePage extends ShowSearchQueryProcessor implements BuildProcessorInterface {
/**
* {@inheritdoc}
*/
public function build(FacetsSummaryInterface $facets_summary, array $build, array $facets) {
// This processor is weighted to occur after the reset facets link.
// Which leaves two cases:
// - No facets selected so no reset link (we must add one).
// - Reset link exists at the top of the list (we must remove the
// search term from the link as well).
$reset_index = $this->getResetLinkIndex($build);
if ($reset_index !== NULL) {
$reset = &$build['#items'][$reset_index];
// Remove query from reset url as well.
$query_params = $reset['#url']->getOption('query');
unset($query_params['page']);
$reset['#url']->setOption('query', $query_params);
}
return $build;
}
}

47
modules/islandora_advanced_search/src/Plugin/facets_summary/processor/ShowActiveExcludedFacets.php

@ -0,0 +1,47 @@
<?php
namespace Drupal\islandora_advanced_search\Plugin\facets_summary\processor;
use Drupal\facets_summary\FacetsSummaryInterface;
use Drupal\facets_summary\Processor\BuildProcessorInterface;
use Drupal\facets_summary\Processor\ProcessorPluginBase;
use Drupal\facets\FacetInterface;
/**
* Provides a processor that shows the search query.
*
* @SummaryProcessor(
* id = "show_active_excluded_facets",
* label = @Translation("Show active excluded facets."),
* description = @Translation("When checked, negated facets will appear in the summary."),
* stages = {
* "build" = 20
* }
* )
*/
class ShowActiveExcludedFacets extends ProcessorPluginBase implements BuildProcessorInterface {
use ShowFacetsTrait;
/**
* {@inheritdoc}
*/
protected function condition(FacetInterface $facet) {
return $facet->getExclude();
}
/**
* {@inheritdoc}
*/
protected function classes() {
return ['facet-summary-item--facet', 'facet-summary-item--exclude'];
}
/**
* {@inheritdoc}
*/
public function build(FacetsSummaryInterface $facets_summary, array $build, array $facets) {
return $this->buildHelper($build, $facets);
}
}

89
modules/islandora_advanced_search/src/Plugin/facets_summary/processor/ShowActiveFacets.php

@ -0,0 +1,89 @@
<?php
namespace Drupal\islandora_advanced_search\Plugin\facets_summary\processor;
use Drupal\Core\Link;
use Drupal\facets_summary\FacetsSummaryInterface;
use Drupal\facets_summary\Processor\BuildProcessorInterface;
use Drupal\facets_summary\Processor\ProcessorPluginBase;
use Drupal\facets\FacetInterface;
use Drupal\facets\Result\ResultInterface;
/**
* Provides a processor that shows the search query.
*
* @SummaryProcessor(
* id = "show_active_facets",
* label = @Translation("Shows active hidden facets."),
* description = @Translation("When checked, undoes 'hide_active_items_processor', etc."),
* stages = {
* "build" = 20
* }
* )
*/
class ShowActiveFacets extends ProcessorPluginBase implements BuildProcessorInterface {
/**
* {@inheritdoc}
*/
public function build(FacetsSummaryInterface $facets_summary, array $build, array $facets) {
// Rebuild list of results, add back ones that have been removed.
$facet_manager = \Drupal::service('facets.manager');
$facet_source_id = $facets_summary->getFacetSourceId();
$facet_manager->updateResults($facet_source_id);
$facet_manager->processFacets($facet_source_id);
$facets_config = $facets_summary->getFacets();
foreach ($facets as $facet) {
$processors = $facet->getProcessors();
/** @var \Drupal\facets\Processor\BuildProcessorInterface $url_handler */
$url_handler = $processors['url_processor_handler'];
$results = $url_handler->build($facet, $facet->getResults());
foreach ($results as $result) {
if ($result->isActive() && $this->resultMissing($facet, $result, $build['#items'])) {
$item = [
'#theme' => 'facets_result_item__summary',
'#value' => $result->getDisplayValue(),
'#show_count' => $facets_config[$facet->id()]['show_count'],
'#count' => $result->getCount(),
'#is_active' => TRUE,
'#facet' => $result->getFacet(),
'#raw_value' => $result->getRawValue(),
];
$item = (new Link($item, $result->getUrl()))->toRenderable();
$item['#wrapper_attributes'] = [
'class' => [
'facet-summary-item--facet',
],
];
$build['#items'][] = $item;
}
}
}
return $build;
}
/**
* Checks if the results are missing for the given facet.
*
* @param \Drupal\facets\FacetInterface $facet
* The facet to check.
* @param \Drupal\facets\Result\ResultInterface $result
* The result of the facet to check.
* @param array $items
* The already completed render array of facets to check against.
*
* @return bool
* TRUE if the result is missing FALSE otherwise.
*/
protected function resultMissing(FacetInterface $facet, ResultInterface $result, array $items) {
foreach ($items as $item) {
$item_facet = $item['#title']['#facet'];
$raw_value = $item['#title']['#raw_value'];
if ($item_facet === $facet && $raw_value === $result->getRawValue()) {
return FALSE;
}
}
return TRUE;
}
}

67
modules/islandora_advanced_search/src/Plugin/facets_summary/processor/ShowFacetsTrait.php

@ -0,0 +1,67 @@
<?php
namespace Drupal\islandora_advanced_search\Plugin\facets_summary\processor;
use Drupal\Core\Link;
use Drupal\Core\Url;
use Drupal\facets\FacetInterface;
/**
* Common logic to toggle the display of facets given a condition.
*/
trait ShowFacetsTrait {
/**
* Checks if the facet should be shown or not.
*/
abstract protected function condition(FacetInterface $facet);
/**
* Classes to include on the shown facet.
*/
abstract protected function classes();
/**
* {@inheritdoc}
*/
protected function buildHelper(array $build, array $facets) {
$request = \Drupal::request();
$query_params = $request->query->all();
foreach ($facets as $facet) {
if ($this->condition($facet)) {
$url_alias = $facet->getUrlAlias();
$filter_key = $facet->getFacetSourceConfig()->getFilterKey() ?: 'f';
$active_items = $facet->getActiveItems();
foreach ($active_items as $active_item) {
$url = Url::createFromRequest($request);
$modified_query_params = $query_params;
$modified_query_params[$filter_key] = array_filter($query_params[$filter_key], function ($query_param) use ($url_alias, $active_item) {
$pos = strpos($query_param, ':');
$alias = substr($query_param, 0, $pos);
$value = substr($query_param, $pos + 1);
return !($alias == $url_alias && $value == $active_item);
});
$url->setOption('query', $modified_query_params);
$item = [
'#theme' => 'facets_result_item__summary',
'#value' => $active_item,
// We do not have counts for excluded/missing facets...
'#show_count' => FALSE,
// Do not know the count.
'#count' => 0,
'#is_active' => TRUE,
'#facet' => $facet,
'#raw_value' => $active_item,
];
$item = (new Link($item, $url))->toRenderable();
$item['#wrapper_attributes'] = [
'class' => $this->classes(),
];
$build['#items'][] = $item;
}
}
}
return $build;
}
}

47
modules/islandora_advanced_search/src/Plugin/facets_summary/processor/ShowMissingFacets.php

@ -0,0 +1,47 @@
<?php
namespace Drupal\islandora_advanced_search\Plugin\facets_summary\processor;
use Drupal\facets_summary\FacetsSummaryInterface;
use Drupal\facets_summary\Processor\BuildProcessorInterface;
use Drupal\facets_summary\Processor\ProcessorPluginBase;
use Drupal\facets\FacetInterface;
/**
* Provides a processor that shows the search query.
*
* @SummaryProcessor(
* id = "show_missing_facets",
* label = @Translation("Shows facets from the url that are missing from the results."),
* description = @Translation("When checked, show facets not included in the solr result but specified in the URL."),
* stages = {
* "build" = 20
* }
* )
*/
class ShowMissingFacets extends ProcessorPluginBase implements BuildProcessorInterface {
use ShowFacetsTrait;
/**
* {@inheritdoc}
*/
protected function condition(FacetInterface $facet) {
return !$facet->getExclude() && empty($facet->getResults());
}
/**
* {@inheritdoc}
*/
protected function classes() {
return ['facet-summary-item--facet'];
}
/**
* {@inheritdoc}
*/
public function build(FacetsSummaryInterface $facets_summary, array $build, array $facets) {
return $this->buildHelper($build, $facets);
}
}

101
modules/islandora_advanced_search/src/Plugin/facets_summary/processor/ShowSearchQueryProcessor.php

@ -0,0 +1,101 @@
<?php
namespace Drupal\islandora_advanced_search\Plugin\facets_summary\processor;
use Drupal\Core\Link;
use Drupal\Core\Url;
use Drupal\facets_summary\FacetsSummaryInterface;
use Drupal\facets_summary\Processor\BuildProcessorInterface;
use Drupal\facets_summary\Processor\ProcessorPluginBase;
/**
* Provides a processor that shows the search query.
*
* @SummaryProcessor(
* id = "show_search_query",
* label = @Translation("Show the current search query"),
* description = @Translation("When checked, this facet will show the search query."),
* stages = {
* "build" = 40
* }
* )
*/
class ShowSearchQueryProcessor extends ProcessorPluginBase implements BuildProcessorInterface {
/**
* {@inheritdoc}
*/
public function build(FacetsSummaryInterface $facets_summary, array $build, array $facets) {
$request = \Drupal::request();
$query_params = $request->query->all();
if (!empty($query_params['search_api_fulltext'])) {
$text = $query_params['search_api_fulltext'];
unset($query_params['search_api_fulltext']);
$url = Url::createFromRequest($request);
$url->setOption('query', $query_params);
$item = [
'#theme' => 'facets_result_item__summary',
'#is_active' => FALSE,
'#value' => $text,
'#show_count' => FALSE,
];
$item = Link::fromTextAndUrl($item, $url)->toRenderable();
$item['#wrapper_attributes'] = [
'class' => [
'facet-summary-item--query',
],
];
// This processor is weighted to occur after the reset facets link.
// Which leaves two cases:
// - No facets selected so no reset link (we must add one).
// - Reset link exists at the top of the list (we must remove the search
// term from the link as well).
$reset_index = $this->getResetLinkIndex($build);
if ($reset_index !== NULL) {
$reset = $build['#items'][$reset_index];
// Remove query from reset url as well.
$query_params = $reset['#url']->getOption('query');
unset($query_params['search_api_fulltext']);
$reset['#url']->setOption('query', $query_params);
array_splice($build['#items'], $reset_index + 1, 0, [$item]);
}
else {
array_unshift($build['#items'], $item);
$text = $this->t('Reset');
if (isset($facets_summary->getProcessorConfigs()['reset_facets']['settings']['link_text'])) {
$text = $facets_summary->getProcessorConfigs()['reset_facets']['settings']['link_text'];
}
$reset = Link::fromTextAndUrl($text, $url)->toRenderable();
$reset['#wrapper_attributes'] = [
'class' => [
'facet-summary-item--clear',
],
];
array_unshift($build['#items'], $reset);
}
return $build;
}
return $build;
}
/**
* Gets the index in the $build render array of the reset link.
*
* @param array $build
* The render array of the FacetSummary block.
*
* @return mixed|null
* The index of the reset link the $build render array.
*/
protected function getResetLinkIndex(array $build) {
if (isset($build['#items'])) {
foreach ($build['#items'] as $index => $item) {
if (isset($item['#wrapper_attributes']['class']) && in_array('facet-summary-item--clear', $item['#wrapper_attributes']['class'])) {
return $index;
}
}
}
return NULL;
}
}

63
modules/islandora_advanced_search/src/Utilities.php

@ -0,0 +1,63 @@
<?php
namespace Drupal\islandora_advanced_search;
use Drupal\islandora_advanced_search\Plugin\Block\AdvancedSearchBlock;
use Drupal\islandora_advanced_search\Plugin\Block\SearchResultsPagerBlock;
/**
* Helper functions.
*/
class Utilities {
/**
* Gets the list of views for which pager blocks have been created.
*
* @return array
* List of view and display ids which have that have been used to
* derive a SearchResultsPagerBlock.
*/
public static function getPagerViewDisplays() {
$views = &drupal_static(__FUNCTION__);
if (!isset($views)) {
$block_storage = \Drupal::entityTypeManager()->getStorage('block');
$active_theme = \Drupal::theme()->getActiveTheme();
$views = [];
/** @var \Drupal\block\Entity\Block $block */
foreach ($block_storage->loadByProperties(['theme' => $active_theme->getName()]) as $block) {
$plugin = $block->getPlugin();
if ($plugin instanceof SearchResultsPagerBlock) {
list($view_id, $display_id) = $plugin->getViewAndDisplayIdentifiers();
$views[$block->id()] = [$view_id, $display_id];
}
}
}
return $views;
}
/**
* Gets the list of views for which advanced search blocks have been created.
*
* @return array
* List of view and display ids which have that have been used to
* derive a SearchResultsPagerBlock.
*/
public static function getAdvancedSearchViewDisplays() {
$views = &drupal_static(__FUNCTION__);
if (!isset($views)) {
$block_storage = \Drupal::entityTypeManager()->getStorage('block');
$active_theme = \Drupal::theme()->getActiveTheme();
$views = [];
/** @var \Drupal\block\Entity\Block $block */
foreach ($block_storage->loadByProperties(['theme' => $active_theme->getName()]) as $block) {
$plugin = $block->getPlugin();
if ($plugin instanceof AdvancedSearchBlock) {
list($view_id, $display_id) = $plugin->getViewAndDisplayIdentifiers();
$views[$block->id()] = [$view_id, $display_id];
}
}
}
return $views;
}
}

58
modules/islandora_advanced_search/templates/facets/facets-item-list--include-exclude-links.html.twig

@ -0,0 +1,58 @@
{#
/**
* @file
* Default theme implementation for a facets item list.
*
* Available variables:
* - items: A list of items. Each item contains:
* - attributes: HTML attributes to be applied to each list item.
* - value: The content of the list element.
* - title: The title of the list.
* - list_type: The tag for list element ("ul" or "ol").
* - wrapper_attributes: HTML attributes to be applied to the list wrapper.
* - attributes: HTML attributes to be applied to the list.
* - empty: A message to display when there are no items. Allowed value is a
* string or render array.
* - context: A list of contextual data associated with the list. May contain:
* - list_style: The ID of the widget plugin this facet uses.
* - facet: The facet for this result item.
* - id: the machine name for the facet.
* - label: The facet label.
*
* @see facets_preprocess_facets_item_list()
*
* @ingroup themeable
*/
#}
<div class="facets-widget- {{- facet.widget.type -}} ">
{% if facet.widget.type %}
{%- set attributes = attributes.addClass('item-list__' ~ facet.widget.type) %}
{% endif %}
{% if items or empty %}
{%- if title is not empty -%}
<h3>{{ title }}</h3>
{%- endif -%}
{%- if items -%}
<{{ list_type }}{{ attributes }}>
{%- for item in less -%}
<li{{ item.attributes }}>{{ item.value }}</li>
{%- endfor -%}
</{{ list_type }}>
{%- if more -%}
<{{ list_type }}{{ attributes }} style="display:none;margin-top:-1em">
{%- for item in more -%}
<li{{ item.attributes }}>{{ item.value }}</li>
{%- endfor -%}
</{{ list_type }}>
<a href="#" class="facets-soft-limit-link">{{ show_more_label }}</a>
{%- endif -%}
{%- else -%}
{{- empty -}}
{%- endif -%}
{%- endif %}
{% if facet.widget.type == "dropdown" %}
<label id="facet_{{ facet.id }}_label">{{ 'Facet'|t }} {{ facet.label }}</label>
{%- endif %}
</div>

33
modules/islandora_advanced_search/templates/facets/facets-result-item--include-exclude-links.html.twig

@ -0,0 +1,33 @@
{#
/**
* @file
* Default theme implementation of a facet result item.
*
* Available variables:
* - value: The item value.
* - raw_value: The raw item value.
* - show_count: If this facet provides count.
* - count: The amount of results.
* - is_active: The item is active.
* - facet: The facet for this result item.
* - id: the machine name for the facet.
* - label: The facet label.
*
* @ingroup themeable
*/
#}
{% if value['text'] is defined %}
<span class="facet-item__value">{{ value['text'] }}
{% if show_count %}
<span class="facet-item__count">({{ count }})</span>
{% endif %}
{% if value['exclude'] is defined %}
<span class="facet-item__operators">{{ value['include'] }} {{ value['exclude'] }}</span>
{% endif %}
</span>
{% else %}
<span class="facet-item__value">{{ value }}</span>
{% if show_count %}
<span class="facet-item__count">({{ count }})</span>
{% endif %}
{% endif %}

20
modules/islandora_advanced_search/templates/facets/facets-result-item--summary.html.twig

@ -0,0 +1,20 @@
{#
/**
* @file
* Default theme implementation of a facet result item.
*
* Available variables:
* - value: The item value.
* - raw_value: The raw item value.
* - show_count: If this facet provides count.
* - count: The amount of results.
* - is_active: The item is active.
* - facet: The facet for this result item.
* - id: the machine name for the facet.
* - label: The facet label.
*
* @ingroup themeable
*/
#}
<span class="facet-item__value">{{ value }}</span>
<span class="fa fa-remove"></span>

6
modules/islandora_audio/CONTRIBUTING.md

@ -16,7 +16,7 @@ If you would like to submit a use case to the Islandora 8 project, please submit
### Documentation
You can contribute documentation in two different ways. One way is to create an issue [here](https://github.com/Islandora/documentation/issues/new), prepending "Documentation:" to the title of the issue. Another way is by pull request, which is the same process as [Contribute Code](https://github.com/Islandora/documentation/blob/master/CONTRIBUTING.md#contribute-code). All documentation resides in [`docs`](https://github.com/Islandora/documentation/tree/master/docs).
You can contribute documentation in two different ways. One way is to create an issue [here](https://github.com/Islandora/documentation/issues/new), prepending "Documentation:" to the title of the issue. Another way is by pull request, which is the same process as [Contribute Code](https://github.com/Islandora/documentation/blob/main/CONTRIBUTING.md#contribute-code). All documentation resides in [`docs`](https://github.com/Islandora/documentation/tree/main/docs).
### Request a new feature
@ -62,9 +62,9 @@ Contributions to the Islandora codebase should be sent as GitHub pull requests.
Take a look at [Creating a pull request](https://help.github.com/articles/creating-a-pull-request). In a nutshell you need to:
1. [Fork](https://help.github.com/articles/fork-a-repo) this repository to your personal or institutional GitHub account (depending on the CLA you are working under). Be cautious of which branches you work from though (you'll want to base your work off master, or for Drupal modules use the most recent version branch). See [Fork a repo](https://help.github.com/articles/fork-a-repo) for detailed instructions.
1. [Fork](https://help.github.com/articles/fork-a-repo) this repository to your personal or institutional GitHub account (depending on the CLA you are working under). Be cautious of which branches you work from though (you'll want to base your work off the default branch). See [Fork a repo](https://help.github.com/articles/fork-a-repo) for detailed instructions.
2. Commit any changes to your fork.
3. Send a [pull request](https://help.github.com/articles/creating-a-pull-request) using the [pull request template](https://github.com/Islandora/documentation/blob/master/.github/PULL_REQUEST_TEMPLATE.md) to the Islandora GitHub repository that you forked in step 1. If your pull request is related to an existing issue -- for instance, because you reported a [bug/issue](https://github.com/Islandora/documentation/issues) earlier -- prefix the title of your pull request with the corresponding issue number (e.g. `issue-123: ...`). Please also include a reference to the issue in the description of the pull. This can be done by using '#' plus the issue number like so '#123', also try to pick an appropriate name for the branch in which you're issuing the pull request from.
3. Send a [pull request](https://help.github.com/articles/creating-a-pull-request) using the [pull request template](https://github.com/Islandora/documentation/blob/main/.github/PULL_REQUEST_TEMPLATE.md) to the Islandora GitHub repository that you forked in step 1. If your pull request is related to an existing issue -- for instance, because you reported a [bug/issue](https://github.com/Islandora/documentation/issues) earlier -- prefix the title of your pull request with the corresponding issue number (e.g. `issue-123: ...`). Please also include a reference to the issue in the description of the pull. This can be done by using '#' plus the issue number like so '#123', also try to pick an appropriate name for the branch in which you're issuing the pull request from.
You may want to read [Syncing a fork](https://help.github.com/articles/syncing-a-fork) for instructions on how to keep your fork up to date with the latest changes of the upstream (official) repository.

4
modules/islandora_audio/README.md

@ -6,14 +6,14 @@
## Introduction
Provides actions to convert audio with a [Homarus](https://github.com/Islandora/Crayfish/tree/dev/Homarus) (`ffmpeg`) server.
Provides actions to convert audio with a [Homarus](https://github.com/Islandora/Crayfish/tree/2.x/Homarus) (`ffmpeg`) server.
## Requirements
- `islandora` and `islandora_core_feature`
- A Homarus microservice
- A message broker (e.g. Activemq) for Islandora 8
- An instance of `islandora-connector-derivative` (from [Alpaca](https://github.com/Islandora/Alpaca/tree/dev/islandora-connector-derivative)) configured for Homarus
- An instance of `islandora-connector-derivative` (from [Alpaca](https://github.com/Islandora/Alpaca/tree/1.x/islandora-connector-derivative)) configured for Homarus
## Installation

3
modules/islandora_audio/config/schema/islandora_audio.schema.yml

@ -29,3 +29,6 @@ action.configuration.generate_audio_derivative:
path:
type: text
label: 'File path with extension'
field.formatter.settings.islandora_file_audio:
type: field.formatter.settings.file_audio

4
modules/islandora_audio/islandora_audio.info.yml

@ -2,6 +2,6 @@ name: 'Islandora Audio'
description: 'Islandora audio derivative actions'
type: module
package: Islandora
core: 8.x
core_version_requirement: ^9 || ^10
dependencies:
- islandora
- drupal:islandora

7
modules/islandora_audio/islandora_audio.libraries.yml

@ -0,0 +1,7 @@
audio:
version: 1.x
js:
js/audio.js: {preprocess: false}
dependencies:
- core/drupal
- core/drupalSettings

15
modules/islandora_audio/islandora_audio.module

@ -30,3 +30,18 @@ function islandora_audio_help($route_name, RouteMatchInterface $route_match) {
default:
}
}
/**
* Implements hook_theme().
*/
function islandora_audio_theme() {
return [
'islandora_file_audio' => [
'variables' => [
'files' => [],
'tracks' => NULL,
'attributes' => NULL,
],
],
];
}

47
modules/islandora_audio/js/audio.js

@ -0,0 +1,47 @@
/*jslint browser: true*/
/*global Audio, Drupal*/
/**
* @file
* Displays Audio viewer.
*/
(function ($, Drupal) {
'use strict';
/**
* If initialized.
* @type {boolean}
*/
var initialized;
/**
* Unique HTML id.
* @type {string}
*/
var base;
function init(context,settings){
if (!initialized){
initialized = true;
if ($('audio')[0].textTracks.length > 0) {
$('audio')[0].textTracks[0].oncuechange = function() {
if (this.activeCues.length > 0) {
var currentCue = this.activeCues[0].text;
$('#audioTrack').html(currentCue);
}
}
}
}
}
Drupal.Audio = Drupal.Audio || {};
/**
* Initialize the Audio Viewer.
*/
Drupal.behaviors.Audio = {
attach: function (context, settings) {
init(context,settings);
},
detach: function () {
}
};
})(jQuery, Drupal);

6
modules/islandora_audio/src/Plugin/Action/GenerateAudioDerivative.php

@ -34,8 +34,8 @@ class GenerateAudioDerivative extends AbstractGenerateDerivative {
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$form = parent::buildConfigurationForm($form, $form_state);
$form['mimetype']['#description'] = t('Mimetype to convert to (e.g. audio/mpeg, audio/m4a, etc...)');
$form['args']['#description'] = t('Additional command line parameters for FFMpeg');
$form['mimetype']['#description'] = $this->t('Mimetype to convert to (e.g. audio/mpeg, audio/m4a, etc...)');
$form['args']['#description'] = $this->t('Additional command line parameters for FFMpeg');
return $form;
}
@ -48,7 +48,7 @@ class GenerateAudioDerivative extends AbstractGenerateDerivative {
if ($exploded_mime[0] != 'audio') {
$form_state->setErrorByName(
'mimetype',
t('Please enter a audio mimetype (e.g. audio/mpeg, audio/m4a, etc...)')
$this->t('Please enter a audio mimetype (e.g. audio/mpeg, audio/m4a, etc...)')
);
}
}

28
modules/islandora_audio/src/Plugin/Field/FieldFormatter/IslandoraFileAudioFormatter.php

@ -0,0 +1,28 @@
<?php
namespace Drupal\islandora_audio\Plugin\Field\FieldFormatter;
use Drupal\islandora\Plugin\Field\FieldFormatter\IslandoraFileMediaFormatterBase;
/**
* Plugin implementation of the 'file_audio' formatter.
*
* @FieldFormatter(
* id = "islandora_file_audio",
* label = @Translation("Audio with Captions"),
* description = @Translation("Display the file using an HTML5 audio tag."),
* field_types = {
* "file"
* }
* )
*/
class IslandoraFileAudioFormatter extends IslandoraFileMediaFormatterBase {
/**
* {@inheritdoc}
*/
public static function getMediaType() {
return 'audio';
}
}

31
modules/islandora_audio/templates/islandora-file-audio.html.twig

@ -0,0 +1,31 @@
{#
/**
* @file
* Default theme implementation to display the file entity as an audio tag.
*
* Available variables:
* - attributes: An array of HTML attributes, intended to be added to the
* audio tag.
* - files: And array of files to be added as sources for the audio tag. Each
* element is an array with the following elements:
* - file: The full file object.
* - source_attributes: An array of HTML attributes for to be added to the
* source tag.
*
* @ingroup themeable
*/
#}
<div id="audioTrack"></div>
<audio {{ attributes }}>
{% for file in files %}
<source {{ file.source_attributes }} />
{% if tracks %}
{% for track in tracks %}
<track {{ track.track_attributes }} />
{% endfor %}
{% endif %}
{% endfor %}
</audio>
{{ attach_library('islandora_audio/audio') }}

9
modules/islandora_audio/tests/src/Functional/GenerateAudioDerivativeTest.php

@ -40,7 +40,7 @@ class GenerateAudioDerivativeTest extends GenerateDerivativeTestBase {
// Create an action to generate a audio derivative.
$this->drupalGet('admin/config/system/actions');
$this->getSession()->getPage()->findById("edit-action")->selectOption("Generate a audio derivative");
$this->getSession()->getPage()->pressButton(t('Create'));
$this->getSession()->getPage()->pressButton('Create');
$this->assertSession()->statusCodeEquals(200);
$this->getSession()->getPage()->fillField('edit-label', "Generate audio test derivative");
@ -53,7 +53,7 @@ class GenerateAudioDerivativeTest extends GenerateDerivativeTestBase {
$this->getSession()->getPage()->fillField('edit-args', "-f mp3");
$this->getSession()->getPage()->fillField('edit-scheme', "public");
$this->getSession()->getPage()->fillField('edit-path', "derp.mov");
$this->getSession()->getPage()->pressButton(t('Save'));
$this->getSession()->getPage()->pressButton('Save');
$this->assertSession()->statusCodeEquals(200);
// Create a context and add the action as a derivative reaction.
@ -66,9 +66,10 @@ class GenerateAudioDerivativeTest extends GenerateDerivativeTestBase {
'name[0][value]' => 'Test Media',
'files[field_media_file_0]' => __DIR__ . '/../../fixtures/test_file.txt',
'field_media_of[0][target_id]' => 'Test Node',
'field_tags[0][target_id]' => 'Preservation Master',
'field_media_use[0][target_id]' => $this->preservationMasterTerm->label(),
];
$this->drupalPostForm('media/add/' . $this->testMediaType->id(), $values, t('Save'));
$this->drupalGet('media/add/' . $this->testMediaType->id());
$this->submitForm($values, 'Save');
$expected = [
'source_uri' => 'test_file.txt',

6
modules/islandora_breadcrumbs/CONTRIBUTING.md

@ -16,7 +16,7 @@ If you would like to submit a use case to the Islandora 8 project, please submit
### Documentation
You can contribute documentation in two different ways. One way is to create an issue [here](https://github.com/Islandora/documentation/issues/new), prepending "Documentation:" to the title of the issue. Another way is by pull request, which is the same process as [Contribute Code](https://github.com/Islandora/documentation/blob/master/CONTRIBUTING.md#contribute-code). All documentation resides in [`docs`](https://github.com/Islandora/documentation/tree/master/docs).
You can contribute documentation in two different ways. One way is to create an issue [here](https://github.com/Islandora/documentation/issues/new), prepending "Documentation:" to the title of the issue. Another way is by pull request, which is the same process as [Contribute Code](https://github.com/Islandora/documentation/blob/main/CONTRIBUTING.md#contribute-code). All documentation resides in [`docs`](https://github.com/Islandora/documentation/tree/main/docs).
### Request a new feature
@ -62,9 +62,9 @@ Contributions to the Islandora codebase should be sent as GitHub pull requests.
Take a look at [Creating a pull request](https://help.github.com/articles/creating-a-pull-request). In a nutshell you need to:
1. [Fork](https://help.github.com/articles/fork-a-repo) this repository to your personal or institutional GitHub account (depending on the CLA you are working under). Be cautious of which branches you work from though (you'll want to base your work off master, or for Drupal modules use the most recent version branch). See [Fork a repo](https://help.github.com/articles/fork-a-repo) for detailed instructions.
1. [Fork](https://help.github.com/articles/fork-a-repo) this repository to your personal or institutional GitHub account (depending on the CLA you are working under). Be cautious of which branches you work from though (you'll want to base your work off the default branch). See [Fork a repo](https://help.github.com/articles/fork-a-repo) for detailed instructions.
2. Commit any changes to your fork.
3. Send a [pull request](https://help.github.com/articles/creating-a-pull-request) using the [pull request template](https://github.com/Islandora/documentation/blob/master/.github/PULL_REQUEST_TEMPLATE.md) to the Islandora GitHub repository that you forked in step 1. If your pull request is related to an existing issue -- for instance, because you reported a [bug/issue](https://github.com/Islandora/documentation/issues) earlier -- prefix the title of your pull request with the corresponding issue number (e.g. `issue-123: ...`). Please also include a reference to the issue in the description of the pull. This can be done by using '#' plus the issue number like so '#123', also try to pick an appropriate name for the branch in which you're issuing the pull request from.
3. Send a [pull request](https://help.github.com/articles/creating-a-pull-request) using the [pull request template](https://github.com/Islandora/documentation/blob/main/.github/PULL_REQUEST_TEMPLATE.md) to the Islandora GitHub repository that you forked in step 1. If your pull request is related to an existing issue -- for instance, because you reported a [bug/issue](https://github.com/Islandora/documentation/issues) earlier -- prefix the title of your pull request with the corresponding issue number (e.g. `issue-123: ...`). Please also include a reference to the issue in the description of the pull. This can be done by using '#' plus the issue number like so '#123', also try to pick an appropriate name for the branch in which you're issuing the pull request from.
You may want to read [Syncing a fork](https://help.github.com/articles/syncing-a-fork) for instructions on how to keep your fork up to date with the latest changes of the upstream (official) repository.

3
modules/islandora_breadcrumbs/config/install/islandora.breadcrumbs.yml

@ -1,3 +0,0 @@
maxDepth: -1
includeSelf: FALSE
referenceField: field_member_of

4
modules/islandora_breadcrumbs/config/install/islandora_breadcrumbs.breadcrumbs.yml

@ -0,0 +1,4 @@
maxDepth: -1
includeSelf: FALSE
referenceFields:
- field_member_of

11
modules/islandora_breadcrumbs/config/schema/islandora_breadcrumbs.schema.yml

@ -1,4 +1,4 @@
islandora.breadcrumbs:
islandora_breadcrumbs.breadcrumbs:
type: config_object
mapping:
maxDepth:
@ -7,6 +7,9 @@ islandora.breadcrumbs:
includeSelf:
type: boolean
label: 'Include Self'
referenceField:
type: string
label: 'Reference Field'
referenceFields:
type: sequence
label: 'Reference Fields'
sequence:
type: string
label: 'Reference Field'

4
modules/islandora_breadcrumbs/islandora_breadcrumbs.info.yml

@ -1,7 +1,7 @@
name: 'Islandora Breadcrumbs'
type: module
description: 'Builds breadcrumbs based on field_member_of relationships.'
core: 8.x
core_version_requirement: ^9 || ^10
package: Islandora
dependencies:
- islandora
- islandora:islandora

18
modules/islandora_breadcrumbs/islandora_breadcrumbs.install

@ -0,0 +1,18 @@
<?php
/**
* @file
* Install/update hook implementations.
*/
/**
* Update referenceField config to referenceFields.
*/
function islandora_breadcrumbs_update_8001() {
$config_factory = \Drupal::configFactory();
$config = $config_factory->getEditable('islandora_breadcrumbs.breadcrumbs');
$config->set('referenceFields', [$config->get('referenceField')]);
$config->clear('referenceField');
$config->save();
return "Updated referenceFields config.";
}

5
modules/islandora_breadcrumbs/islandora_breadcrumbs.links.menu.yml

@ -0,0 +1,5 @@
system.islandora_breadcrumbs_settings:
title: 'Breadcrumbs Settings'
parent: system.admin_config_islandora
route_name: system.islandora_breadcrumbs_settings
description: 'Configure Islandora breadcrumb settings'

7
modules/islandora_breadcrumbs/islandora_breadcrumbs.routing.yml

@ -0,0 +1,7 @@
system.islandora_breadcrumbs_settings:
path: '/admin/config/islandora/breadcrumbs'
defaults:
_form: 'Drupal\islandora_breadcrumbs\Form\IslandoraBreadcrumbsSettingsForm'
_title: 'Islandora Breadcrumbs Settings'
requirements:
_permission: 'administer site configuration'

2
modules/islandora_breadcrumbs/islandora_breadcrumbs.services.yml

@ -1,6 +1,6 @@
services:
islandora_breadcrumbs.breadcrumb:
class: Drupal\islandora_breadcrumbs\IslandoraBreadcrumbBuilder
arguments: ['@entity_type.manager', '@config.factory']
arguments: ['@entity_type.manager', '@config.factory', '@islandora.utils']
tags:
- { name: breadcrumb_builder, priority: 100 }

132
modules/islandora_breadcrumbs/src/Form/IslandoraBreadcrumbsSettingsForm.php

@ -0,0 +1,132 @@
<?php
namespace Drupal\islandora_breadcrumbs\Form;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
/**
* Configure islandora_breadcrumbs settings.
*/
class IslandoraBreadcrumbsSettingsForm extends ConfigFormBase {
/**
* Config settings.
*
* @var string
*/
const SETTINGS = 'islandora_breadcrumbs.breadcrumbs';
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'islandora_breadcrumbs_settings';
}
/**
* {@inheritdoc}
*/
protected function getEditableConfigNames() {
return [
static::SETTINGS,
];
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$config = $this->config(static::SETTINGS);
$form['maxDepth'] = [
'#type' => 'number',
'#default_value' => $config->get('maxDepth'),
'#min' => -1,
'#step' => 1,
'#title' => $this->t('Maximum number of ancestor breadcrumbs'),
'#description' => $this->t("Stops adding ancestor references when the chain reaches this number. The count does not include the current node when enabled. The default value, '-1' disables this feature."),
];
$form['includeSelf'] = [
'#type' => 'checkbox',
'#title' => $this->t('Include the current node in the breadcrumbs?'),
'#default_value' => $config->get('includeSelf'),
];
// Using the textarea instead of a select so the site maintainer can
// provide an ordered list of items rather than simply selecting from a
// list which enforces it's own order.
$form['referenceFields'] = [
'#type' => 'textarea',
'#title' => $this->t('Entity Reference fields to follow'),
'#default_value' => implode("\n", $config->get('referenceFields')),
'#description' => $this->t("Entity Reference field machine names to follow when building the breadcrumbs.<br>One per line.<br>Valid options: @options",
[
"@options" => implode(", ", static::getNodeEntityReferenceFields()),
]
),
'#element_validate' => [[get_class($this), 'validateReferenceFields']],
];
return parent::buildForm($form, $form_state);
}
/**
* Returns a list of node entity reference field machine names.
*
* We use this for building the form field description and for
* validating the reference fields value.
*/
protected static function getNodeEntityReferenceFields() {
return array_keys(\Drupal::service('entity_field.manager')->getFieldMapByFieldType('entity_reference')['node']);
}
/**
* Turns a text area into an array of values.
*
* Used for validating the field reference text area
* and saving the form state.
*/
protected static function textToArray($string) {
return array_filter(array_map('trim', explode("\n", $string)), 'strlen');
}
/**
* Callback for settings form.
*
* @param array $element
* An associative array containing the properties and children of the
* generic form element.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form for the form this element belongs to.
*
* @see \Drupal\Core\Render\Element\FormElement::processPattern()
*/
public static function validateReferenceFields(array $element, FormStateInterface $form_state) {
$valid_fields = static::getNodeEntityReferenceFields();
foreach (static::textToArray($element['#value']) as $value) {
if (!in_array($value, $valid_fields)) {
$form_state->setError($element, t('"@field" is not a valid entity reference field!', ["@field" => $value]));
}
}
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$this->configFactory->getEditable(static::SETTINGS)
->set('referenceFields', static::textToArray($form_state->getValue('referenceFields')))
->set('maxDepth', $form_state->getValue('maxDepth'))
->set('includeSelf', $form_state->getValue('includeSelf'))
->save();
parent::submitForm($form, $form_state);
}
}

70
modules/islandora_breadcrumbs/src/IslandoraBreadcrumbBuilder.php

@ -4,12 +4,12 @@ namespace Drupal\islandora_breadcrumbs;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Breadcrumb\Breadcrumb;
use Drupal\Core\Breadcrumb\BreadcrumbBuilderInterface;
use Drupal\Core\Link;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\islandora\IslandoraUtils;
/**
* Provides breadcrumbs for nodes using a configured entity reference field.
@ -31,6 +31,13 @@ class IslandoraBreadcrumbBuilder implements BreadcrumbBuilderInterface {
*/
protected $nodeStorage;
/**
* Islandora utils.
*
* @var \Drupal\islandora\IslandoraUtils
*/
protected $utils;
/**
* Constructs a breadcrumb builder.
*
@ -38,10 +45,13 @@ class IslandoraBreadcrumbBuilder implements BreadcrumbBuilderInterface {
* Storage to load nodes.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The configuration factory.
* @param \Drupal\islandora\IslandoraUtils $utils
* Islandora utils service.
*/
public function __construct(EntityTypeManagerInterface $entity_manager, ConfigFactoryInterface $config_factory) {
public function __construct(EntityTypeManagerInterface $entity_manager, ConfigFactoryInterface $config_factory, IslandoraUtils $utils) {
$this->nodeStorage = $entity_manager->getStorage('node');
$this->config = $config_factory->get('islandora.breadcrumbs');
$this->config = $config_factory->get('islandora_breadcrumbs.breadcrumbs');
$this->utils = $utils;
}
/**
@ -54,7 +64,14 @@ class IslandoraBreadcrumbBuilder implements BreadcrumbBuilderInterface {
$nid = $attributes->getRawParameters()->get('node');
if (!empty($nid)) {
$node = $this->nodeStorage->load($nid);
return (!empty($node) && $node->hasField($this->config->get('referenceField')) && !$node->get($this->config->get('referenceField'))->isEmpty());
if (empty($node)) {
return FALSE;
}
foreach ($this->config->get('referenceFields') as $field) {
if ($node->hasField($field)) {
return TRUE;
}
}
}
}
@ -66,50 +83,29 @@ class IslandoraBreadcrumbBuilder implements BreadcrumbBuilderInterface {
$nid = $route_match->getRawParameters()->get('node');
$node = $this->nodeStorage->load($nid);
$breadcrumb = new Breadcrumb();
$breadcrumb->addCacheableDependency($this->config);
$breadcrumb->addLink(Link::createFromRoute($this->t('Home'), '<front>'));
$chain = [];
$this->walkMembership($node, $chain);
$chain = array_reverse($this->utils->findAncestors($node, $this->config->get('referenceFields'), $this->config->get('maxDepth')));
if (!$this->config->get('includeSelf')) {
array_pop($chain);
// XXX: Handle a looping breadcrumb scenario by filtering the present
// node out and then optionally re-adding it after if set to do so.
$chain = array_filter($chain, function ($link) use ($nid) {
return $link !== $nid;
});
if ($this->config->get('includeSelf')) {
array_push($chain, $nid);
}
$breadcrumb->addCacheableDependency($node);
// Add membership chain to the breadcrumb.
foreach ($chain as $chainlink) {
$breadcrumb->addCacheableDependency($chainlink);
$breadcrumb->addLink($chainlink->toLink());
$node = $this->nodeStorage->load($chainlink);
$breadcrumb->addCacheableDependency($node);
$breadcrumb->addLink($node->toLink());
}
$breadcrumb->addCacheContexts(['route']);
return $breadcrumb;
}
/**
* Follows chain of field_member_of links.
*
* We pass crumbs by reference to enable checking for looped chains.
*/
protected function walkMembership(EntityInterface $entity, &$crumbs) {
// Avoid infinate loops, return if we've seen this before.
foreach ($crumbs as $crumb) {
if ($crumb->uuid == $entity->uuid) {
return;
}
}
// Add this item onto the pile.
array_unshift($crumbs, $entity);
if ($this->config->get('maxDepth') > 0 && count($crumbs) >= $this->config->get('maxDepth')) {
return;
}
// Find the next in the chain, if there are any.
if ($entity->hasField($this->config->get('referenceField')) &&
!$entity->get($this->config->get('referenceField'))->isEmpty()) {
$this->walkMembership($entity->get($this->config->get('referenceField'))->entity, $crumbs);
}
}
}

16
modules/islandora_breadcrumbs/tests/src/Functional/BreadcrumbsTest.php

@ -20,7 +20,7 @@ class BreadcrumbsTest extends IslandoraFunctionalTestBase {
*
* @var array
*/
public static $modules = [
protected static $modules = [
'islandora_breadcrumbs',
];
@ -56,7 +56,7 @@ class BreadcrumbsTest extends IslandoraFunctionalTestBase {
/**
* {@inheritdoc}
*/
public function setUp() {
public function setUp(): void {
parent::setUp();
// Create some nodes.
@ -114,6 +114,18 @@ class BreadcrumbsTest extends IslandoraFunctionalTestBase {
// We should still escape it and have the same trail as before.
$this->assertBreadcrumb($this->nodeD->toUrl()->toString(), $breadcrumbs);
// Delete 'A', removing it from the chain.
$this->nodeA->delete();
// The new breadcrumb chain without 'A'.
$breadcrumbs = [
Url::fromRoute('<front>')->toString() => 'Home',
$this->nodeB->toUrl()->toString() => $this->nodeB->label(),
$this->nodeC->toUrl()->toString() => $this->nodeC->label(),
];
$this->assertBreadcrumb($this->nodeD->toUrl()->toString(), $breadcrumbs);
}
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save