Bugzilla – Attachment 184525 Details for
Bug 16631
Show groups/categories of reports to authorized staff
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Help
|
New Account
|
Log In
[x]
|
Forgot Password
Login:
[x]
Bug 16631: Update Report Listing
0001-16631-reports-libraries-1.patch (text/plain), 835.74 KB, created by
Casey Conlin
on 2025-07-23 00:56:44 UTC
(
hide
)
Description:
Bug 16631: Update Report Listing
Filename:
MIME Type:
Creator:
Casey Conlin
Created:
2025-07-23 00:56:44 UTC
Size:
835.74 KB
patch
obsolete
>From 1bf4904dcd46879161ae87dd787e8ed9a8c03789 Mon Sep 17 00:00:00 2001 >From: Casey Conlin <101153261+CaseyConlin@users.noreply.github.com> >Date: Tue, 22 Jul 2025 20:45:17 -0400 >Subject: [PATCH] 16631 reports libraries (#1) >MIME-Version: 1.0 >Content-Type: text/plain; charset=UTF-8 >Content-Transfer-Encoding: 8bit > >* Bug 38928: Handle rft_id or id > >If a semicolon exists, the key should be what's before the semicolon, and its value, whats the after. >Otherwise, default to 'doi' as it was before. > >To test: >1) Enable ILLModule. >2) Visit the following URLs >http://localhost:8080/cgi-bin/koha/opac-illrequests.pl?op=create&backend=Standard&openurl=1&genre=article&id=10.1016/j.cognition.2024.105913 >http://localhost:8080/cgi-bin/koha/opac-illrequests.pl?op=create&backend=Standard&openurl=1&genre=article&id=doi:10.1016/j.cognition.2024.105913 >3) Notice that in both cases, only the doi value '10.1016/j.cognition.2024.105913' should be present in the input. > >Signed-off-by: Caroline Cyr La Rose <caroline.cyr-la-rose@inlibro.com> >Signed-off-by: Baptiste Wojtkowski <baptiste.wojtkowski@biblibre.com> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 38928: (QA follow-up) > >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 23010: Add system preference PreventWithdrawingItemsStatus > >To test: > >1. APPLY PATCH, updatedatabase, and restart_all >2. Search for the system preference, PreventWithDrawingItemsStatus >3. Select both in transit and checked out, then save >4. Find an item and put it in transit. >5. Now from the item detail page, go to manage items > edit item >6. Using the item that was put in transit, attempt to change the status to withdrawn. >7. You should see an error at the top of the page 'In transit item cannot be withdrawn.' Make sure the item did not get marked as withdrawn. >8. Using a different item, do a checkout >9. Now from the item detail page, go to manage items > edit item >10. Using the checked out item, attempt to change the status to withdrawn. >11. vYou should see an error at the top of the page 'Onloan item cannot be withdrawn.' Make sure the item did not get marked as withdrawn. > >12. Now, using the in transit item, go to the items tab. ( catalogue/moredetail.pl?biblionumber=x ) >13. Under Statuses, attempt to withdraw the item. >14. You should see an error near the top of the page "Cannot withdraw item in transit". >15. Now, using the checked out item, go to the items tab. ( catalogue/moredetail.pl?biblionumber=144 ) >16. Under Statuses, attempt to withdraw the item. >17. You should see an error near the top of the page "Cannot withdraw checked out item.". > >18. Using both the in-transit and checked out item, attempt to modify the status through batchMod. >19. Ensure that the items are not being updated to withdrawn. > >20. Turn off PreventWithDrawingItemsStatus system preferece. >21. Going through each of the steps again, except this time your items should successfully withdraw. > >prove t/db_dependent/Koha/Item.t > >Signed-off-by: Roman Dolny <roman.dolny@jezuici.pl> >Signed-off-by: Emily Lamancusa <emily.lamancusa@montgomerycountymd.gov> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 23010: Add ability to block withdrawning of items with certain statuses > >Signed-off-by: Roman Dolny <roman.dolny@jezuici.pl> >Signed-off-by: Emily Lamancusa <emily.lamancusa@montgomerycountymd.gov> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 23010: Handle error in updateitem.pl and template > >Signed-off-by: Roman Dolny <roman.dolny@jezuici.pl> >Signed-off-by: Emily Lamancusa <emily.lamancusa@montgomerycountymd.gov> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 23010: Add unit tests > >Signed-off-by: Roman Dolny <roman.dolny@jezuici.pl> >Signed-off-by: Emily Lamancusa <emily.lamancusa@montgomerycountymd.gov> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 23010: Fix file permission of atomicupdate > >Signed-off-by: Emily Lamancusa <emily.lamancusa@montgomerycountymd.gov> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 23010: Update pref name to PreventWithdrawingItemsStatus > >Signed-off-by: Emily Lamancusa <emily.lamancusa@montgomerycountymd.gov> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 23010: Report on count of items not modified via batchMod > >Signed-off-by: Emily Lamancusa <emily.lamancusa@montgomerycountymd.gov> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 23010: Handle error in C4/Items.pm using try/catch > >Signed-off-by: Emily Lamancusa <emily.lamancusa@montgomerycountymd.gov> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 23010: Add tests for AutomaticItemModificationByAge > >Signed-off-by: Emily Lamancusa <emily.lamancusa@montgomerycountymd.gov> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 23010: Remove JS added by mistake > >Signed-off-by: Emily Lamancusa <emily.lamancusa@montgomerycountymd.gov> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 23010: Clean up try/catch in additem.pl > >Signed-off-by: Emily Lamancusa <emily.lamancusa@montgomerycountymd.gov> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 23010: More unit tests > >Signed-off-by: Emily Lamancusa <emily.lamancusa@montgomerycountymd.gov> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 23010: (QA follow-up) a few trivial fixes > >- Update the dbrev to use say_success, and make dbrev and installer > consistent >- Change error div in moredetail.tt from info to warning > >Signed-off-by: Emily Lamancusa <emily.lamancusa@montgomerycountymd.gov> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 23010: Adjust tests > >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 23010: DBRev 25.06.00.001 > >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 39939: Cancel selected holds button on waitingreserves.pl is the same color as the background > >This patch makes some minor changes to the "Holds awaiting pickup" page >so that the cancel buttons are clear and usable. > >The patch also wraps the reference to the >TransferWhenCancelAllWaitingHolds system preference in a permission >check so that it can be linked to the preference when appropriate. > >To test you should have some waiting holds in three categories: > >1. Holds which have not been waiting longer than the number of days > specified in the ReservesMaxPickUpDelay system preference. > >2. Holds which have been waiting longer. I belive the only way to test > this is to manually update the expiration date of an already waiting > hold so that it is in the past. > >3. Waiting holds which a patron has requested be cancelled. (Requires > that the patron and hold are allowed according to the "Cancellation > allowed" setting under Administration -> Circulation and fine rules > -> Default waiting hold cancellation policy. > - Log in to the OPAC as a patron who has a waiting hold. On the "Your > summary" page, under the "Holds" tab, click the "Request to cancel" > button for at least one of the waiting holds. > >- In the staff interface, go to Circulation -> Holds awaiting pickup. > - Under each tab, check the appearance of the "Cancel selected" > buttons (and the "Cancel and transfer all" button under "Holds > awaiting past..." tab). >- When testing as a user with system preference privileges, and the > TransferWhenCancelAllWaitingHolds preference set to "Don't transfer," > the hint shown under the "Holds awaiting past..." tab should be linked > to system preferences. A user without system preference privileges > should not see a link. > >Sponsored-by: Athens County Public Libraries >Signed-off-by: David Nind <david@davidnind.com> >Signed-off-by: Emily Lamancusa <emily.lamancusa@montgomerycountymd.gov> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 39939: (follow-up) Fix terminology - syspref to system preference > >Spell system preference in full, instead of syspref. > >Signed-off-by: David Nind <david@davidnind.com> >Signed-off-by: Emily Lamancusa <emily.lamancusa@montgomerycountymd.gov> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 39206: Add an allowlist to Koha::CookieManager > >This patch adds a bit more control to what CookieManager does by >adding a hardcoded allowlist of cookie names that are cleared at >logout. Allowing at the same time to add entries to that list by >using koha-conf <remove_cookie> lines or removing entries from the >hardcoded list by using <do_not_remove_cookie> lines. > >The patch fixes the expiration of cookies that should be removed >by passing max-age 0. > >Also it adds a theoretical path correction for always_show_holds but >since we do not clear that cookie, it is currently unused. This seems >to be the only Koha cookie where we use a longer path. > >Test plan: >Run t/CookieManager.t > >Go to OPAC, login, select a few OPAC search results and send them >to cart. This would create cookie bib_list. (Check dev tools.) > >Logout from OPAC and check cookie in your browser dev tools. What >you see, depends on the browser. But the cookie should be either >gone or empty and expired (FF: Session). > >Now add a <do_not_remove_cookie> line for bib_list in koha-conf. >Restart all. Repeat search, add to cart. Logout. Check again in dev >tools that bib_list is not empty, not expired. > >Check out an item. And click on 'Always show checkouts...' on >the patron checkout form. This should create the cookie with >value DO. Logout from intranet. Check that cookie was not affected. >Now add a <remove_cookie> line for the following cookie: >issues-table-load-immediately-circulation. Restart all. >Login and logout from staff again. Check that cookie is empty >and expired, or just gone. > >Bonus for devs: Create some custom cookie, and test keeping or >removing it similar as above. > >Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl> >Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de> >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 39206: Modify koha-conf templates > >Test plan: >Just read the patch. > >Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl> >Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de> >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40242: Remove double operator in Quotes.pm > >Test plan: >Read patch. >Run t/db_dependent/Koha/Quotes.t > >Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 39912: Update RealTimeHoldsQueue when hold pickup location is updated > >When a hold's pickup location is changed we should rebuild the holds queue. > >To test: >1 - Enable RealTimeHoldsQueue >2 - Enable LocalHoldsPriority where pickup library matches home library >3 - Place two biblio level holds on a bib with one item for two different patrons for delivery to a different library then the home library >4 - Check the queue - firstpatron should have priority >5 - Change the pickup location for the second hold to be picked up at the home library >6 - Check the queue, unchanged >7 - Change the pickup location back >8 - Apply patch, restart all >9 - Change the pickup location to the items home library again >10 - Check the queue >11 - Second patron has now been given priority > >Signed-off-by: Michelle Spinney <mspinney@clamsnet.org> >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40022: Add the 25.11 release team > >This patch updates the teams.yaml to include the voted in 25.11 >release team. > >Test plan >1/ Check against https://wiki.koha-community.org/wiki/Release_Teams > >Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40022: Add missing roles to the include > >This patch adds missing role mappings to the about-team.inc template >to ensure all roles defined in teams.yaml are properly displayed. > >Added role mappings include: >- manager_assistants (plural form) >- maintainer_assistants (plural form) >- maintainer_mentors (plural form) >- accessibility_advocates (plural form) >- security_manager >- meeting_facilitator >- social_media >- website > >Also adds display logic for historical roles that may not be present >in current teams but appear in contributor lists from teams.yaml. > >This ensures complete coverage of all team roles across historical >and current release teams. > >Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40022: Condense display by hiding versions under ellipses > >This patch improves the contributor section display in the about page >by implementing a condensed view for version lists. When a contributor >has more than 2 versions for a role, only the first 2 are shown with >an ellipsis (...) that can be hovered to reveal the remaining versions. > >The implementation: >- Sorts versions in reverse chronological order (most recent first) >- Shows first 2 versions directly >- Hides additional versions under a hoverable ellipsis using Bootstrap tooltips >- Adds CSS styling for the ellipsis with dotted underline and help cursor > >This makes the contributor lists more readable while preserving access >to complete version information. > >Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40022: Sync teams in Koha, Wiki and release-tools > >Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40022: (QA follow-up) Tidy about-team.inc, about.tt > >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40101: Unit tests > >Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io> >Signed-off-by: Kristi Krueger <kkrueger@cuyahogalibrary.org> >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40101: Add `Koha::Patron->can_place_holds()` > >This patch adds a new method to the `Koha::Patron` class. > >As described in the POD, this method makes checks on the patron's >current situation and returns a boolean telling if it should be allowed >to place holds. This doesn't check specific item/biblio holdability. It >only covers patron specific conditions that could prevent holds to be >placed. > >Both in `opac-reserve.pl` and `reserve/request.pl` this checks are all >run, and all blocking situations are presented to the UI user. Feedback >in community (so far) expressed this is a desirable situation, so that >behavior can be retained with the use of the `no_short_circuit` >parameter. > >If `no_short_circuit` is not passed, then the routine will return on the >first blocking condition it finds. > >An `overrides` parameter is added, allowing to be passed a list of >strings that match the possible error messages. > >This way, this method can be asked not to check for patron expiration, >for example, and things like that. This is of course designed with the >API overrides use case in mind. > >To test: >1. Apply this patches >2. Run: > $ ktd --shell > k$ prove t/db_dependent/Koha/Patron.t >=> SUCCESS: Tests pass! >3. Sign off :-D > >Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io> >Signed-off-by: Kristi Krueger <kkrueger@cuyahogalibrary.org> >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40101: Make opac-reserve.pl use `$patron->can_place_holds()` > >Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io> >Signed-off-by: Kristi Krueger <kkrueger@cuyahogalibrary.org> >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 39951: Fix column filter offsets > >Test plan: >1) Enable ERM module >2) Create an agreement and a license with a vendor >3) The table filters will be out of sync >- Agreements - only the vendor filter is incorrect, it sits above the name column instead of the vendor column >- Licenses - all dropdown filter options will be left shifted by one >4) Apply patch >5) yarn js:build >6) Hard refresh the browser >7) All the filters should now be above the correct columns > >Signed-off-by: Roman Dolny <roman.dolny@jezuici.pl> > >Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 39498: Correct patron restrictions displays > >This patch updates the way patron restrictions are displayed in the OPAC >and staff interface, with the intention of making the details of each >restriction more clear. In the OPAC, a new include file is created to be >reused on the holds page and the user summary page. opac-user.pl is >updated to allow a parameter to be passed to automatically open the >"Overdues" tab. > >Test plan: >1. Before applying the patch, check how the display is currently. First > set syspref PatronRestrictionTypes to "Allow" >2. Add two different new restriction types, fill out both "Code" and > "Label" >3. Go to a patron's checkout page and add one of each of these > restrictions, but do not fill out the comment field when adding them! >4. Observe that in the checkouts page and details page of the patron, it > just says "Restricted since..." and does not show the restriction > types. >5. Add another restriction, this time fill out the comment field >6. Observe that now in checkout and detail pages, the labels from the > restriction types are listed and the comment shown. >7. Apply patch >8. Go to patron's details and checkout pages and check the restrictions > are now displayed correctly, that is the text "with the explanation" > is only shown for the restrictions that actually have a comment > 8b. Log in to the OPAC with that patron and check they are displayed > correctly in their summary page and when the patron is trying to > place a hold. >9. Go to a different patron's account and add a restriction without a > comment >10. Check it is now displayed correctly >11. Add more restrictions and check they are displayed correctly. Also > add an OVERDUES restriction: > 11a. Find a patron with an overdue item checked out (or create an > overdue checkout on a patron's account) > 11b. Go to Tools -> Overdue notice/status triggers > 11c. Set up a rule for that patron's category, e.g. "First" -> > Delay: 1, make sure to mark the check box for "Restrict" and > "Print" > 11d. Run misc/cronjobs/overdue_notices.pl > 11e. That patron should now also have an OVERDUES restriction >12. Turn PatronRestrictionTypes off again and check the patrons if > everything is still displayed correctly >13. Go to another different patron and add a restriction with no > comment, check if it is displayed correctly >14. Add another restriction with a comment and check it is displayed > correctly >15. Log in to the OPAC with the patron that had OVERDUES restriction and > check they are displayed correctly in their summary page and when > the patron is trying to place a hold. >16. Same thing for the other patron without the OVERDUES restriction. > >Signed-off-by: David Nind <david@davidnind.com> >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 39657: Unit tests > >Signed-off-by: Kristi Krueger <kkrueger@cuyahogalibrary.org> >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 39657: Spec changes > >Signed-off-by: Kristi Krueger <kkrueger@cuyahogalibrary.org> >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 39657: Add more checks and overrides to hold creation endpoint > >This patch adds new behaviors to the `POST /holds` endpoint. > >It uses the framework we created in the past for overriding policy >rules, and add new options: > >* expired >* debt_limit >* bad_address >* card_lost >* restricted >* hold_limit > >Some status codes are changed from 403 to 409. This should be revisited >accross the codebase, as I think we made a wrong choice. Happy to review >in this bug. > >The feature makes use of the newly introduced >`$patron->can_place_holds()` method, which accepts to be passed through >the overrides. > >To test: >1. Apply this patches >2. Run: > $ ktd --shell > k$ yarn api:bundle > k$ koha-plack --restart kohadev > k$ prove t/db_dependent/api/v1/holds.t >=> SUCCESS: Tests pass! >3. Test the endpoint with the various scenarios using your favourite > REST tool (Postman!) >4. Sign off :-D > >Signed-off-by: Kristi Krueger <kkrueger@cuyahogalibrary.org> >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40079: Add ul/ol and li to allowed tags in notes profile > >This patch add the unordered list, ordered list and list item html tags >to the list of allowed tags in C4::Scrubber. > >Test plan: >1) Add a course reserve and include the following in the public note > field: > <ul> > <li>First item</li> > <li>Second item</li> > </ul> >2) View the course details page and note "First item Second item" all > appears on the same line (the ul and li tags have been stripped) >3) View the course reserves page and note the same issue in the public > notes field of the table display >4) Apply this patch >5) Repeat 2 and 3 and note you now have a bullet pointed list displayed > in both places >6) The display of the note on the details page may look mis-aligned.. > that will be dealt with on it's own bug > >Signed-off-by: David Nind <david@davidnind.com> > >Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40079: (follow-up) Add dl, dt, dd tags to notes profile scrubber > >Adds support for HTML description list elements (dl, dt, dd) to the >'note' profile in the HTML scrubber configuration. This allows >librarians to use description lists in note fields for better >structured content presentation. > >Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40079: (follow-up) Update unit test > >Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40079: (follow-up) Improve test coverage and fix undef handling > >This patch modernizes the Scrubber.t unit test with comprehensive >coverage following Koha best practices: > >- Uses subtest structure for logical grouping >- Tests all scrubber types (default, comment, note) >- Includes security testing for XSS prevention >- Covers edge cases and error handling >- Tests new list elements added in previous commits > >Also fixes C4::Scrubber to handle undef parameters cleanly by >treating them as 'default' type instead of generating warnings. > >Test plan: >1. Run prove t/Scrubber.t >2. Verify all tests pass without warnings >3. Confirm comprehensive coverage of module functionality > >Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40254: Fix error in overrides check allowing too much > >This patch fixes a subtle (yet naive) error making the controller not >care about blocking conditions if they were more than one and the first >one was overridden. > >To test: >1. Apply the regression tests >2. Run: > $ ktd --shell > k$ prove t/db_dependent/api/v1/holds.t >=> FAIL: Tests fail! Some tests expect a 409 (denied for a conflict) but >return a 201! >3. Apply this patch >4. Repeat 2 >=> SUCCESS: Tests pass! >5. Sign off :-D > >Signed-off-by: Lisette Scheer <lisette@bywatersolutions.com> >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40254: Regression tests > >This patch makes the tests related to `can_place_holds` cover more use >cases, and highlight an underlying issue in the controller. > >Signed-off-by: Lisette Scheer <lisette@bywatersolutions.com> >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40039: Add production enhancements to build-git-snapshot tool > >this patch adds some useful features to allow the build-git-snapshot tool to build production packages > >the patch adds the following options: > > --urgency, -u > the urgency string for the resulting package. Default is '$urgency'. > > --incr, -i > set debian revision (default = '-1') > > --(no)autochangelog > whether or not to update the debian/changelog file. > Default is to update it. > >note: this patch does not change any of the existing behaviour of the tool > >test plan to come... > >Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 39534: Hide reports subgroup filter if there are no subgroups > >This patch updates the JavaScript which runs on the list of saved SQL >reports, adding a check for report subgroups to the display of the >report subgroups dropdown. The patch also hides the subgroups dropdown >if there is no saved tab, in which case the tabsInit function doesn't >run. > >To test you should have multiple reports organized in multiple groups >and subgroups. See the test plan on Bug 39534. > >- Go to Reports -> Saved SQL reports. >- If the page loads with the "All" tab active, there should be no > subgroups dropdown. >- If the page loads under a different tab, the subgroups dropdown should > only show if that group has subgroups. >- Switch between the various report group tabs to confirm that the > subgroups dropdown is shown or hidden correctly based on the existence > of subgroups. > >Sponsored-by: Athens County Public Libraries >Signed-off-by: Anneli Ãsterman <anneli.osterman@koha-suomi.fi> >Signed-off-by: Lisette Scheer <lisette@bywatersolutions.com> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 39954: Fix cataloging search results incorrect menu markup > >This patch corrects the dropdown menu markup in the "Records found in >the reservoir" section of the cataloging search results page. It was not >upgraded to Bootstrap 5 markup. > >To test, apply the patch and go to Cataloging. > >- Click "New from Z39.50/SRU" >- Perform a search which will return multiple results. >- Close the popup window and perform the same search in the "Cataloging > search" header form. >- Scroll down to the "Records found in the reservoir" section. >- Click the "Actions" menu on one of the results. The menu should look > correct. > >Sponsored-by: Athens County Public Libraries >Signed-off-by: Roman Dolny <roman.dolny@jezuici.pl> >Signed-off-by: Lisette Scheer <lisette@bywatersolutions.com> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40250: Wrong link to NoIssuesChargeGuarantorWithGuarantees in patron category page > >This patch corrects the link to the NoIssuesChargeGuarantorsWithGuarantees >system preference in the patron category creation/modification page. > >To test: >1. Apply patch >2. Go to Administration > Patron categories >3. Click 'New category' >4. Check that the hint under 'Guarantors with guarantees checkout charge > limit' links to the correct system preference >5. Click on the link and make sure it links to the correct system preference > >Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40250: Terminology changes 'preference' to 'system preference' > >This patch corrects the terminology used in the patron category >settings page, changing 'preference' to 'system preference'. >https://wiki.koha-community.org/wiki/Terminology#P > >To test: >1. Apply patch >2. Go to Administration > Patron categories >3. Click 'New category' >4. Check that the hints under 'Checkout charge limit', 'Guarantees > checkout charge limit', and 'Guarantors with guarantees checkout > charge limit' use 'system preference' and not 'preference' > >Signed-off-by: Owen Leonard <oleonard@myacpl.org> > >Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40244: Fix typo in branchoverdues.tt > >This patch fixes a trivial typo. The text appears in two more places >with the right form on the same template. > >Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io> >Signed-off-by: Owen Leonard <oleonard@myacpl.org> > >Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 39871: Don't pass undef for non-nullable fields from batch mod tool > >To test: >1. Set not-for-loan, withdrawn, and damaged statuses on some items >2. Try to edit only those items with the Batch item modification tool >3. Check the checkboxes to clear the withdrawn, not-for-loan, and > damaged statuses >4. Click Save >5. View the background job report > --> Note that it says the items could not be modified >6. Look at the items > --> The statuses have not been cleared >7. Apply patch and restart_all >8. Repeat steps 2-5 > --> The background job report says the modifications were successful >9. View the items > --> The statuses have been cleared > >Signed-off-by: David Flater <flaterdavid@gmail.com> >Signed-off-by: Owen Leonard <oleonard@myacpl.org> > >Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 39871: (QA follow-up) Do not hardcode nullable check > >Lets check DBIx's columns_info. > >Test plan: >Try to blank an integer column and a string column. Verify results. > >Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 34776: Restore patron messaging preferences if error occurs during new account creation > >If error occurs while creating a new patron messaging >preferences are either lost or reseted as default >fro patron category. This patch adds new method >restore_form_values to restore already added messaging >preferences. > >To test: >1. Enable syspref EnhancedMessagingPreferences. >2. Create a new patron but cause an error to prevent it from >saving (e.g. add wrong age). >3. Add messaging preferences for patron (if there are default >preferences, add also new ones or remove defaults). >4. Attempt to save. >=> Note that messaging preferences were lost and you need to >readd them. >5. Apply this patch. >6. Repeat steps from 2 to 4. >=> Note that messaging preferences which you added are kept >even if error occurs. > >Sponsored-by: Koha-Suomi Oy >Signed-off-by: Jesse Maseto <jesse@bywatersolutions.com> >Signed-off-by: David Nind <david@davidnind.com> > >Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 34776: Unit tests > >Just a small beginning.. > >Signed-off-by: David Nind <david@davidnind.com> > >Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 34776: Remove unneeded if statement > >We don't need to check if $op is add_form after >error, since this value always changes. > >Sponsored-by: Koha-Suomi Oy >Signed-off-by: David Nind <david@davidnind.com> > >Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 34776: Add unit tests for sub restore_form_values > >Test that input values don't change when send to the >sub restore_form_values. > >To test prove t/Form_MessagingPreferences.t > >Sponsored-by: Koha-Suomi Oy >Signed-off-by: David Nind <david@davidnind.com> > >Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40030: Escape values when displaying action logs comparison > >When viewing the action logs for a system preference change, there is an >option to view a comparison between different log lines for the same >system preference. However, if a system preference value contains HTML >markup (e.g. in IntranetUserJS), the diff text is only escaped on >sections where a change is marked, so unchanged lines containing HTML >markup result in the HTML being rendered. > >Escape the string values for unchanged sections as well, to ensure that >HTML markup in the diff text will not be rendered. > >To test: >1. Edit IntranetUserJS and add some code that contains HTML markup. > For example: >$("body").append('<a id="helloworld" class="btn btn-default">Say hi</a>'); >2. Save the preference >3. Make another change to IntranetUserJS that does not affect the line > you just added, and save again to ensure there are at least 2 action > log lines for comparison >4. Go to Tools > Log viewer >5. Select System Preferences module and Submit >6. Find the log entries for the above two revisions to IntranetUserJS >7. Check the Compare checkboxes on those entries, and click View > comparison >--> Note that the HTML button is rendered in the comparison >8. Apply patch >9. Clear browser cache and refresh the page >10. Repeat step 7 >--> The button is not rendered and the code is displayed as written >11. Make a change to the line you added in step 1 (for example, replace > "Say hi" with "Say hello") >12. Refresh the page >13. Check the checkboxes for the newest 2 log entries and click View > comparison to view the diff for that last change >--> Confirm the change is shown correctly - the button is not rendered > and nothing has been double-escaped > >Signed-off-by: Phil Ringnalda <phil@chetcolibrary.org> > >Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40249: "Copy settings" should be "Copy permissions" > >This patch changes the "Copy sesttings" button on the patron permissions >page to return it to its previous text "Copy permissions." This is >consistent with related controls on the page. > >To test, apply the patch and locate a patron in the staff interface. >From their checkout or detail page choose More -> Set permissions from >the toolbar. > >In the toolbar of permissions-related controls you should see a button >labeled "Copy permissions." > >Sponsored-by: Athens County Public Libraries >Signed-off-by: David Flater <flaterdavid@gmail.com> > >Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40164: Add Template::Plugin::JSON dependency > >Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io> >Signed-off-by: David Cook <dcook@prosentient.com.au> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 31632: Link a suggestion to an existing order > >This enhancement allows library staff to link an order (in an unclosed basket, or a standing order) to an accepted suggestion. > >To test: > >1) In the staff interface, go to Suggestions and create at least two suggestions. >2) Mark at least two suggestions as Accepted. >3) Go to Acquisitions, create a vendor if you don't have one already. >4) Have or create two baskets - one should be standing, and one not standing (normal). >5) In the normal basket (not standing), add an order from any source but NOT from a suggestion. >6) When you save the order, there should be a new link in the Modify column of the Orders table - "Search for suggestion to link". Click this. >7) Choose one of your suggestions to link to the order. >8) You should be redirected back to the basket. Confirm your order has been correctly linked to the suggestion. Confirm there is no link to "Search for suggestion to link" for this order now that a suggestion has already been linked. >9) Add another order to this basket from any source but NOT from a suggestion. >10) Close the basket. Confirm there is no link to "Search for suggestion to link" because the basket is closed, but you could reopen the basket to link the order if you wanted to. >11) Go to Baskets in the left navigation so you can find your standing basket. >12) Add an order to your standing basket from any source but NOT from a suggestion. >13) Go to the vendor and Receive shipments. Receive your standing order and finish receiving. >14) Go to the standing basket. There should be a new unreceived standing order for the record you just received, with a link to "Search for suggestion to link". Click this. >15) Choose a suggestion to link to the order. >16) You should be redirected back to the basket. Confirm your orders for this record have been correctly linked to the suggestion. > >Sponsored-by: Pymble Ladies' College >Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io> >Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com> >Signed-off-by: Emily Lamancusa <emily.lamancusa@montgomerycountymd.gov> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 31632: (follow-up) Simplify link text > >Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io> >Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com> >Signed-off-by: Emily Lamancusa <emily.lamancusa@montgomerycountymd.gov> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 31632: (follow-up) CSRF protection for linking order to suggestion > >Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com> >Signed-off-by: Emily Lamancusa <emily.lamancusa@montgomerycountymd.gov> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 31632: (QA follow-up) Adjust TT filters and remove extra form-submit.js > >Signed-off-by: Emily Lamancusa <emily.lamancusa@montgomerycountymd.gov> >Signed-off-by: Aleisha Amohia <aleishaamohia@hotmail.com> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 31632: (QA follow-up) Align behavior with order from suggestion > >If an order is added from a suggestion in the first place, the >suggestion automatically changes status to ORDERED, and a notice is >sent out to the patron. If a suggestion is linked to the order after >the fact, it should move to ORDERED and notify the patron as well. > >Signed-off-by: Emily Lamancusa <emily.lamancusa@montgomerycountymd.gov> >Signed-off-by: Aleisha Amohia <aleishaamohia@hotmail.com> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 33729: Added date accessioned to items search table > >Test plan: >1. Apply patch >2. Go to item search > 1. Notice that there is now a date accessioned column > 2. Export the table into a CSV and notice that there is also a date accessioned column there >3. Sign off and have an amazing day :D > >Signed-off-by: Emmanuel Bétemps <e.betemps@gmail.com> >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 33729: Added dateaccessioned to column_settings.yml > >Signed-off-by: Andrew Auld <andrew.auld@openfifth.co.uk> >Signed-off-by: Emmanuel Bétemps <e.betemps@gmail.com> >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 33729: Added datacolumn for date accessioned > >Signed-off-by: Emmanuel Bétemps <e.betemps@gmail.com> >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 33729: (QA follow-up) Tidy > >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 38939: Add reservenote column to members/holdshistory.pl > >To test: >1. APPLY PATCH, restart_all >2. Make some holds for patrons in both the staff client and the OPAC. ( For OPAC turn on OpacHoldNotes ) >3. Make sure you add some notes to each of these holds. >4. Go to the patron account and click on the 'Holds history' tab. >5. Make sure the hold note is there and looks correct. >6. Ensure you can hide the coluimn properly via Table settings. > >Signed-off-by: Andrew Auld <andrew.auld@openfifth.co.uk> >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 39441: Some staff interface templates have div.container instead of div.container-fluid > >This patch updates a few templates so that div.container is replaced >with div.container-fluid. div.container gives us a fixed maximum width >that isn't consistent the rest of Koha. > >To test, apply the patch and go to the staff interface. > >- Perform a catalog search which will return multiple results. >- In the search results, check multiple results and click "Add to cart." >- Click the cart icon in the top menu to trigger the cart popup. >- The contents of the cart popup should fill the whole width of the > window. >- The other instances of this are less noticeable because of how the > pages work. These changes are more about consistency: > - The OPAC place hold page > - The club enrollment process (both in the OPAC and the staff > interface) > - As long as these interactions work correctly all is good. > >Sponsored-by: Athens County Public Libraries >Signed-off-by: Lisette Scheer <lisette@bywatersolutions.com> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40281: Hide the type column on the patron circulation history page > >This fixes the patron circulation history page so that the type column >is hidden. > >The type column on a patron's circulation history page was hidden by >Bug 38954 - Hide checkout type using colvis. > >However, the commit for Bug 37273 - Add ID column to Agreements >table in the ERM, inadvertently reversed hiding the column. > >Test plan: >1. Check out an item to a patron (for example, 39999000011418 to Mary > Burton). >2. Look at the patron's circulation history page (Patrons > Mary Burton > > Circulation history), and note there is a 'Type' column shown > with a value of 'standard_checkout'. >3. Note that you can't hide the column, from either selecting the > 'Columns' or 'Configure' options for the table. >4. Apply the patch, restart everything (restart_all), and clear your > browser cache. >5. Repeat step 2. Note that there is now no 'Type' column shown and > it is not configurable, from either selecting the 'Columns' or > 'Configure' options for the table. > >Signed-off-by: David Nind <david@davidnind.com> >Signed-off-by: Laura_Escamilla <laura.escamilla@bywatersolutions.com> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 34561: Move IntranetReportsHomeHTML to HTML customizations > >This patch moves the IntranetReportsHomeHTML system preference into >HTML customizations, making it possible to have language- and >library-specific content. > >To test you should have some content in the IntranetReportsHomeHTML >system preference before applying the patch. Apply the patch and run the >database update process. > >- In the staff client, go to Tools -> HTML customizations and verify > that the content from IntranetReportsHomeHTML is now stored there. >- The HTML customization entry form should offer > IntranetReportsHomeHTML as a choice under "Display location." >- Update and reinstall active translations (for instance fr-FR): > - perl misc/translator/translate update fr-FR > - perl misc/translator/translate install fr-FR >- Enable the translation if necessary under Administration -> System > preferences -> language. >- Edit the IntranetReportsHomeHTML HTML customization and add unique > content to the "fr-FR" tab. > >- Go to the reports home page. You should see the > content you added to the IntranetReportsHomeHTML HTML > customization. >- Switch to your updated translation and confirm that the content you > added for your translation shows up correctly. >- Go to Administration -> System preferences and search for > "IntranetReportsHomeHTML." It should return no results. > >Sponsored-by: Athens County Public Libraries >Signed-off-by: Ray Delahunty <lib-systems@arts.ac.uk> >Signed-off-by: Lisette Scheer <lisette@bywatersolutions.com> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 34561: (QA follow-up) Rename to StaffReportsHome to match othe html customizations > >I noticed the rest of the html customizations don't have html in the option name, so I renamed for consistency. > >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 34561: DBRev 25.06.00.002 > >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Revert "Bug 40164: Add Template::Plugin::JSON dependency" > >This reverts commit d3783f8859cbcdd3dce01f37c41bf47981200eea. > >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* reports_branches table SQL established > >* Make DBversion in sync with the Koha module > >* Add Filter Reports system preference. Add UI for managing reports. > >* Add ReportsBranch.pm > >* Not working. Trying to get branches to show up in guided_reports_start.tt. > >* Bug 40261: Tidy debian/build-git-snapshot > >Signed-off-by: David Flater <flaterdavid@gmail.com> >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 20601: Untranslatable strings in circulation statistics > >This patch makes some updates to the circulation statistics wizard in >order to make more information translatable. > >The patch also adds some template plugins to the output of filter >options in order to be able to show descriptions instead of codes, and >corrects the markup for the 'Patron library' dropdown which wasn't >working. > >To test apply the patch and go to Reports -> Statistics wizards -> >Circulation. > >- Test setting some or all options in the "Filter column" and running > the report. > - Confirm that the "Patron library" dropdown works. >- At the top of the results there should be a list of the filter options > you selected and their values. >- All the filter labels should be correct, and the filter values should > should be in their full/description form rather than in code form > (e.g. library, item type, collection code, sorting field, etc.) > >Sponsored-by: Athens County Public Libraries >Signed-off-by: David Nind <david@davidnind.com> >Signed-off-by: Aleisha Amohia <aleishaamohia@hotmail.com> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 37305: Subfield order matters > >This patch updates the use of a hash to store and then pass subfields to >the MARC::Field new method to an array with key/value pairs using fat comma's. > >This allows us to maintain the integrity check that we're passing string >as the first arguament in each pair whilst also allowing us to ensure >fields are passed in the order they are found. > >Sponsored-by: Open Fifth <https://openfifth.co.uk> >Signed-off-by: Heather Hernandez <flyingendpaper@gmail.com> >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 37305: Remove C4::Biblio::prepare_host_field > >This patch updates the only use of prepare_host_field to use the well >tested Koha::Biblio->generate_marc_host_field method instead and then >removes the aforementioned prepare_host_field method from C4::Biblio. > >Test plan >1) Ensure the "enhanced workflow" for creating analytics still functions > as expected. > >Sponsored-by: Open Fifth <https://openfifth.co.uk> >Signed-off-by: Heather Hernandez <flyingendpaper@gmail.com> >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 37305: (follow-up) Only add control number subfield when defined > >The previous commit introduced a bug where the control number subfield 'w' >was being added to the MARC field even when undefined, causing test failures. > >This patch adds a condition to only push the control number subfield when >the value is actually defined, matching the pattern used for other subfields >in the method. > >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40046: (bug 39606 follow-up) Remove waits and screenshots > >From bug 39606: >""" >Why are you taking screenshots? >The wait statements will certainly cause random failures. > >cy.wait(5000); >cy.wait(2000); > >Notice the following couple of lines: > cy.get("#fileuploadstatus").contains("100%"); > cy.get("legend").contains("Look for existing records in catalog?").should('be.visible'); > >It makes Cypress wait 10s until "100%" appears (ie. the file is updated), then the block with the dropdown lists you are going to interact with to be visible. > >You should almost never use cy.wait. >""" > >Test plan: > yarn cypress run --spec t/cypress/integration/Tools/ManageMarcImport_spec.ts >should still pass > >Signed-off-by: David Nind <david@davidnind.com> >Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40046: Prevent random failures when verifying select's values > >Commands like .select() are queued. We need to prevent the should to >happen before the select is executed. > >Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40046: Use xml and auto-select it > >Absolutely no idea why the previous code was not selecting MARCXML (MARC >was passed and so the import failed) > > cy.get('select[name="format"]') > .select("MARCXML", { force: true }) > .should("have.value", "MARCXML"); > >But I found a bug in the JS code: we are supposed to auto-detect .xml >file and pre-select MARCXML. So let's rely on that behaviour! > >sample.mrc is actually an xml file so it makes sense to rename it > >Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 39289: Display all checkouts in the batch extend due date tools > >When paginate is false, pageLength is still used. >If more than 20 checkouts should be displayed, only 20 will. > >Test plan: >Have more than 20 checkouts, use the batch extend due date tool and >notice that all checkouts are displayed when this patch is applied. > >Signed-off-by: Magnus Enger <magnus@libriotech.no> >Tested on top of bug 39081. > >Signed-off-by: Baptiste Wojtkowski <baptiste.wojtkowski@biblibre.com> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40277: Remove warn in C4::Koha::GetAuthorisedValues() > >When using the OPAC and you are not logged in, there are warnings >in the logs: >[WARN] Use of uninitialized value $branch_limit in concatenation (.) or string > >It comes from C4::Koha::GetAuthorisedValues() : > my $branch_limit = C4::Context->userenv ? C4::Context->userenv->{"branch"} : ""; > my $cache_key = "AuthorisedValues-$category-$opac-$branch_limit"; > >C4::Context->userenv->{"branch"} can be undef > >The patch fixes using C4::Context::mybranch(). > >Test plan: >1. Set the OpacAdvancedSearchTypes system preference to something > other than "itemtypes", for example "loc". >2. Go to OPAC without logging in, and perform a search. >3. Check the /var/log/koha/kohadev/plack-opac-error.log and note > the warnings in the log: > [WARN] Use of uninitialized value $branch_limit in concatenation (.) or ... > (Alternative, tail the logs: tail -f /var/log/koha/kohadev/*.log >4. Apply the patch. >5. Repeat step 2 and check that there are no longer any warnings. >6. Sign off. > >Signed-off-by: David Nind <david@davidnind.com> > >Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40277: Add a test > >Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40129: Fix selenium tests > >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 38899: Fix Cypress tests > >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40164: Add Template::Plugin::JSON dependency > >Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io> >Signed-off-by: David Cook <dcook@prosentient.com.au> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40164: (follow-up) Update debian/control > >Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 39876: Move `git ls-files` to Koha::Devel::Files > >This patch introduces a new Koha::Devel namespace. It clearly shows >that its purpose is for development and will not contain any business >logic. > >Signed-off-by: David Nind <david@davidnind.com> >Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 39876: Add context for tidy > >With this patch we adjust what's needed to make misc/devel/tidy.pl run >identically as before > >Signed-off-by: David Nind <david@davidnind.com> >Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 39876: Apply module to other tests > >We are ready to reuse Koha::Devel::Files in other places where >`git ls-files` is used. > >Signed-off-by: David Nind <david@davidnind.com> >Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 39876: Get TT files for tt_tidy > >Signed-off-by: David Nind <david@davidnind.com> >Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 39876: Make find-missing-op-in-forms.t test TT files > >Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 39876: Add build-git-snapshot type resolution > >Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 39876: Make build-git-snapshot pass codespell > >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40168: Restore afterEach in KohaTable Cypress tests > >We defined an anonymous function inside an arrow function, and Cypress does not call it. > >Test plan: >Set AlwaysShowHoldingsTableFilters to "Do not" >Run t/cypress/integration/KohaTable/Holdings_spec.ts: > yarn cypress run --spec t/cypress/integration/KohaTable/Holdings_spec.ts > >When done, have a look at the value of AlwaysShowHoldingsTableFilters >=> without this patch it has been modified (not restored) and is set to >"Do" >=> With this patch applied the original value is restored at the end of >the tests (actually the end of each test) > >Signed-off-by: David Nind <david@davidnind.com> >Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40168: Remove cleanup() call from afterEach > >Signed-off-by: David Nind <david@davidnind.com> >Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40170: Replace cypress-mysql with mysql2 - Cypress tests > >This patch suggests to replace cypress-mysql with mysql2 > >cypress-mysql provides a custom Cypress command (cy.query) that run within the browser context. >This approach mixes test-side and server-side and is limited in flexibility and error handling. > >mysql2 runs inside Cypress plugins (Node process) via cy.task calls, which are executed server-side (outside the browser) > >This is needed for the follow-up bug reports where we need to request >the DB from other plugins > >Test plan: >Run `yarn install` to install the mysql2 >All the Cypress tests modified by this patch must still pass >The new test t/cypress/integration/t/db.ts must also pass > >Signed-off-by: David Nind <david@davidnind.com> >Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40316: Remove warnings from selenium/regressions.t > >Wide character in print at /usr/share/perl/5.36/Test2/Formatter/TAP.pm line 156. > >Test plan: >Confirm that > % prove t/db_dependent/selenium/regressions.t >still passes and does not generate warnings > >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40317: Remove warnings from Auth_with_shibboleth.t output > >t/db_dependent/Auth_with_shibboleth.t .. 1/5 GetPreparedLetter called at t/db_dependent/Auth_with_shibboleth.t line 73. >EnqueueLetter called at t/db_dependent/Auth_with_shibboleth.t line 80. >SendQueuedMessages called with message_id: 42 at t/db_dependent/Auth_with_shibboleth.t line 90. > >Not sure what exactly is going on here, but warnings_are is not supposed >to show the warnings in the output. I think it's because of the logger >mock. A workaround is to use a variable we increment then compare to see >if we reach the sub. > >Test plan: > % prove t/db_dependent/Auth_with_shibboleth.t >should return green without warnings > >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 39448: Layout improvement for search filter administration > >This patch makes a few minor changes to the search filter administration >template. > >To test, apply the patch and rebuild the staff interface CSS. > >- If the SavedSearchFilters system preference is not enabled, go to > Administration -> System preferences and enable it. >- Perform a catalog search. > - From the search results page, click "Save as search filter" at the > top of the results. > - Save your filter. >- Go to Administration -> Search filters. > - The table of filters should be wrapped in a "page-section" div with > the heading above it. > - In the "Actions" column the buttons should be aligned in a row with > space between each. > - Click "Edit filter" > - In the modal the form fields should be well laid out. > >Sponsored-by: Athens County Public Libraries >Signed-off-by: Felicie <felicie.thiery@biblibre.com> >Signed-off-by: Lisette Scheer <lisette@bywatersolutions.com> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40176: Add tests > >This patch adds a new subtest to xt/api.t >It ensures that maxLength will appear in the rest api spec file when >there is a size constraint at the DB level > >Signed-off-by: David Nind <david@davidnind.com> >Signed-off-by: Matt Blenkinsop <matt.blenkinsop@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40176: Add maxLength to item spec file > >Test plan: >Apply the "tests" patch first > prove xt/api.t >=> fail >Apply this patch >=> pass > >Signed-off-by: David Nind <david@davidnind.com> >Signed-off-by: Matt Blenkinsop <matt.blenkinsop@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40177: Adjust tests > >Signed-off-by: David Nind <david@davidnind.com> >Signed-off-by: Matt Blenkinsop <matt.blenkinsop@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40177: Add maxLength to library spec file > >Test plan: >Apply the "tests" patch first > prove xt/api.t >=> fail >Apply this patch >=> pass > >Signed-off-by: David Nind <david@davidnind.com> >Signed-off-by: Matt Blenkinsop <matt.blenkinsop@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40178: Adjust tests > >Signed-off-by: David Nind <david@davidnind.com> >Signed-off-by: Matt Blenkinsop <matt.blenkinsop@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40178: Add maxLength to patron spec file > >Test plan: >Apply the "tests" patch first > prove xt/api.t >=> fail >Apply this patch >=> pass > >Signed-off-by: David Nind <david@davidnind.com> >Signed-off-by: Matt Blenkinsop <matt.blenkinsop@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40179: Adjust tests > >Signed-off-by: David Nind <david@davidnind.com> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40179: Add maxLength to patron_category spec file > >Test plan: >Apply the "tests" patch first > prove xt/api.t >=> fail >Apply this patch >=> pass > >Signed-off-by: David Nind <david@davidnind.com> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* The reports list can be limited by library via the query. > >* Add branch libraries to reports edit view. > >* Revert "Bug 36586: Define self-checkin timer using Javascript class" > >This reverts commit ff403dcfb3e2972597a49b447f4d55c208f13f4d. > >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40170: Update yarn.lock > >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40337: Define checkprevcheckout as ENUM > >On bug 40178 and bug 40179 we defined checkprevcheckout attributes as >ENUM at the REST API level, so the specs prevent us from inserting >values that are no yes, no, inherit in DB. >However the DB schema didn't have that constraint. >This has been caught by some tests that were failing: >eg. t/db_dependent/selenium/patrons_search.t with "Not in enum list: yes, no, inherit." > >Test plan: >Apply this patch, run `updatedatabase`, `dbic` and `restart_all` >`prove t/db_dependent/selenium/patrons_search.t` should now pass. > >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40337: DBIC schema changes > >Signed-off-by: Eric Garcia <cubingguy714@gmail.com> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40337: DBRev 25.06.00.003 > >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40337: Handle enum in Koha::Patron->anonymize > >We need to reset to the default value. We might have a problem (later) >if it's NOT NULL and no default is set, which is not handled by this >patch. But the situation does not seem to exist yet anyway. > >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40337: Do not set checkprevcheckout to an empty string when importing patrons > >We must remove it so that it will be set as the default value defined >at the DBMS level > >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40319: Fix spacing in address display include > >This patch makes some spacing changes to the address display include in >order to prevent address parts (e.g. city, state, zip, etc) from missing >spaces in between. > >To test, apply the patch and locate or edit a patron record to include >data in all main address fields. > >- Check out to the patron and check the display of the address in the > sidebar. Confirm that the spacing is correct. >- Test with all variations of the AddressFormat system preference. >- Test with and without the use of road types (Administration -> > Authorized values -> ROADTYPE) and street numbers. > >Sponsored-by: Athens County Public Libraries >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40165: Incomplete logic for controlling display of OPAC language footer > >This patch adds some additional logic to the template which controls the >appearance of the footer in the OPAC. Some additional checks are >required to make sure an empty footer doesn't display in some >circumstances. > >To test, apply the patch and install at least one additional translation >besides English. > >Check any OPAC page when various combinations of system preferences are >enabled: > >- opaclanguagesdisplay >- OPACLanguages >- OpacLangSelectorMode >- OPACReportProblem >- CookieConsent >- OpacKohaUurl > >In particular, confirm that there is not an empty footer when >opaclanguagesdisplay is enabled but there is only one translation >enabled. > >Sponsored-by: Athens County Public Libraries >Signed-off-by: Roman Dolny <roman.dolny@jezuici.pl> > >Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40111: Fix title sorting on two reports > >This patch modifies two reports in order to correct sorting on title >columns. > >To test, apply the patch and got to reports -> Lost items. > >- Run the report with parameters that will return multiple results. > - Test sorting the title column. The articles 'a', 'an', and 'the' > should be ignored. >- Perform the same test with the "Orders by fund" report. > >Sponsored-by: Athens County Public Libraries >Signed-off-by: Anneli Ãsterman <anneli.osterman@koha-suomi.fi> > >Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40092: Fill auto-populated control fields in MARC editors > >Problem: Clicking Save doesn't fill control fields automatically in both the authority and bibliographic MARC editors. > >Regression introduced when class `framework_plugin` was added: `AreMandatoriesNotOk()` rewrites the element `class` attribute with a hardâcoded string, dropping existing classes. Because the plugins that populate 000/003/005/008 rely on `framework_plugin`, the inputs are skipped and the fields remain empty. > >This patch stops clobbering the attribute and instead appends the visual 'notFilled' class via `elt.classList.add()`. The one-line fix is applied in both `authorities.tt` and `addbiblio.tt`. > >Test plan: > >Authority editor > >1. Authorities â New authority â choose framework (e.g. Topical term). >2. Leave all fields blank, click Save. > Before patch: 000, 003, 008 remain blank; required tags turn yellow. >3. Apply patch, hardârefresh, repeat steps 1â2. > After patch: control fields autoâpopulate as in 24.11. > >Bibliographic editor > >1. Cataloguing â New record (Not in Advanced MARC editor). >2. Click Save without entering data. > Before patch: control fields stay blank; record cannot be saved. >3. Apply patch, hardârefresh, repeat. > After patch: record is blocked, but 000/005/008 autoâpopulate. > >Both editors behave like 24.11; mandatory highlighting still works. > >Signed-off-by: David Nind <david@davidnind.com> >Signed-off-by: Aleisha Amohia <aleishaamohia@hotmail.com> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 32934: Unit tests > >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 32934: Correctly honor the no block due date for SIP checkout messages > >**Problem:** >When using SIP checkout messages with the "no block" flag, the specified due date >was being ignored. Instead of honoring the provided due date, the system would: >1. Use the due date as the transaction timestamp (incorrect behavior) >2. Calculate a new due date based on circulation rules >3. Return an empty due date field (AH) in the SIP response > >This affected SIP2 integrations where external systems needed to specify exact >due dates for checkouts, particularly for offline circulation scenarios. > >**Root Cause:** >The `ProcessOfflineIssue` function in C4::Circulation was only returning a >success/failure message, but the SIP checkout transaction needed access to the >resulting checkout object to properly set the due date. The SIP Transaction::Checkout >module was calling `ProcessOfflineIssue` but couldn't retrieve the checkout details >to populate the transaction's due date field. > >**Solution:** >1. Modified `ProcessOfflineIssue` to return both the status message AND the checkout > object: `return ( "Success.", $checkout );` >2. Updated `ProcessOfflineOperation` to handle the new return format by capturing > only the message: `( $report ) = ProcessOfflineIssue( $operation );` >3. Modified SIP Transaction::Checkout to capture both return values and use the > checkout object to set the proper due date via `duedatefromissue` >4. Fixed the timestamp parameter to use `dt_from_string` instead of the due date > >**Test Plan:** >1. **Unit Tests:** > - Run `prove t/db_dependent/Circulation/OfflineCirculation.t` to verify > ProcessOfflineIssue returns checkout object and respects due_date parameter > - Run `prove t/db_dependent/SIP/Transaction.t` to verify SIP checkout honors > no_block_due_date parameter > >2. **Manual SIP Testing:** > - Connect to SIP server via telnet on port 6001 > - Send checkout message with no block flag and specific due date: > `111YN20250115 18000120241201 180001AP|AO|AC|AD|AB<barcode>|AA<cardnumber>|` > - Verify response contains correct due date in AH field > - Verify checkout record in database has the specified due date > >3. **Regression Testing:** > - Verify normal SIP checkouts (without no block) still work correctly > - Verify offline circulation operations continue to function > - Verify ProcessOfflineOperation still processes queued operations correctly > >Signed-off-by: Brendan Lawlor <blawlor@clamsnet.org> >Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com> >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40160: Use HTTPS for links to community websites > >Some web-browsers now only allow HTTPS, we should change links in >templates with SSL : >https://schema.koha-community.org, https://wiki.koha-community.org ... > >Test plan : >Just look patch to check http > https > >Signed-off-by: Owen Leonard <oleonard@myacpl.org> > >Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40088: Do not show edit button for default framework > >In Administration > MARC bibliographic frameworks > "MARC structure": >Do not show "Edit framework" button in tools bar since one can not edit >the default framework description. >Currently it leads to new framework creation. > >Test plan : >1) Go to Administration > MARC bibliographic frameworks >2) On default framework row, click Actions > MARC structure >3) Check you dont see in tool bar "Edit framework" button >4) Go to Administration > MARC bibliographic frameworks >5) On a framework row (not default one), click Actions > MARC structure >6) Check you see in tool bar "Edit framework" button > >Signed-off-by: Owen Leonard <oleonard@myacpl.org> > >Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 39572: Improve EDIFACT order confirmation message > >Add EAN account information to EDIFACT order confirmation message >when the BasketConfirmations system preference is enabled. > >To test: >1. Set EDIFACT system preference to Enable >2. Set BasketConfirmations system preference to Enable >3. Create at least 2 EANs, one with a description and one without a > description > i. Go to Acquisitions > Library EANs > ii. Click "New EAN" and fill in the fields >4. Create an EDI account > i. Go to Acquisitions > EDI accounts > ii. Click "New account" > iii. Click Submit (for testing purposes you don't need to fill in > the fields) >5. Find or create a basket for the same vendor that's on the EDI account >6. Add an order to the basket from any source >7. In the menu bar at the top of the basket screen, click "Create > EDIFACT order" and select an EAN >--> Note that the confirmation page doesn't tell you which EAN was > selected >8. Click "No, don't close" >9. Apply patch and restart_all >10. Repeat step 7 >--> The confirmation page should now display both the basket and the > selected EAN >11. Click "Cancel" >--> Confirm that the basket is still open >12. Repeat step 7 >13. Click "Yes, generate order and close basket" >--> Confirm the basket is closed >14. Click "EDIFACT messages" in the sidebar menu >--> Confirm that you see a message with type ORDERS for that basket > >Signed-off-by: David Nind <david@davidnind.com> >Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com> >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40332: Show tools sidebar category for batch extend due dates permission > >To test: >1 - have or create a user with the permissions batch_extend_due_dates edit_quotes but no other Tools >2 - log into koha as that user, go to Tools module, confirm both Batch Extend Due Dates and Quote Editor are available from the Tools module home page >3 - Click into either available tool, confirm the sidebar menu contains the category Additional Tools and a link to Quote Editor, but nothing else >4 - add the edit_notices permission to your user >5 - reload your page from step 3, confirm the sidebar menu now also contains the Patrons & Circulation category with both Notices & Slips and Batch Extend Due Dates. >6 - apply patch, restart services, reload page, confirm nothing has changed >7 - remove the edit_notices permission from your user >8 - reload your page from step 3, confirm the sidebar menu now contains the Patrons & Circulation category with only Batch Extend Due Dates. > >Signed-off-by: David Flater <flaterdavid@gmail.com> >Signed-off-by: Emily Lamancusa <emily.lamancusa@montgomerycountymd.gov> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40330: Conform and add labels to the OPAC > >Test plan: > >Enable the system preference: marcflavour: choose UNIMAR, >PrivacyPolicyConsent, ILLModule, TagsEnabled, OpacCloud. > >1- Apply the patch >2- Go to the OPAC, log in to a patron account. Go to personal details, > check the html code of the date of birth field, and notice that there > is a aria-hidden="false". >3- Inspect the search bar, and notice in the library select, there is a > label with a for attribute which match the id. >4- Search a record, click on "save to list" and check the Category > select, inspect if the label has a for attribute and match the select > id. >5- Go to interlibrary Loan Request in the user menu, Check if the "Note" > field has a label for attribute which match the id. >6- Go to Consent, check if the checkbox has a label for "Yes" and "No". >7- Go to Suggestions and check that the checkbox has the same id as the > label in the Summary column. >8- Go to "Tag Cloud", inspect the HTML code of the "Tags to show from > other users" label, notice that there is a for attribute which match > the id of the input tag. and check if in the html code there is an > aria-label with the term of the tag. >9- Go to Subject Cloud, check the html code, and notice that there is a > label that surounds the input tag. >10- Go to Advanced Search, click on "More options", and check that all > the additional options have a label with a for attribute which match > the id of the select. > >Signed-off-by: Owen Leonard <oleonard@myacpl.org> >Signed-off-by: Emily Lamancusa <emily.lamancusa@montgomerycountymd.gov> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 39545: Better generation of 773 field for a child record (MARC21) > >When creating a child record (New > New child record from Normal view) >the information put in field 773 is very basic, sometimes not >complete (e.g. only one ISBN, only 245 $a in 773 $t, 773 $w >missing 003 of the host record etc.) and put in random order. > >Test plan: >========== >1. Find any record to be a host record for an analytical record. You > can enrich the record with multiple 020, 490+830 fields, make sure that > there are 001 and 003 fields present. >2. From upper menu chose New > New child record. >3. See the generated field 773 - the subfields are incomplete and in > random order. >4. Apply the patch ; restart_all. >5. Repeat p. 2.-3. See that the generated 773 field contains now much > more information, including 773 $7 and also preset leader. > >Signed-off-by: Roman Dolny <roman.dolny@jezuici.pl> >Rebased-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >The rebase removed the majority of this patch as the prepare_host_field >method was removed entirely upstream. The follow-up patch re-implements >the same but in the newer Koha::Biblio->generate_marc_host_field method >instead. We also dropped the Unit tests that were introduced in the >subsequent patch. > >Sponsored-by: Open Fifth <https://openfifth.co.uk> >Signed-off-by: Heather Hernandez <flyingendpaper@gmail.com> >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 39545: (follow-up) Apply same logic to Koha::Biblio > >This patch updates the Koha::Biblio->generate_marc_host_field method to >more closely match the updated logic in C4::Biblio::prep_marc_host. > >We change a number of things including: > 1) Constructinging the 773 subfield generation from a hash to an array > to preserve field order. > 2) Updating main entry handling for 100/110/111 fields to clone the > field and then remove relevant subfields as apposed to fetching only > a limited set of subfields. > 3) Add handling to generate subfield 7 content depending on LDR and > Main Entry information > 4) Improve handling for title subfield construction from 245 to respect > non-filing characters and remove trailing punctuation > 5) Remove trailing punctuation from subfield b when generated from 250 > 6) Remove generation of subfield s from the 240 field entirely > 7) Remove trailing punctuation from subfield d when generated from 260 > 8) Add generation of subfield k from 800-830 fields > >Rebased-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >We dropped the upstream aforementioned C4::Biblio method, but this patch >faithfully replicates it's functionality within >Koha::Biblio->generate_marc_host_field > >Sponsored-by: Open Fifth <https://openfifth.co.uk> >Signed-off-by: Heather Hernandez <flyingendpaper@gmail.com> >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 39545: (QA follow-up) Restore 240 field processing for subfield s > >This patch restores the generation of subfield s from field 240 (Uniform Title) >in the MARC21 773 field generation, addressing cataloger feedback about the >critical importance of this field for: > >- Bibles and books of the Bible >- Laws and statutes >- Translations >- Academic and specialized library materials >- Legal materials and treaties > >The MARC21 standard defines subfield s as "Uniform title (NR)" and this data >is essential for proper bibliographic relationships. > >This patch also fixes a warning when 245 indicator 2 contains non-numeric >values by adding proper validation. > >Test plan: >1. Create a record with a 240 field containing uniform title data >2. Generate a child record from it >3. Verify the 773 field now contains subfield s with the uniform title >4. Run prove t/db_dependent/Koha/Biblio.t to ensure all tests pass > >Sponsored-by: Open Fifth <https://openfifth.co.uk> >Signed-off-by: Heather Hernandez <flyingendpaper@gmail.com> >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 39881: Add patron card number to transfers to receive table > >To test: >0. APPLY PATCH >1. Transfer an item from Library A to Library B >2. Place and item-level hold on the item >3. Set library to Library B >4. Go to Circulation > Transfers to receive >5. In the 'On hold for' column you should now see the patron card number. > >Signed-off-by: David Nind <david@davidnind.com> >Signed-off-by: Aleisha Amohia <aleishaamohia@hotmail.com> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40172: Replace jQuery with vanilla JS in js/fetch/http-client.js > >So that we can reuse it from somewhere else. >The current need is to be able to reuse it from Cypress tests. > >Test plan: >Try to hit several places where APIClient is used: > * Syspref edition > * Translation of item type description (be aware of bug 40161) > * Delete of cover images > * checkin, renew from the checkout list > * etc. > >Note that this does not affect Vue components. > >Signed-off-by: David Cook <dcook@prosentient.com.au> >Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 36947: Do a locale-based sort for ES facet names > >This change uses a configurable locale-based collator to sort >the ES facet names. > >Test plan: >0. Apply the patch >1. vi /etc/locale.gen >2. Uncomment the locale you want to generate (e.g. fi_FI.UTF-8 UTF-8) >3. locale-gen >4. vi "/etc/default/koha-common" >5. Add the following to the bottom of the file: >export LC_ALL=fi_FI.UTF-8 >6. koha-plack --restart koha-common >7. Setup some test records with authors with accented and unaccented names, > and different cases for the lead letter >e.g. Aa author, Ãa author2, aa author, étienne >8. Switch to using Elasticsearch and reindex >koha-elasticsearch -b -v --rebuild kohadev >9. Do a test search > e.g. http://localhost:8081/cgi-bin/koha/catalogue/search.pl?q=test >10. Confirm the facet names are sorted in ascending order following >Finnish collation rules > e.g. >aa author >Aa author >étienne >Farley, David >Humble, Jez >Martin, Robert C. >Ãa author > >NOTE: Any collation and language can be used. Finnish is just >an example of a Latin-based script which has a different >alphabetical ordering than just A-Z > >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: David Cook <dcook@prosentient.com.au> >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 36947: Add system preference for configurable locale-based facet sorting > >This follow-up adds two major enhancements to the locale-based facet sorting >implementation: > >1. **System Preference Integration** > - Adds FacetSortingLocale system preference for configurable locale selection > - Dynamically detects available system locales using Koha::I18N > - Provides user-friendly dropdown with locale descriptions > - Supports both Elasticsearch and Zebra search engines > >2. **Zebra Search Engine Support** > - Extends locale-based sorting to Zebra (in addition to existing Elasticsearch) > - Implements _sort_facets_zebra() function in C4::Search > - Maintains consistent Unicode-aware sorting behavior across search engines > >Key Features: >- Dynamic locale detection via Koha::I18N::available_locales() >- Proper module architecture with comprehensive unit tests >- Clean fallback chain: preference â system LC_COLLATE â default >- Admin UI integration in System Preferences â I18N/L10N >- Universal facet sorting for international library systems > >Test plan: >1. Apply patch and restart services >2. Install/run database update >3. Navigate to Administration â System Preferences â I18N/L10N >4. Verify "Sort facet names using" dropdown shows available system locales >5. Test facet sorting with different locales and both search engines >6. Run: prove t/Koha/I18N.t t/Koha/SearchEngine/Elasticsearch/Search.t t/db_dependent/Search_FacetSorting.t > >Signed-off-by: David Cook <dcook@prosentient.com.au> >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 36947: (QA follow-up) Add missing POD documentation to Koha::I18N > >The Koha::I18N module was missing POD documentation. This adds complete >documentation for all exported functions including usage examples and >descriptions. > >Signed-off-by: David Cook <dcook@prosentient.com.au> >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 36947: (QA follow-up) Fix hash key name for locale-list syspref type > >Signed-off-by: David Cook <dcook@prosentient.com.au> >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 36947: (QA follow-up) Fix locale-list to show selected values > >Signed-off-by: David Cook <dcook@prosentient.com.au> >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 36947: (QA follow-up) Add skippable unit test for fi_FI locale > >This patch adds a skippable unit test for the fi_FI locale to prove >that the patches work well for other locales that don't use >the default Unicode collation. It will run on koha-testing-docker, >but will be skipped on systems lacking it. > >This patch also fleshes out the existing unit tests a bit more >to prove that the patches also improves the sorting of names >with diacritics generally. That is, the default Unicode sort >is better than the existing stringwise/bytewise sort even for >English and French locales. > >Test plan: >0. prove -v t/Koha/SearchEngine/Elasticsearch/Search.t \ > t/db_dependent/Search_FacetSorting.t > >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 36947: (QA follow-up) Add FacetSortingLocale to Search.t mock preferences > >The test was failing because the new FacetSortingLocale system preference >added was not included in the mock preference handler in t/db_dependent/Search.t. >This caused warnings about the unknown preference when the _sort_facets_zebra >function was called. > >This commit adds the FacetSortingLocale preference to the mock, returning >'default' as the value, which matches the expected fallback behavior of >the new unicode collation sorting feature. > >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 36947: DBRev 25.06.00.004 > >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Populate Library limitations selection with libraries > >* Get selected reports and save selected reports from edit screen working. > >* Refactor getting selected lirbaries. > >* Add library limits info to duplicate section and fix limit info not showing after saving. > >* Add unit tests for reports_branches updates. > >* Bug 40370: Remove incorrect DB audit instruction and fix HTML formatting > >This change removes the incorrect instruction from about.pl: >"Run the following SQL to fix the database" and fixes the HTML >formatting so that the line "-- Convert schema '' to '':;" is >displayed correctly at the start of the line. > >Test plan: >0. Apply the patch >1. Go to http://localhost:8081/cgi-bin/koha/about.pl?tab=database >2. Note that "Run the following SQL to fix the database" no >longer appears and "--Convert schema '' to '':;" starts >at the beginning of the line instead of with leading whitespaces. > >Signed-off-by: Emily Lamancusa <emily.lamancusa@montgomerycountymd.gov> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40370: (QA follow-up) Change raw filter to html > >The diff doesn't contain intentional HTML formatting, and pre tags do >not prevent un-escaped HTML from being interpreted, so we should not >use the raw filter here > >Signed-off-by: Emily Lamancusa <emily.lamancusa@montgomerycountymd.gov> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 34157: Exporting labels as a barcode range can cause 500 error > >This fixes the error 500 generated when using the label >generator if you use the "Bibliographic data precedes >barcode" layout type when printing a barcode range. > >Test plan: >1. Set up information for testing: > 1.1 Create a new label layout: > 1.1.1 Cataloging > Tools > Label creator > 1.1.2 New > Label layout > 1.1.3 Add these details: > - Layout name: BZ34157 > - Choose layout type: Bibliographic data precedes barcode > - Select 'List fieds' > - Data fields: add some text at the end > title, author, isbn, issn, itemtype, barcode, > itemcallnumber, 'Some text' > 1.1.3 Save > 1.2 Ranges of barcode numbers for testing: > 1.2.1 Where the numbers are not used in KTD, and are LOWER than > the highest barcode number (39999000019193): > 500000 to 500100 > 1.2.3 Where the numbers are not used in KTD, and are HIGHER than > the highest barcode number (39999000019193): > 39999000020000 to 39999000020100 > 1.2.2 Where the numbers are used in KTD: > 3999900000001 to 3999900001001 >2. Print a barcode range using the ranges in 1.2: > 2.1 Cataloging > Tools > Label creator > Print barcode range > 2.2 Enter the barcode ranges > 2.3 Select a layout to be applied: BZ34157 > 2.4 Export > 2.5 Click "Download as PDF" > 2.6 Results for all barcode ranges: > ==> You get a page with "An error has occured! Error 500...". >3. Repeat step 2, but select the DEFAULT layout for 2.3: > ==> Results for all barcode ranges: PDFs with barcode labels > successfully generated >4. Apply the patches and restart everything (restart_all) >5. Repeat steps 2 and 3. >6. Results for all barcode ranges and the BZ34157 and DEFAULT layouts: > ==> PDFs with barcode labels successfully generated > >Signed-off-by: Tadeusz âtadzikâ SoÅnierz <tadeusz@sosnierz.com> >Signed-off-by: David Nind <david@davidnind.com> > >Bug 34157: (Follow up) Removed the debugging data > >Signed-off-by: David Nind <david@davidnind.com> > >Bug 34157: (follow-up) Tidiness fix > >Signed-off-by: David Nind <david@davidnind.com> > >Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl> >[EDIT] Squashed >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40128: StripWhitespaceChars can create empty subfields > >When StripWhitespaceChars is enabled and the user by chance enters >some white spaces in the input field, Koha generates empty subfield >(or even entirely empty field). > >Test plan: >========== >1. Enable StripWhitespaceChars system preference. >2. Edit an existing record, putting just some spaces into an empty > subfield. Save the record. >3. Go to bibliographic record Normal view and in modal "MARC preview" > notice that the subfield you entered spaces in exists as an empty > subfield. If it was the only subfield in a field, the field exists > with no data at all. This is wrong. >4. Apply the patch ; restart_all. >5. Repeat 2. and 3. You should not see any empty subfield now. If the > empty subbfield was the only subfield in a field, the field should > have been removed from the record. > >Signed-off-by: Roman Dolny <roman.dolny@jezuici.pl> > >Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40128: Unit tests > >Signed-off-by: Roman Dolny <roman.dolny@jezuici.pl> > >Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40169: Add a test > >Signed-off-by: David Nind <david@davidnind.com> > >Amended by: Jonathan Druart >Remove .only > >Signed-off-by: Matt Blenkinsop <matt.blenkinsop@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40169: mockData - Do not replace _id if passed > >If we build an object and pass a primary key (ie. suffixed by _id) we >should not replace it. > >Test plan: >The test highlights the problem, it won't affect existing tests. >0. Apply only the first patch (Cypress tests) >1. Run yarn cypress run t/cypress/integration/t/mockData.ts >=> It fails >2. Apply this patch >=> It passes > >Signed-off-by: David Nind <david@davidnind.com> > >Amended-by: Jonathan Druart >Remove .only > >Signed-off-by: Matt Blenkinsop <matt.blenkinsop@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40347: Remove warnings from Koha/Hold.t > >Those diag statements are not really needed > > # Filling a hold when pref enabled should trigger a test > # Filling a hold when pref disabled should not trigger a test > # Updating a hold location when pref disabled should not trigger a test > # Updating a hold location when pref enabled should trigger a test > # Update with no change to pickup location should not trigger a test > >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40348: Remove warnings from api/v1/two_factor_auth.t > >02:28:36 koha_1 | WARNING: The key derivation method "opensslv1" is deprecated. Using -pbkdf=>'pbkdf2' would be better. >02:28:36 koha_1 | Pass -nodeprecate=>1 to inhibit this message. >02:28:36 koha_1 | at /kohadevbox/koha/Koha/Patron.pm line 3216. > >We should deal with that on a separate bug report. For now we can remove >the warnings from the log/output. > >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40353: Remove warnings from Koha/Patron.t > >C4::Context->userenv not defined! at /kohadevbox/koha/Koha/Patron.pm line 2152. >C4::Context->userenv not defined! at /kohadevbox/koha/Koha/Patron.pm line 2152. >C4::Context->userenv not defined! at /kohadevbox/koha/Koha/Patron.pm line 2152. > >We need to mock the userenv > >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40351: Remove warning from oha/SearchEngine/Elasticsearch/Search.t > >Use of uninitialized value in string comparison (cmp) at /kohadevbox/koha/Koha/SearchEngine/Elasticsearch/Search.pm line 536. > >I think this is the correct fix. We generate a random av category that >is used in the data, so $label get undef: >521 $label = $authorised_values{$t}; > >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40350: Remove warnings from t/db_dependent/Holds.t > >Use of uninitialized value in numeric le (<=) at /kohadevbox/koha/C4/Circulation.pm line 3151. >Use of uninitialized value in addition (+) at /kohadevbox/koha/C4/Circulation.pm line 3158. >Use of uninitialized value in numeric le (<=) at /kohadevbox/koha/C4/Circulation.pm line 3151. >Use of uninitialized value in addition (+) at /kohadevbox/koha/C4/Circulation.pm line 3158. >Use of uninitialized value in numeric le (<=) at /kohadevbox/koha/C4/Circulation.pm line 3151. >Use of uninitialized value in addition (+) at /kohadevbox/koha/C4/Circulation.pm line 3158. > >$issue->renewal_count is undefined in C4::Circulation:3151, but it's NOT >NULL in DB >We need to refetch the row to get the default defined at the DB level > >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40402: Make xt/find-license-problems.t process files that are in the git index > >And only those files. > >Currently the test is failing with >02:52:31 koha_1 | # Failed test at xt/find-license-problems.t line 65. >02:52:31 koha_1 | # got: '0' >02:52:31 koha_1 | # expected: '1' >02:52:33 koha_1 | # File ./misc/translator/po/LICENSE has wrong copyright: hasgpl=1, hasv3=1, hasorlater=1, haslinktolicense=0, hasfranklinst=0 >02:52:33 koha_1 | # Looks like you failed 1 test of 1381. >02:52:33 koha_1 | [00:51:24] xt/find-license-problems.t > >Because a LICENSE file has been added to koha-l10n, and now this test fails on it. > >https://gitlab.com/koha-community/koha-l10n/-/commit/27ab9ff4d2462c3f95cd54ae4a2e53ce7d2f6a65 > >But we should not test files that are not part of the git index. > >Test plan: >0. Do not apply this patch >1. Edit xt/find-license-problems.t, add > warn scalar(@files); >2. Run `prove xt/find-license-problems.t` >=> Notice that 4056 files are processed >Note that you need to have a clean git repo. You can run the following >git command BUT BEWARE /*\ it will remove all changes not committed > `git clean -d -f` >3. Apply this patch, prove again >=> Tests are passing, with the same number of tests >xt/find-license-problems.t .. ok >All tests successful. >Files=1, Tests=4056 > >Note that I think we lost one file, as Test::NoWarnings is adding a >test. But I don't think it is worth the trouble to find out which one is >missing. > >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> >Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40171: Fix tt param name > >Its tran_fail, not tran_error. > >Signed-off-by: David Nind <david@davidnind.com> >Signed-off-by: Lisette Scheer <lisette@bywatersolutions.com> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40171: Remove extraneous join() > >Signed-off-by: David Nind <david@davidnind.com> >Signed-off-by: Lisette Scheer <lisette@bywatersolutions.com> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40171: Return fail if no transports for patron > >Test plan, k-t-d, don't apply patches yet: >1) Enable ILLModule >2) Create a new ill request at: ><staff_url>/cgi-bin/koha/ill/ill-requests.pl?method=create&backend=Standard >3) Pick any type and any library, pick '42' for patron. This is the 'koha' user. >4) Create the request, click 'Send notice to patron'. Pick any template. Notice nothing is shown on the UI. >5) Apply patches. Repeat. Notice you get the message: > "The requested notice was NOT queued for delivery by email, SMS". >6) Edit the 'koha' patron and tick the following boxes on 'Patron messaging preferences': > - Interlibrary loan ready > - Interlibrary loan unavailable >7) Go back to the request and click 'Send notice to patron' and pick any option again. Notice now you get a success message. > >Signed-off-by: David Nind <david@davidnind.com> >Signed-off-by: Lisette Scheer <lisette@bywatersolutions.com> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40344: Fix KohaTable_spec.ts > >> Cannot read properties of undefined (reading 'description') > >Not sure since which change this is failing but it has always been >wrong. > >The code in patron-search.inc is: > var categories = [% To.json(categories) | $raw %].map(e => { > e['_id'] = e.categorycode.toLowerCase(); > e['_str'] = e.description; > return e; > }); > var categories_map = categories.reduce((map, e) => { > map[e._id] = e; > return map; > }, {}); > >It's the keys that are lower cased. > >Test plan: > > `yarn cypress run \ > --config video=false,screenshotOnRunFailure=false \ > --spec t/cypress/integration/KohaTable/KohaTable_spec.ts` >should return green > >Signed-off-by: Matt Blenkinsop <matt.blenkinsop@openfifth.co.uk> >Signed-off-by: Lisette Scheer <lisette@bywatersolutions.com> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40116: Do not display "Patron is already a guarantor..." popup when error occurs > >If a yellow error message appears when saving the guaranteed patron, >also an extra popup message "Patron is already a guarantor for this >patron" will appear on the screen. It should only appear when you try >to add the same patron as a guarantor that already is a guarantor for >that patron. This happens because js function select_user is triggered >every time when guarantor data is send back to front-end with variable >new_guarantors if error occurs. Already existing guarantors shouldn't >be send back to front-end since their data is not lost. This patch >changes existing guarantors form elements to use classes "guarantor_id" >and "guarantor_relationship" to prevent this. > >To test: >1. Find a patron with Patron guarantor or add first a Patron guarantor > (not a Non-patron guarantor) to a juvenile patron and save. >2. Edit again the same patron with guarantor and modify their age > to be greater than the upperage or under the required age of the > patron category. > => Notice that you will get a yellow message "The following fields > are wrong. Please fix them..." > => Notice that you will also get a pop up notice that says "Patron is > already a guarantor for this patron". >3. Apply this patch. >4. Repeat step 2. > => Confirm that yellow message box is displayed but "Patron is..." > pop-up is not > => Also confirm that guarantors data is not lost. >5. Try to add same patron guarantor as guarantor for guarantee patron > again. > => Confirm that pop-up is displayed right after attempting to add > guarantor. >6. Try to add new guarantor for patron. > => New guarantor should be added. >7. Attempt to save the patron. > => Confirm that both guarantors data is still displayed in form. >8. Fix patrons age and attempt to save. > => Patron is saved and both guarantors are displayed in their > details. > >Sponsored-by: Koha-Suomi Oy >Signed-off-by: Owen Leonard <oleonard@myacpl.org> >Signed-off-by: Aleisha Amohia <aleishaamohia@hotmail.com> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40043: Try to prevent Agreements_spec.ts to fail randomly > >One test is failing with: >""" >https://on.cypress.io/element-has-detached-from-dom" type="CypressError"><![CDATA[CypressError: Timed out retrying after 10050ms: `cy.click()` failed because the page updated while this command was executing. Cypress tried to locate elements based on this query: > >> <a.show> > >We initially found matching element(s), but while waiting for them to become actionable, they disappeared from the page. Common situations why this happens: > - Your JS framework re-rendered asynchronously > - Your app code reacted to an event firing and removed the element > >You can typically solve this by breaking up a chain. For example, rewrite: > >> `cy.get('button').click().click()` > >to > >> `cy.get('button').as('btn').click()` >> `cy.get('@btn').click()` > >https://on.cypress.io/element-has-detached-from-dom > at retryActionability (http://localhost:8081/__cypress/runner/cypress_runner.js:112246:74) > at tryCatcher (http://localhost:8081/__cypress/runner/cypress_runner.js:1807:23) > at Promise.attempt.Promise.try (http://localhost:8081/__cypress/runner/cypress_runner.js:4315:29) > at whenStable (http://localhost:8081/__cypress/runner/cypress_runner.js:143192:68) > at <unknown> (http://localhost:8081/__cypress/runner/cypress_runner.js:143133:14) > at tryCatcher (http://localhost:8081/__cypress/runner/cypress_runner.js:1807:23) > at Promise._settlePromiseFromHandler (http://localhost:8081/__cypress/runner/cypress_runner.js:1519:31) > at Promise._settlePromise (http://localhost:8081/__cypress/runner/cypress_runner.js:1576:18) > at Promise._settlePromise0 (http://localhost:8081/__cypress/runner/cypress_runner.js:1621:10) > at Promise._settlePromises (http://localhost:8081/__cypress/runner/cypress_runner.js:1701:18) > at Promise._fulfill (http://localhost:8081/__cypress/runner/cypress_runner.js:1645:18) > at <unknown> (http://localhost:8081/__cypress/runner/cypress_runner.js:5450:46) >From Your Spec Code: > at Context.eval (webpack://koha/./t/cypress/integration/ERM/Agreements_spec.ts:630:18)]]></failure> > </testcase> > </testsuite> >""" > >My guess is that it happens because we don't wait for the /agreements >response. > >Test plan: >Test should still pass, we will see how Jenkins behave after it's >pushed. > >Signed-off-by: David Nind <david@davidnind.com> > >Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40389: Remove t/dummy.t > >This file should have been removed years ago. > >See the history: > >commit a5a419616ddaf81e2a9dc6a87ab92bcf2b336fde >Date: Tue Aug 28 01:50:55 2007 +0100 > > Disable tests > >@ t/dummy.t:1 @ >+# Dummy test until Test::Harness or similar >+# is used by the other tests to check deps. >+print "1..1\nok 1\n"; > >Then > >commit 7f019954460801a5fe480640cf2674dbcca9f625 >Date: Fri Dec 7 17:15:22 2007 -0600 > > installer (part 3): enabled 'make test' > >- # disable tests >- 'test' => {TESTS => 't/dummy.t'}, > >Signed-off-by: David Nind <david@davidnind.com> > >Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40403: Remove warning from Circulation_holdsqueue.t > >Use of uninitialized value in addition (+) at /kohadevbox/koha/C4/Biblio.pm line 3049. > >The context is: > >3045 my $current_issues = $biblioitem->totalissues // 0; >3046 if ( defined $value ) { >3047 $totalissues = $value; >3048 } else { >3049 $totalissues = $biblioitem->totalissues + $increase; >3050 } >3051 return 0 if $current_issues == $totalissues; # No need to update if no changes > >It feels correct to use $current_issues to suppress the warning > >Signed-off-by: David Nind <david@davidnind.com> > >Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40380: Remove warnings from Patrons/Import.t - Use of uninitialized value $csvkeycol{"dateexpiry"} in array element > >Use of uninitialized value $csvkeycol{"dateexpiry"} in array element at /kohadevbox/koha/Koha/Patrons/Import.pm line 310, <$handle_2> line 2. >Use of uninitialized value $csvkeycol{"dateexpiry"} in array element at /kohadevbox/koha/Koha/Patrons/Import.pm line 310, <$handle_3> line 2. >Use of uninitialized value $csvkeycol{"dateexpiry"} in array element at /kohadevbox/koha/Koha/Patrons/Import.pm line 310, <$handle_4> line 2. >Use of uninitialized value $csvkeycol{"dateexpiry"} in array element at /kohadevbox/koha/Koha/Patrons/Import.pm line 310, <$fh> line 2. > >Signed-off-by: David Nind <david@davidnind.com> > >Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40380: Remove warnings from Patrons/Import.t - Argument "" isn't numeric in numeric eq (==) > >Using perl -MCarp::Always: >Argument "" isn't numeric in numeric eq (==) at /usr/share/perl5/DBIx/Class/Row.pm line 1017, <$handle_1> line 2. >DBIx::Class::Row::_eq_column_values(Koha::Schema::Result::Borrower=HASH(0x57c409b9ec08), "autorenew_checkouts", "", 1) called at /usr/share/perl5/DBIx/Class/Row.pm line 945 > >Signed-off-by: David Nind <david@davidnind.com> > >Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40384: Remove warnings from Koha/Plugins/Patron.t > >t/db_dependent/Koha/Plugins/Patron.t .. 1/5 Plugin error (Test Plugin): Exception 'Koha::Exception' thrown 'patron_barcode_transform called with parameter: test_cn_1' >Plugin error (Test Plugin): Exception 'Koha::Exception' thrown 'patron_barcode_transform called with parameter: test_cn_1' >Plugin error (Test Plugin): Exception 'Koha::Exception' thrown 'patron_barcode_transform called with parameter: test_cn_1' > >Signed-off-by: David Nind <david@davidnind.com> > >Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40373: Remove warning from Reserves.t > >t/db_dependent/Reserves.t .. 10/70 No reserves HOLD_CANCELLATION letter transported by email at /kohadevbox/koha/C4/Letters.pm line 602. > >We do not test the notification here, we can simply ignore the letters code. >However we do test it later so we need to limit the scope of mock > >Signed-off-by: David Nind <david@davidnind.com> > >Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40385: Remove warning from Reserves/CancelExpiredReserves.t > >t/db_dependent/Reserves/CancelExpiredReserves.t .. 3/4 No reserves HOLD_CANCELLATION letter transported by email at /kohadevbox/koha/C4/Letters.pm line 602. > >Signed-off-by: David Nind <david@davidnind.com> > >Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40388: Remove warning from t/Labels.t > >Use of uninitialized value $cn_item in sprintf at /kohadevbox/koha/C4/ClassSplitRoutine/LCC.pm line 53. > >Signed-off-by: David Nind <david@davidnind.com> > >Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40378: Remove warnings from api/v1/biblios.t > >t/db_dependent/api/v1/biblios.t .. 12/15 DBIx::Class::Storage::DBI::_dbh_execute(): DBI Exception: DBD::mysql::st execute failed: Duplicate entry 'cZt7U7mP0KD' for key 'itembarcodeidx' at /kohadevbox/koha/Koha/Obj >ect.pm line 174 >t/db_dependent/api/v1/biblios.t .. 13/15 DBIx::Class::Storage::DBI::_dbh_execute(): DBI Exception: DBD::mysql::st execute failed: Duplicate entry 'N44vCoe4tX41' for key 'itembarcodeidx' at /kohadevbox/koha/Koha/Ob >ject.pm line 174 > >Signed-off-by: David Nind <david@davidnind.com> > >Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40390: Remove warnings from t/db_dependent/Biblio.t > >t/db_dependent/Biblio.t .. 2/25 Finding auth type GENRE/FORM at t/db_dependent/Biblio.t line 300. >Finding auth type PERSO_NAME at t/db_dependent/Biblio.t line 300. >Finding auth type PERSO_NAME at t/db_dependent/Biblio.t line 300. >Finding auth type PERSO_NAME at t/db_dependent/Biblio.t line 300. >Finding auth type PERSO_NAME at t/db_dependent/Biblio.t line 300. >t/db_dependent/Biblio.t .. 12/25 Finding auth type TOPIC_TERM at t/db_dependent/Biblio.t line 300. >Finding auth type TOPIC_TERM at t/db_dependent/Biblio.t line 300. >Finding auth type TOPIC_TERM at t/db_dependent/Biblio.t line 300. >Finding auth type PERSO_NAME at t/db_dependent/Biblio.t line 300. >Finding auth type PERSO_NAME at t/db_dependent/Biblio.t line 300. >Finding auth type PERSO_NAME at t/db_dependent/Biblio.t line 300. >t/db_dependent/Biblio.t .. ok > >Signed-off-by: David Nind <david@davidnind.com> > >Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 38102: Fix limit to 50 items in the checkout history - OPAC > >Checkout history does not limit to 50 items, it's a regression from bug >33949. > >To limit to 50 items we need to use "rows", not "limit". > >Test plan: >Have more than 50 checkouts and confirm that by default the table only >displays 50 entries. > >Signed-off-by: Owen Leonard <oleonard@myacpl.org> >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 38102: Fix default sort order > >DBIx::Class::Storage::DBI::_dbh_execute(): DBI Exception: >DBD::mysql::st execute failed: Unknown column 'date_due desc' in 'order >clause' at /kohadevbox/koha/Koha/Objects.pm line 401 > >Signed-off-by: Owen Leonard <oleonard@myacpl.org> >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40419: Changes to testing logic > >This patch makes a couple changes to the tests: > >* Remove check for Copyright line. The Copyright line should not be mandatory. >* Remove dependency of GPL-related checks on the copyrigth line > existence. (this highlights several missed checks). >* Skip testing-related templates. Koha ships some templates that are not > excluded correctly. > >This 'fix' will highlight existing errors that were skipped until now. > >To test: >1. Run: > $ ktd --shell > k$ prove xt/find-license-problems.t >=> FAIL: Tests pass! Some files should make them fail! >2. Apply this patch >3. Repeat 1 >=> SUCCESS: Tests fail! > >Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40419: Fix wrong license > >Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40304: Zebrasrv configs now work on ARM CPUs > >This patch adds more module search paths to the zebra-biblios-dom.cfg and zebra-authorities-dom.cfg, so that a larger range of CPU architectures will work with Zebra search unit tests. > >This will not change Debian package installs, but it will fix any git-based installs. > >TO TEST (you'll need access to an ARM-based CPU, and, preferably, an x86-64 machine): >ON THE ARM-BASED MACHINE: >a) start ktd, then ktd --shell >b) in ktd, run: prove -v t/db_dependent/Search.t >c) notice how the test now fails >APPLY PATCH >d) rerun step b >e) notice how the test now passes >ON THE X86-64-BASED MACHINE: >f) run steps a-e >g) notice how the test passes, both before and after applying the patch >SIGN OFF > >Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de> >Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 38966: Fix wrong POD in Koha/CoverImages.pm and Koha/Acquisition/Order/Claims.pm > >Test plan: >1. perldoc Koha/Acquisition/Order/Claims.pm >2. Check that it mentions Koha::Acquisition::Order::Claims >3. perldoc Koha/CoverImages.pm >4. Check that it mentions Koha::CoverImages.pm >4. These files no longer mention 'Koha::Cities' > >Signed-off-by: David Nind <david@davidnind.com> > >Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40421: Make HTML elements data attributes > >Test Plan: >1. Apply patch >2. In Staff Interface inspect element and notice HTML has data attributes > >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> >Signed-off-by: Emily Lamancusa <emily.lamancusa@montgomerycountymd.gov> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 39997: List of closed serials: reopening shouldn't require the syspref RoutingSerials > >Test plan: >1 - Create a closed serial >2 - Disable the syspref "RoutingSerials" >3 - Go to cgi-bin/koha/serials/serials-search.pl and search for the > closed serial ; click on the "closed" panel of the result -> on the > actions button, there is no option to reopen >4 - Apply patch >5 - Repeat 3, there is now an option to reopen. > >Signed-off-by: Owen Leonard <oleonard@myacpl.org> > >Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40438: Remove warnings from Koha/Old/Hold.t > >it's an integer. > >t/db_dependent/Koha/Old/Hold.t .. 1/3 >Value not allowed for auto_incr biblionumber in Biblio at >/kohadevbox/koha/t/lib/TestBuilder.pm line 431. > >Signed-off-by: Owen Leonard <oleonard@myacpl.org> >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40437: Remove warning from Koha/Installer.t > >t/db_dependent/Koha/Installer.t .. 1/4 >Use of uninitialized value $db_version in numeric eq (==) at >/kohadevbox/koha/Koha/Installer.pm line 32. > >Signed-off-by: Owen Leonard <oleonard@myacpl.org> >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40429: Remove warnings from Koha/Patron/Modifications.t > >t/db_dependent/Koha/Patron/Modifications.t .. 1/7 DBIx::Class::Storage::DBI::insert(): Missing value for primary key column 'borrowernumber' on BorrowerModification - perhaps you forgot to set its 'is_auto_increme >nt' attribute during add_columns()? Treating 'borrowernumber' implicitly as an autoinc, and attempting value retrieval at /kohadevbox/koha/Koha/Object.pm line 174 >t/db_dependent/Koha/Patron/Modifications.t .. 5/7 DBIx::Class::Storage::DBI::insert(): Missing value for primary key column 'verification_token' on BorrowerModification - perhaps you forgot to set its 'is_auto_inc >rement' attribute during add_columns()? Treating 'verification_token' implicitly as an autoinc, and attempting value retrieval at /kohadevbox/koha/Koha/Object.pm line 174 > >Signed-off-by: David Nind <david@davidnind.com> >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40411: Remove warnings from Koha/SearchEngine/Elasticsearch.t > >Warnings encountered while roundtripping a MARC record to/from USMARC. Failing over to MARCXML. at t/db_dependent/Koha/SearchEngine/Elasticsearch.t line 854. > >=> This one is expected: we have generated a record with large fields > >item-level_itypes set but no itemtype set for item (1568) at /kohadevbox/koha/Koha/Schema/Result/Item.pm line 1100. >item-level_itypes set but no itemtype set for item (1568) at /kohadevbox/koha/Koha/Schema/Result/Item.pm line 1100. > >=> Those 2 ones can be removed by create a biblio and items using >build_sample_biblio and build_sample_item > >Signed-off-by: David Nind <david@davidnind.com> >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40410: Remove warning from Letters. > >t/db_dependent/Letters.t .. 95/104 Use of uninitialized value in string eq at /kohadevbox/koha/Koha/Patron.pm line 389. > >contactname can be NULL in DB. > >Signed-off-by: David Nind <david@davidnind.com> >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40409: Make Overdues.t fail > >The test is wrong, the item is not correctly checked in, it should fail. > >Signed-off-by: David Nind <david@davidnind.com> >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40409: Remove warnings from t/db_dependent/Overdues.t > >The tests was wrong, we should make sure the item has been checked in. > >We were missing the mock userenv: >Can't use an undefined value as a HASH reference at /kohadevbox/koha/C4/Circulation.pm line 2744. at /kohadevbox/koha/C4/Circulation.pm line 2787 > >Signed-off-by: David Nind <david@davidnind.com> >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40406: Remove warnings from selenium/basic_workflow.t > >t/db_dependent/selenium/basic_workflow.t .. 1/22 CP main = 1.93 >t/db_dependent/selenium/basic_workflow.t .. 3/22 CP add patron category = 3.56 >t/db_dependent/selenium/basic_workflow.t .. 5/22 CP add patron = 4.31 >CP add biblio = 0.29 >t/db_dependent/selenium/basic_workflow.t .. 11/22 CP add items = 9.81 >t/db_dependent/selenium/basic_workflow.t .. 16/22 CP checkout = 5.97 >t/db_dependent/selenium/basic_workflow.t .. 20/22 CP checkin = 4.04 >t/db_dependent/selenium/basic_workflow.t .. 22/22 CP holds = 5.60 >t/db_dependent/selenium/basic_workflow.t .. ok > >This has been used in the past for benchmarking Koha. > >I suggest to keep the warn but comment it. > >Signed-off-by: David Nind <david@davidnind.com> >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40404: Remove diag from t/Test/Mock/Logger.t > >t/Test/Mock/Logger.t .. 1/10 # trace: > # (No trace messages) > # debug: > # "Debug message" at /usr/share/perl/5.36/Test/Builder.pm line 374 > # info: > # (No info messages) > # warn: > # (No warn messages) > # error: > # (No error messages) > # fatal: > # (No fatal messages) >t/Test/Mock/Logger.t .. ok >All tests successful. >Files=1, Tests=10, 0 wallclock secs ( 0.03 usr 0.00 sys + 0.40 cusr 0.07 csys = 0.50 CPU) >Result: PASS > >This one is a bit tricky: we cannot use Test::Warn to capture the output because Test::Builder->diag does not write directly to STDERR. >It has its own failure_output handle (which defaults to STDERR) but bypasses Perlâs standard layers. > >Signed-off-by: David Nind <david@davidnind.com> >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40381: Remove warning from Koha/SearchEngine/Elasticsearch/ExportConfig.t > >DBIx::Class::Storage::DBI::insert(): Missing value for primary key column 'filter' on SearchMarcToField - perhaps you forgot to set its 'is_auto_increment' attribute during add_columns()? Treating 'filter' implicitly as an autoinc, and attempting value retrieval at /kohadevbox/koha/Koha/SearchField.pm line 42 >Signed-off-by: David Nind <david@davidnind.com> >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40377: Remove warnings from HoldsQueue/TransportCostOptimizations.t > >UseTransportCostMatrix set to yes, but matrix not populated at /kohadevbox/koha/C4/HoldsQueue.pm line 188. >UseTransportCostMatrix set to yes, but matrix not populated at /kohadevbox/koha/C4/HoldsQueue.pm line 188. > >It seems that they are expected, we simply need to catch them. > >Signed-off-by: David Nind <david@davidnind.com> >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40376: Remove warning from AuthorisedValues.t > >t/db_dependent/AuthorisedValues.t .. 1/17 DBIx::Class::Storage::DBI::insert(): Missing value for primary key column 'tagsubfield' on MarcSubfieldStructure - perhaps you forgot to set its 'is_auto_increment' attrib >ute during add_columns()? Treating 'tagsubfield' implicitly as an autoinc, and attempting value retrieval at /kohadevbox/koha/Koha/Object.pm line 174 > >It is a bit scary but it's only that we forgot to pass a value for >tagsubfield (that is NOT NULL and part of the PK) > >Signed-off-by: David Nind <david@davidnind.com> >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40374: Remove warnings from Koha/Booking.t > >t/db_dependent/Koha/Booking.t .. 1/2 DBIx::Class::Storage::DBI::_dbh_execute(): DBI Exception: DBD::mysql::st execute failed: Duplicate entry '7' for key 'PRIMARY' at /kohadevbox/koha/Koha/Object.pm line 174 >DBIx::Class::Storage::DBI::_dbh_execute(): DBI Exception: DBD::mysql::st execute failed: Data truncated for column 'status' at row 1 at /kohadevbox/koha/Koha/Object.pm line 174 > >Signed-off-by: David Nind <david@davidnind.com> >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40372: Remove warning from api/v1/holds.t > >t/db_dependent/api/v1/holds.t .. 1/16 No reserves HOLD_CANCELLATION letter transported by email at /kohadevbox/koha/C4/Letters.pm line 602. > >We do not test the notification here, we can simply ignore the letters >code > >Signed-off-by: David Nind <david@davidnind.com> >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Pass the current branch as the filter branchcode > >* Fix library limit method issue by defining use statements > >* Filtering the list based on the library limits works > >* Bug 40173: Make http-client reusable from Cypress tests > >We will need to call REST API endpoints from Cypress tests, and more >specifically from Cypress's tasks. > >It will be useful to reuse what has been done in http-client.js instead >of using the low-level fetch JS function. > >1. Add missing get and getAll functions >2. Allow overwrite of return_response and mark_submitting >3. Add a "default" client that will bring flexibility (can call any > routes) >4. Add rspack config to generate > t/cypress/plugins/dist/api-client.cjs.js that will make it reusable > from Cypress's tasks > >Signed-off-by: Owen Leonard <oleonard@myacpl.org> >Signed-off-by: Matt Blenkinsop <matt.blenkinsop@openfifth.co.uk> >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40173: Return the response early if the caller needs it > >We returned the response too late: if something was wrong we processed >the response already and thrown an error. > >This may introduce side-effects but it feels like a correct change. > >Signed-off-by: Owen Leonard <oleonard@myacpl.org> >Signed-off-by: Matt Blenkinsop <matt.blenkinsop@openfifth.co.uk> >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40173: Add test for the api-client Cypress plugin > >Signed-off-by: Owen Leonard <oleonard@myacpl.org> >Signed-off-by: Matt Blenkinsop <matt.blenkinsop@openfifth.co.uk> >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40173: Remove Cypress warnings > >Cypress Warning: Cypress detected that you returned a promise in a >test, but also invoked one or more cy commands inside of that promise. > >Signed-off-by: Owen Leonard <oleonard@myacpl.org> >Signed-off-by: Matt Blenkinsop <matt.blenkinsop@openfifth.co.uk> >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40174: Allow proper E2E testing with Cypress > >This is the first step toward implementing true end-to-end testing with >Cypress. > >Until now, we have been mocking responses using cy.intercept(), but this >approach can lead to confusion. In some cases, we have even had to mock >global JavaScript variables, which makes the code unnecessarily complex >and unconventional (e.g. win.categories_map). > >All the bug reports listed under this tree have helped lay the >groundwork for this patch. With it, we are able to build mock objects >(via plugin mockData) and insert them directly into the database >(insertData). Once the tests are complete, weâll restore the database to >its previous state by removing any data generated during the tests. > >The tests in KohaTable/Holdings_spec.ts have been updated to leverage >this new setup. > >Additionally, a caching mechanism has been added to prevent _id >attributes from being generated with the same values (a rare issue we >occasionally encountered). > >While this is still not a perfect solution, it introduces a solid >foundation for future improvements. > >Test plan: >All Cypress tests must pass > >Signed-off-by: Matt Blenkinsop <matt.blenkinsop@openfifth.co.uk> >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40174: Introduce Cypress auth plugin > >So we can easily retrieve the value for the Authorization header. > >Signed-off-by: Matt Blenkinsop <matt.blenkinsop@openfifth.co.uk> >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40174: Use api-client > >Signed-off-by: Matt Blenkinsop <matt.blenkinsop@openfifth.co.uk> >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40174: Deal with enum > >Data too long for column 'checkprevcheckout' > >Signed-off-by: Matt Blenkinsop <matt.blenkinsop@openfifth.co.uk> >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40174: Set minLength to 3 > >When we generate several objects we often get "Duplicate ID" from the >server. This mostly happens when the tests failed previously and the >objects have not been removed from the DB. > >Signed-off-by: Matt Blenkinsop <matt.blenkinsop@openfifth.co.uk> >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40174: Require RESTBasicAuth turned ON > >From MM thread https://chat.koha-community.org/koha-community/pl/aptoodssm7gu8x491onfgxs1xr > >Cypress tests need RESTBasicAuth - Iâm stuck in a chicken-and-egg problem being eaten by its own tail... >I need advice. Some Cypress tests need RESTBasicAuth, but it's disabled by default. >My plan was to turn it on globally (ie. set before any tests, and reset to its original value after) >but we cannot right now: >Update in DB could work, but its cached >Call the svc script won't work, as we are not logged in yet (and this script require the session cookie) > >so, I have a lot of solutions, but not sure which one to pick >* have a "flush cache" route (smells the rabbit hole) >* login to get the cookie, set the syspref via the svc script, logout (ugly, and will slower the cypress tests) >* require the pref on for cypress tests >* ? > >I would opt now for the third option, turn it on in Jenkins when we run the cypress tests. And explode for devs when the pref is not set. >What do you think? > >If we push this as it, Jenkins is going to fail. >We need to restructure how Jenkins is running the Cypress tests anyway: >we need a dedicated job to run them all. We will turn ON the syspref for >this job. > >Signed-off-by: Matt Blenkinsop <matt.blenkinsop@openfifth.co.uk> >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40174: Do not generate bigint > >Prevent "Out of range value for column 'deliverytime'" in a later >follow-up bug reports. > >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40180: (bug 40118 follow-up) Add Cypress tests > >This patch heavily reuses what has been introduced by the previous >patches in the tree, and add a insertSampleHold task to be reusable >easily in other tests. > >Test plan: >All Cypress tests must pass > >Signed-off-by: Owen Leonard <oleonard@myacpl.org> >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40180: Add tests for insertSampleHold > >Signed-off-by: Owen Leonard <oleonard@myacpl.org> >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40180: Improve deleteSampleObjects > >We want the task to accept an Object or an Array of Objects > >Signed-off-by: Owen Leonard <oleonard@myacpl.org> >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40346: Introduce loginOpac and visitOpac - Cypress > >To be able to test the OPAC interface we need to add those 2 new >commands > >Signed-off-by: Owen Leonard <oleonard@myacpl.org> >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40346: Add tests > >Signed-off-by: Owen Leonard <oleonard@myacpl.org> >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40346: Use KOHA_INTRANET_URL for the baseUrl > >This will prevent the test to reload when the URL changes > >See https://github.com/cypress-io/cypress/issues/2777 > >Signed-off-by: Owen Leonard <oleonard@myacpl.org> >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40181: Make sure data won't be added by insertData > >We are here testing that data inserted by insertData tasks can be >easily removed from the DB using deleteSampleObjects > >Test plan: >Cypress test t/cypress/integration/t/insertData.ts must pass > >Signed-off-by: Owen Leonard <oleonard@myacpl.org> >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40301: Add insertSampleCheckout > >Signed-off-by: Owen Leonard <oleonard@myacpl.org> >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40301: (bug 40299 follow-up) Add Cypress tests > >Signed-off-by: Owen Leonard <oleonard@myacpl.org> >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40345: Pass a patron to insertSampleCheckout > >So we won't generate a new patron per checkout > >Signed-off-by: Owen Leonard <oleonard@myacpl.org> >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40345: Add insertSamplePatron > >So it's easier to build and insert a new patron > >Signed-off-by: Owen Leonard <oleonard@myacpl.org> >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40345: Force checkout > >Pass the confirmation token to force the checkout > >Signed-off-by: Owen Leonard <oleonard@myacpl.org> >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40345: Add tests for bug 38102 > >Test plan: > yarn cypress run --spec t/cypress/integration/KohaTable/OPACCirculationHistory_spec.ts >must return green > >Revert commits from bug 38102 >Run again the Cypress tests >=> They fail > >Signed-off-by: Owen Leonard <oleonard@myacpl.org> >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40345: Properly cleanup old checkouts > >Signed-off-by: Owen Leonard <oleonard@myacpl.org> >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40345: Use then to prevent async issues > >Signed-off-by: Owen Leonard <oleonard@myacpl.org> >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40433: Add missing maxLength to item, library and patron > >The test added in xt/api.t was not fully accurate, we skipped the >columns that have the same name as the api attribute (ie. the ones not >part of to_api_mapping) > >Test plan: >prove xt/api.t should pass > >Signed-off-by: Owen Leonard <oleonard@myacpl.org> >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40430: Fix Toolbar_spec.ts > >See comment 1 on the bug. > >We are adding the capability to insert vendors and baskets. > >Test plan: >Confirm that > yarn cypress run t/cypress/integration/Toolbar_spec.ts >pass, even if you do not have any vendors in DB. > >Signed-off-by: Owen Leonard <oleonard@myacpl.org> >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40318: (bug 40067 follow-up) Prevent receive shipments opening in a new tab - vendor list view > >1. Acquisitions > Search vendors > Search >2. In the actions column, click "Receive shipments" >=> Without this patch the receive shipment from vendor [name] page opens in a new tab/window >=> With this patch applied it does not > >Signed-off-by: David Nind <david@davidnind.com> >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40318: Add tests > >Signed-off-by: David Nind <david@davidnind.com> >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40447: Add comprehensive JSDoc documentation to Cypress plugins > >This patch adds detailed JSDoc documentation to all Cypress testing >plugin files. > >Documentation added to: >- insertData.js: Data insertion utilities with examples >- api-client.js: API wrapper functions with usage patterns >- mockData.js: Schema-based mock data generation >- auth.js: Authentication helper functions >- db.js: Database query utilities >- readYamlFile.js: YAML file reading utilities >- index.js: Main plugin configuration > >Each function now includes: >- Complete parameter descriptions with types >- Return value documentation >- Practical usage examples >- Error conditions and handling >- Module-level overviews > >This addresses the documentation gap identified during QA review, >hopefully making it easier for future devs to adopt the new framework of >tools. > >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40379: Remove warnings from t/db_dependent/www > >XMLin complains because there are several nodes with the same name >attribute. Not sure what is correct or not, but we actually cannot use >the admin/DBMS user to login to the Koha interface. > >We use KOHA_USER env var or default to 'koha'. > >Signed-off-by: David Nind <david@davidnind.com> > >Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40401: Implement Koha::Patron->is_anonymous > >The first motivation of this patch is to remove the following warnings >from t/db_dependent/Auth.t: >Use of uninitialized value $anonymous_patron in string eq at /kohadevbox/koha/C4/Auth.pm line 1497 > >Actually a correct fix seems to add a new `is_anonymous` method to >Koha::Patron in order to remove the warnings from everywhere else. > >Test plan: >Confirm that you cannot log in using the anonymous patron and that the 2 >tests are passing > prove t/db_dependent/Auth.t t/db_dependent/Koha/Patron.t > >Signed-off-by: David Nind <david@davidnind.com> >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40401: (follow-up) Fix grammar and rollback in tests > >- Fixed grammar: "Returns true if the patron is the anonymous patron" >- Fixed test cleanup: use txn_rollback instead of txn_begin >- Fixed test subroutine closure syntax > >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40407: Remove legacy "pre-wrap" versions > >From https://css-tricks.com/snippets/css/make-pre-text-wrap/ > >/* Browser specific (not valid) styles to make preformatted text wrap */ > >pre { > white-space: pre-wrap; /* css-3 */ > white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ > white-space: -pre-wrap; /* Opera 4-6 */ > white-space: -o-pre-wrap; /* Opera 7 */ > word-wrap: break-word; /* Internet Explorer 5.5+ */ >} > >... I think it's now safe to remove the backward compatibility versions and simply keep pre-wrap. > >Test plan (from bug 921)0: > * place a hold on a title with a very very long title or author > * mark it as "waiting for pickup" by doing a checkin > * go to command line interface, and run > * misc/cronjobs/gather_print_notices.pl /tmp > * misc/cronjobs/printoverdues.sh /tmp > * if your title/author is long enough, it's splitted on 2 lines > with this patch (it is not before this patch) > >It will also remove the warnings from t/db_dependent/Patron/Borrower_Discharge.t >WARNING: Ignored `white-space: -moz-pre-wrap` at 6:14, invalid value. >WARNING: Ignored `white-space: -o-pre-wrap` at 7:14, invalid value. >WARNING: Ignored `word-wrap: break-work` at 8:14, invalid value. > >Signed-off-by: David Nind <david@davidnind.com> >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 18772: Remove warnings from t/ImportBatch.t > >Use of uninitialized value in concatenation (.) or string at /usr/local/share/perl/5.20.2/MARC/File/XML.pm line 399, <__ANONIO__> chunk 1. > >Those are 2 warnings coming from MARC::File::XML when called with >invalid XML. >We should not use Test::Warn as they are not expected and that may be >fixed upstream. > >Signed-off-by: David Nind <david@davidnind.com> >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 32296: Capitalization - UNIMARC labels for 181$b value builder > >This patch changes the capitalisation for the labels displayed in >the MARC editor for UNIMARC 181$b, when the value builder >unimarc_field_181b.pl is used. The labels are changed from capital >case to sentence case, to be consistent with other labels: >- Specification of type (position 0) >- Specification of motion (position 1) >- Specification of dimensionality (position 2) >- Sensory specification 1, 2 and 3 (positions 3 to 5) > >Test plan: >1. Check the capitalisation in the patch. > >Signed-off-by: David Nind <david@davidnind.com> >Signed-off-by: Emily Lamancusa <emily.lamancusa@montgomerycountymd.gov> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 32284: Fix capitalization for UNIMARC value builders > >This patch changes the capitalization for the labels displayed >in the MARC tag editor for UNIMARC subfields 181$a, 181$c, >$182$a, 182$c, and 183$c, when the value builders are used. The >labels are changed from capital case to sentence case, for >consistency with other labels: >- Content Form -> Content form (181$a) >- Content Type -> Content type (181$c) >- Media Type Code -> Media type code (182$a) >- Media Type -> Media type (182$c) >- In the dropdown list (183$a): > . Audio Carriers -> Audio carriers > . Computer Carriers -> Computer carriers > . Microform Carriers -> Microform carriers > . Microscopic Carriers -> Microscopic carriers > . Stereographic Carriers -> Stereographic carriers > >Test plan: >1. In your KTD shell navigate to >koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/value_builder >2. Run "git grep Audio Carriers" you should get results >3. Run "git grep Computer Carriers" you should get results >4. Run "git grep Content Form" you should get results >5. Run "git grep Content Type" you should get results >6. Run "git grep Media Type" you should get results >7. Run "git grep Media Type Code" you should get results >8. Run "git grep Microform Carriers" you should get results >9. Run "git grep Microscopic Carriers" you should get results >10. Apply the patch >11. Repeat steps 2-9 you should not get results > >Signed-off-by: David Nind <david@davidnind.com> >Signed-off-by: Emily Lamancusa <emily.lamancusa@montgomerycountymd.gov> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 32287: Capitalization - UNIMARC labels for 214$r and $s > >This patch changes the capitalisation for the labels displayed in >the OPAC and staff interface for UNIMARC 214$r and $s from capital >case to sentence case, to be consistent with other labels: >- 214$r: Printing and/or publishing information transcribed > as found in the main source of information >- 214$s: Printing and/or publishing information transcribed > as found in the colophon > >Test plan: >1. Have a look at the patch and confirm the changes look correct. > >Signed-off-by: David Nind <david@davidnind.com> >Signed-off-by: Emily Lamancusa <emily.lamancusa@montgomerycountymd.gov> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40458: add pod coverage to Discharge.pm > >Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl> >[EDIT] Replaced borrowers in the ->request POD by borrower >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> > >* Bug 40174: Fix codespell > >Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org> > >* Remove comments. > >--------- > >Signed-off-by: Caroline Cyr La Rose <caroline.cyr-la-rose@inlibro.com> >Signed-off-by: Baptiste Wojtkowski <baptiste.wojtkowski@biblibre.com> >Signed-off-by: Lucas Gass <lucas@bywatersolutions.com> >Signed-off-by: Roman Dolny <roman.dolny@jezuici.pl> >Signed-off-by: Emily Lamancusa <emily.lamancusa@montgomerycountymd.gov> >Signed-off-by: David Nind <david@davidnind.com> >Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl> >Signed-off-by: Paul Derscheid <paul.derscheid@lmscloud.de> >Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Signed-off-by: Michelle Spinney <mspinney@clamsnet.org> >Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io> >Signed-off-by: Kristi Krueger <kkrueger@cuyahogalibrary.org> >Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org> >Signed-off-by: Lisette Scheer <lisette@bywatersolutions.com> >Signed-off-by: Anneli Ãsterman <anneli.osterman@koha-suomi.fi> >Signed-off-by: David Cook <dcook@prosentient.com.au> >Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com> >Signed-off-by: Aleisha Amohia <aleishaamohia@hotmail.com> >Signed-off-by: Emmanuel Bétemps <e.betemps@gmail.com> >Signed-off-by: Andrew Auld <andrew.auld@openfifth.co.uk> >Signed-off-by: Laura_Escamilla <laura.escamilla@bywatersolutions.com> >Signed-off-by: Ray Delahunty <lib-systems@arts.ac.uk> >Signed-off-by: David Flater <flaterdavid@gmail.com> >Signed-off-by: Heather Hernandez <flyingendpaper@gmail.com> >Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk> >Signed-off-by: Felicie <felicie.thiery@biblibre.com> >Signed-off-by: Matt Blenkinsop <matt.blenkinsop@openfifth.co.uk> >Signed-off-by: Eric Garcia <cubingguy714@gmail.com> >Signed-off-by: Brendan Lawlor <blawlor@clamsnet.org> >Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com> >Signed-off-by: Owen Leonard <oleonard@myacpl.org> >Co-authored-by: Pedro Amorim <pedro.amorim@ptfs-europe.com> >Co-authored-by: Baptiste Wojtkowski <baptiste.wojtkowski@biblibre.com> >Co-authored-by: Lucas Gass <lucas@bywatersolutions.com> >Co-authored-by: Emily Lamancusa <emily.lamancusa@montgomerycountymd.gov> >Co-authored-by: Owen Leonard <oleonard@myacpl.org> >Co-authored-by: David Nind <david@davidnind.com> >Co-authored-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl> >Co-authored-by: Nick Clemens <nick@bywatersolutions.com> >Co-authored-by: Martin Renvoize <martin.renvoize@openfifth.co.uk> >Co-authored-by: Paul Derscheid <paul.derscheid@lmscloud.de> >Co-authored-by: Tomas Cohen Arazi <tomascohen@theke.io> >Co-authored-by: Matt Blenkinsop <matt.blenkinsop@openfifth.co.uk> >Co-authored-by: PhilipOrr <philip.orr@lmscloud.de> >Co-authored-by: Mason James <mtj@kohaaloha.com> >Co-authored-by: Caroline Cyr La Rose <caroline.cyr-la-rose@inlibro.com> >Co-authored-by: Emmi Takkinen <emmi.takkinen@koha-suomi.fi> >Co-authored-by: Aleisha Amohia <aleisha@catalyst.net.nz> >Co-authored-by: Laura_Escamilla <laura.escamilla@bywatersolutions.com> >Co-authored-by: Lisette Scheer <lisette@bywatersolutions.com> >Co-authored-by: Cameron E. Tidd <ctidd@citlink.net> >Co-authored-by: Martin Renvoize <martin.renvoize@ptfs-europe.com> >Co-authored-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org> >Co-authored-by: Fridolin Somers <fridolin.somers@biblibre.com> >Co-authored-by: John Doe <you@example.com> >Co-authored-by: Andrii Nugged <nugged@gmail.com> >Co-authored-by: Lari Strand <lari.strand@koha-suomi.fi> >Co-authored-by: Andrew Fuerste Henry <andrew@bywatersolutions.com> >Co-authored-by: nina martinez <nina.martinez@biblibre.com> >Co-authored-by: Janusz Kaczmarek <januszop@gmail.com> >Co-authored-by: David Cook <dcook@prosentient.com.au> >Co-authored-by: Matthias Le Gac <matthias.le-gac@inlibro.com> >Co-authored-by: Pedro Amorim <pedro.amorim@openfifth.co.uk> >Co-authored-by: Jake Deery <jake.deery@openfifth.co.uk> >Co-authored-by: Doris Tam <doristam@catalyst.net.nz> >Co-authored-by: Eric Garcia <cubingguy714@gmail.com> >Co-authored-by: Toni Gardiner <tonig@catalyst.net.nz> >Co-authored-by: Cath Leone <catherineleone@catalyst.net.nz> >Co-authored-by: Brian Norris <briann@catalyst.net.nz> >--- > C4/Auth.pm | 6 +- > C4/Biblio.pm | 138 +--- > C4/Circulation.pm | 10 +- > C4/ClassSplitRoutine/LCC.pm | 2 +- > C4/Form/MessagingPreferences.pm | 45 ++ > C4/Items.pm | 12 +- > C4/Koha.pm | 2 +- > C4/Labels/Label.pm | 8 +- > C4/SIP/ILS/Transaction/Checkout.pm | 6 +- > C4/Scrubber.pm | 4 +- > C4/Search.pm | 42 +- > Koha.pm | 2 +- > Koha/Acquisition/Order/Claims.pm | 2 +- > Koha/BackgroundJob/BatchUpdateItem.pm | 2 +- > Koha/Biblio.pm | 213 +++--- > Koha/CookieManager.pm | 130 +++- > Koha/Course.pm | 20 +- > Koha/Course/Instructor.pm | 20 +- > Koha/Course/Instructors.pm | 20 +- > Koha/Course/Item.pm | 20 +- > Koha/Course/Items.pm | 20 +- > Koha/Course/Reserve.pm | 20 +- > Koha/Course/Reserves.pm | 20 +- > Koha/Courses.pm | 20 +- > Koha/CoverImages.pm | 2 +- > Koha/Devel/Files.pm | 192 +++++ > Koha/Encryption.pm | 5 +- > Koha/Exceptions/MarcOverlayRule.pm | 20 +- > Koha/Filter/MARC/TrimFields.pm | 10 +- > Koha/Hold.pm | 3 + > Koha/I18N.pm | 262 +++++++ > Koha/ILL/Backend/Standard.pm | 17 +- > Koha/ILL/Request.pm | 2 + > Koha/Import/OAI/Authorities.pm | 1 + > Koha/Import/OAI/Authority.pm | 1 + > Koha/Import/OAI/Biblio.pm | 1 + > Koha/Import/OAI/Biblios.pm | 1 + > Koha/Installer.pm | 2 +- > Koha/Item.pm | 18 + > Koha/Items.pm | 9 +- > Koha/Manual.pm | 1 + > Koha/Patron.pm | 184 ++++- > Koha/Patron/Discharge.pm | 92 +++ > Koha/Patrons/Import.pm | 8 +- > Koha/Quotes.pm | 2 +- > Koha/REST/V1/CirculationRules.pm | 20 +- > Koha/REST/V1/Holds.pm | 31 +- > Koha/REST/V1/Suggestions.pm | 17 +- > Koha/Report.pm | 20 +- > Koha/Reports.pm | 29 +- > Koha/Schema/Result/Borrower.pm | 12 +- > Koha/Schema/Result/Category.pm | 12 +- > Koha/Schema/Result/Deletedborrower.pm | 12 +- > Koha/Schema/Result/ReportsBranch.pm | 85 +++ > Koha/Schema/Result/SavedSql.pm | 220 ++++++ > Koha/SearchEngine/Elasticsearch/Search.pm | 49 +- > acqui/basket.pl | 10 +- > acqui/newordersuggestion.pl | 26 +- > admin/columns_settings.yml | 7 +- > admin/preferences.pl | 12 + > api/v1/swagger/definitions/item.yaml | 14 + > api/v1/swagger/definitions/library.yaml | 4 + > api/v1/swagger/definitions/patron.yaml | 15 + > api/v1/swagger/definitions/patron_category.yaml | 7 + > api/v1/swagger/paths/holds.yaml | 18 + > catalogue/moredetail.pl | 9 +- > catalogue/updateitem.pl | 10 +- > cataloguing/addbiblio.pl | 5 +- > cataloguing/additem.pl | 7 +- > circ/circulation.pl | 2 +- > cpanfile | 1 + > cypress.config.ts | 11 +- > debian/build-git-snapshot | 102 ++- > debian/control | 3 + > debian/templates/koha-conf-site.xml.in | 8 +- > debian/templates/zebra-authorities-dom-site.cfg.in | 2 +- > debian/templates/zebra-biblios-dom-site.cfg.in | 2 +- > docs/teams.yaml | 171 +++-- > etc/koha-conf.xml | 8 +- > etc/zebradb/zebra-authorities-dom.cfg | 2 +- > etc/zebradb/zebra-biblios-dom.cfg | 2 +- > ill/ill-requests.pl | 4 +- > installer/data/mysql/db_revs/250600001.pl | 20 + > installer/data/mysql/db_revs/250600002.pl | 40 + > installer/data/mysql/db_revs/250600003.pl | 32 + > installer/data/mysql/db_revs/250600004.pl | 22 + > installer/data/mysql/en/optional/sample_news.yml | 12 +- > installer/data/mysql/kohastructure.sql | 19 +- > installer/data/mysql/mandatory/sysprefs.sql | 5 +- > installer/data/mysql/updatedatabase.pl | 16 + > koha-tmpl/intranet-tmpl/lib/jsdiff/jsdiff.js | 2 +- > koha-tmpl/intranet-tmpl/lib/jsdiff/jsdiff.min.js | 2 +- > .../intranet-tmpl/prog/css/src/staff-global.scss | 10 + > .../intranet-tmpl/prog/en/includes/about-team.inc | 71 +- > .../batch_item_record_modification.inc | 7 + > .../en/includes/catalogue/itemsearch_item.csv.inc | 4 +- > .../en/includes/catalogue/itemsearch_item.json.inc | 4 +- > .../includes/csv_headers/catalogue/itemsearch.tt | 2 +- > .../prog/en/includes/guided-reports-view.inc | 6 +- > .../intranet-tmpl/prog/en/includes/header.inc | 18 +- > .../prog/en/includes/html-customization-help.inc | 2 + > .../en/includes/member-display-address-style.inc | 50 +- > .../prog/en/includes/patron_messages.inc | 46 +- > .../intranet-tmpl/prog/en/includes/prefs-menu.inc | 11 + > .../intranet-tmpl/prog/en/includes/tools-menu.inc | 2 +- > koha-tmpl/intranet-tmpl/prog/en/modules/about.tt | 11 +- > .../intranet-tmpl/prog/en/modules/acqui/basket.tt | 25 +- > .../prog/en/modules/acqui/newordersuggestion.tt | 15 +- > .../prog/en/modules/admin/categories.tt | 8 +- > .../prog/en/modules/admin/localization.tt | 5 +- > .../prog/en/modules/admin/marctagstructure.tt | 4 +- > .../en/modules/admin/preferences/acquisitions.pref | 2 +- > .../en/modules/admin/preferences/circulation.pref | 9 +- > .../en/modules/admin/preferences/i18n_l10n.pref | 5 + > .../prog/en/modules/admin/preferences/opac.pref | 10 +- > .../prog/en/modules/admin/preferences/patrons.pref | 10 +- > .../prog/en/modules/admin/preferences/reports.pref | 9 + > .../modules/admin/preferences/staff_interface.pref | 6 - > .../prog/en/modules/admin/search_filters.tt | 20 +- > koha-tmpl/intranet-tmpl/prog/en/modules/auth.tt | 2 +- > .../prog/en/modules/authorities/authorities.tt | 2 +- > .../intranet-tmpl/prog/en/modules/basket/basket.tt | 2 +- > .../prog/en/modules/batch/print-notices.tt | 3 - > .../prog/en/modules/catalogue/detail.tt | 85 +-- > .../prog/en/modules/catalogue/itemsearch.tt | 2 + > .../prog/en/modules/catalogue/moredetail.tt | 10 + > .../prog/en/modules/cataloguing/addbiblio.tt | 2 +- > .../prog/en/modules/cataloguing/addbooks.tt | 6 +- > .../prog/en/modules/cataloguing/additem.tt | 2 + > .../value_builder/unimarc_field_181a.tt | 2 +- > .../value_builder/unimarc_field_181b.tt | 12 +- > .../value_builder/unimarc_field_181c.tt | 2 +- > .../value_builder/unimarc_field_182a.tt | 2 +- > .../value_builder/unimarc_field_182c.tt | 2 +- > .../value_builder/unimarc_field_183a.tt | 10 +- > .../prog/en/modules/circ/article-requests.tt | 29 +- > .../prog/en/modules/circ/branchoverdues.tt | 2 +- > .../prog/en/modules/circ/pendingreserves.tt | 2 +- > .../prog/en/modules/circ/transferstoreceive.tt | 3 + > .../prog/en/modules/circ/waitingreserves.tt | 34 +- > .../prog/en/modules/clubs/patron-enroll.tt | 2 +- > .../prog/en/modules/members/holdshistory.tt | 12 +- > .../prog/en/modules/members/member-flags.tt | 2 +- > .../prog/en/modules/members/memberentrygen.tt | 4 +- > .../prog/en/modules/members/moremember.tt | 2 +- > .../en/modules/reports/guided_reports_start.tt | 47 +- > .../prog/en/modules/reports/issues_stats.tt | 72 +- > .../prog/en/modules/reports/itemslost.tt | 2 +- > .../prog/en/modules/reports/orders_by_budget.tt | 2 +- > .../prog/en/modules/reports/reports-home.tt | 29 +- > .../prog/en/modules/serials/serials-search.tt | 24 +- > .../prog/en/modules/tools/additional-contents.tt | 2 +- > .../en/modules/tools/batch_extend_due_dates.tt | 1 + > .../prog/en/modules/tools/stage-marc-import.tt | 2 +- > .../prog/en/xslt/UNIMARCslimUtils.xsl | 4 +- > koha-tmpl/intranet-tmpl/prog/js/datatables.js | 55 +- > .../intranet-tmpl/prog/js/fetch/api-client.js | 2 + > .../prog/js/fetch/default-api-client.js | 19 + > .../intranet-tmpl/prog/js/fetch/http-client.js | 94 ++- > .../prog/js/vue/components/ERM/AgreementsList.vue | 2 +- > .../prog/js/vue/components/ERM/LicensesList.vue | 6 +- > .../prog/js/vue/components/Vendors/VendorList.vue | 2 +- > .../intranet-tmpl/prog/js/vue/fetch/http-client.js | 48 +- > .../opac-tmpl/bootstrap/css/src/_responsive.scss | 5 + > koha-tmpl/opac-tmpl/bootstrap/css/src/opac.scss | 51 +- > .../opac-tmpl/bootstrap/en/includes/calendar.inc | 2 +- > .../opac-tmpl/bootstrap/en/includes/masthead.inc | 1 + > .../bootstrap/en/includes/opac-bottom.inc | 4 +- > .../bootstrap/en/includes/patron-restrictions.inc | 27 + > .../bootstrap/en/includes/subtypes_unimarc.inc | 46 +- > .../opac-tmpl/bootstrap/en/includes/usermenu.inc | 9 +- > .../opac-tmpl/bootstrap/en/modules/clubs/enroll.tt | 2 +- > .../bootstrap/en/modules/opac-addbybiblionumber.tt | 2 +- > .../opac-tmpl/bootstrap/en/modules/opac-browse.tt | 2 +- > .../opac-tmpl/bootstrap/en/modules/opac-browser.tt | 2 +- > .../bootstrap/en/modules/opac-illrequests.tt | 2 +- > .../bootstrap/en/modules/opac-patron-consent.tt | 12 +- > .../opac-tmpl/bootstrap/en/modules/opac-reserve.tt | 48 +- > .../opac-tmpl/bootstrap/en/modules/opac-results.tt | 12 +- > .../bootstrap/en/modules/opac-suggestions.tt | 2 +- > .../opac-tmpl/bootstrap/en/modules/opac-tags.tt | 6 +- > .../bootstrap/en/modules/opac-tags_subject.tt | 2 +- > .../opac-tmpl/bootstrap/en/modules/opac-user.tt | 36 +- > .../bootstrap/en/modules/opac-virtual-card.tt | 27 +- > .../opac-tmpl/bootstrap/en/modules/sci/sci-main.tt | 43 +- > .../opac-tmpl/bootstrap/en/modules/sco/sco-main.tt | 29 +- > .../bootstrap/en/xslt/UNIMARCslimUtils.xsl | 4 +- > koha-tmpl/opac-tmpl/bootstrap/js/browse.js | 4 +- > koha-tmpl/opac-tmpl/bootstrap/js/timeout.js | 39 - > members/holdshistory.pl | 7 - > members/memberentry.pl | 13 +- > members/moremember.pl | 4 - > members/readingrec.pl | 5 +- > misc/cronjobs/overdue_notices.pl | 3 - > misc/devel/Koha/Schema/Result/ReportsBranch.pm | 85 +++ > misc/devel/tidy.pl | 67 +- > .../maintenance/search_for_data_inconsistencies.pl | 2 +- > misc/sip_cli_emulator.pl | 34 +- > opac/opac-readingrecord.pl | 10 +- > opac/opac-reserve.pl | 78 +- > opac/opac-user.pl | 1 + > opac/sci/sci-main.pl | 4 + > opac/sco/sco-main.pl | 7 + > package.json | 2 +- > reports/guided_reports.pl | 105 ++- > reports/orders_by_fund.pl | 26 +- > rspack.config.js | 34 + > t/CookieManager.t | 97 ++- > t/Form_MessagingPreferences.t | 78 +- > t/ImportBatch.t | 24 +- > t/Koha/I18N.t | 48 +- > t/Koha/SearchEngine/Elasticsearch/Search.t | 127 +++- > t/Labels.t | 6 +- > t/RecordProcessor.t | 10 +- > t/Scrubber.t | 218 ++++-- > t/Test/Mock/Logger.t | 20 +- > t/cypress/fixtures/{sample.mrc => sample.xml} | 0 > t/cypress/integration/Acquisitions/Vendors_spec.ts | 66 ++ > t/cypress/integration/Auth/csrf.ts | 66 +- > t/cypress/integration/ERM/Agreements_spec.ts | 14 +- > t/cypress/integration/ERM/DataProviders_spec.ts | 2 +- > t/cypress/integration/ERM/UsageReports_spec.ts | 4 +- > .../integration/Islands/AcquisitionsMenu_spec.ts | 10 +- > .../KohaTable/CirculationHistory_spec.ts | 23 + > t/cypress/integration/KohaTable/Holdings_spec.ts | 576 ++++++-------- > t/cypress/integration/KohaTable/KohaTable_spec.ts | 4 +- > .../KohaTable/OPACCirculationHistory_spec.ts | 53 ++ > .../integration/KohaTable/PatronSearch_spec.ts | 53 +- > .../integration/KohaTable/PendingHolds_spec.ts | 101 +++ > t/cypress/integration/Toolbar_spec.ts | 81 +- > .../integration/Tools/ManageMarcImport_spec.ts | 60 +- > t/cypress/integration/t/api-client.ts | 28 + > t/cypress/integration/t/commands.ts | 27 + > t/cypress/integration/t/db.ts | 15 + > t/cypress/integration/t/insertData.ts | 278 +++++++ > t/cypress/integration/t/mockData.ts | 20 + > t/cypress/plugins/api-client.js | 177 +++++ > t/cypress/plugins/auth.js | 40 + > t/cypress/plugins/db.js | 66 ++ > t/cypress/plugins/index.js | 109 ++- > t/cypress/plugins/insertData.js | 825 +++++++++++++++++++++ > t/cypress/plugins/mockData.js | 215 +++++- > t/cypress/plugins/readYamlFile.js | 31 + > t/cypress/support/e2e.js | 34 +- > t/db_dependent/Auth.t | 3 +- > t/db_dependent/Auth_with_shibboleth.t | 31 +- > t/db_dependent/AuthorisedValues.t | 4 +- > t/db_dependent/Biblio.t | 18 +- > t/db_dependent/Circulation/OfflineCirculation.t | 62 +- > t/db_dependent/Circulation_holdsqueue.t | 3 +- > t/db_dependent/Holds.t | 5 +- > .../HoldsQueue/TransportCostOptimizations.t | 43 +- > .../Items/AutomaticItemModificationByAge.t | 106 ++- > t/db_dependent/Koha.t | 6 +- > t/db_dependent/Koha/Biblio.t | 209 ++++-- > t/db_dependent/Koha/Booking.t | 29 +- > t/db_dependent/Koha/Hold.t | 49 +- > t/db_dependent/Koha/Installer.t | 3 +- > t/db_dependent/Koha/Item.t | 54 +- > t/db_dependent/Koha/Items/BatchUpdate.t | 67 +- > t/db_dependent/Koha/Old/Hold.t | 5 +- > t/db_dependent/Koha/Patron.t | 254 ++++++- > t/db_dependent/Koha/Patron/Modifications.t | 38 +- > t/db_dependent/Koha/Patrons.t | 17 +- > t/db_dependent/Koha/Patrons/Import.t | 3 +- > t/db_dependent/Koha/Plugins/Patron.t | 8 +- > t/db_dependent/Koha/Reports.t | 38 +- > t/db_dependent/Koha/SearchEngine/Elasticsearch.t | 32 +- > .../Koha/SearchEngine/Elasticsearch/ExportConfig.t | 12 +- > .../Koha/SearchEngine/Elasticsearch/Search.t | 3 +- > t/db_dependent/Letters.t | 3 +- > t/db_dependent/Overdues.t | 10 +- > t/db_dependent/Reserves.t | 12 +- > t/db_dependent/Reserves/CancelExpiredReserves.t | 12 +- > t/db_dependent/SIP/Transaction.t | 21 +- > t/db_dependent/Search.t | 2 + > t/db_dependent/Search_FacetSorting.t | 119 +++ > t/db_dependent/api/v1/biblios.t | 33 +- > t/db_dependent/api/v1/holds.t | 258 ++++++- > t/db_dependent/api/v1/two_factor_auth.t | 3 +- > t/db_dependent/selenium/basic_workflow.t | 7 +- > t/db_dependent/selenium/opac_ill_requests.t | 9 +- > t/db_dependent/selenium/regressions.t | 11 +- > t/db_dependent/www/auth_values_input_www.t | 11 +- > t/db_dependent/www/batch.t | 11 +- > t/db_dependent/www/history.t | 11 +- > t/db_dependent/www/search_utf8.t | 8 +- > t/dummy.t | 4 - > t/lib/TestBuilder.pm | 7 +- > tools/batchMod.pl | 21 +- > xt/api.t | 51 +- > xt/author/codespell.t | 13 +- > xt/author/pod_checker.t | 7 +- > xt/author/podcorrectness.t | 8 +- > xt/find-license-problems.t | 47 +- > xt/find-missing-csrf.t | 11 +- > xt/find-missing-filters.t | 12 +- > xt/find-missing-op-in-forms.t | 12 +- > xt/perltidy.t | 8 +- > xt/pl_valid.t | 17 +- > xt/single_quotes.t | 11 +- > xt/tt_tidy.t | 6 +- > yarn.lock | 46 +- > 303 files changed, 7863 insertions(+), 2352 deletions(-) > create mode 100644 Koha/Devel/Files.pm > create mode 100644 Koha/Schema/Result/ReportsBranch.pm > create mode 100755 installer/data/mysql/db_revs/250600001.pl > create mode 100755 installer/data/mysql/db_revs/250600002.pl > create mode 100755 installer/data/mysql/db_revs/250600003.pl > create mode 100755 installer/data/mysql/db_revs/250600004.pl > create mode 100644 koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/reports.pref > create mode 100644 koha-tmpl/intranet-tmpl/prog/js/fetch/default-api-client.js > create mode 100644 koha-tmpl/opac-tmpl/bootstrap/en/includes/patron-restrictions.inc > delete mode 100644 koha-tmpl/opac-tmpl/bootstrap/js/timeout.js > create mode 100644 misc/devel/Koha/Schema/Result/ReportsBranch.pm > rename t/cypress/fixtures/{sample.mrc => sample.xml} (100%) > create mode 100644 t/cypress/integration/KohaTable/CirculationHistory_spec.ts > create mode 100644 t/cypress/integration/KohaTable/OPACCirculationHistory_spec.ts > create mode 100644 t/cypress/integration/KohaTable/PendingHolds_spec.ts > create mode 100644 t/cypress/integration/t/api-client.ts > create mode 100644 t/cypress/integration/t/commands.ts > create mode 100644 t/cypress/integration/t/db.ts > create mode 100644 t/cypress/integration/t/insertData.ts > create mode 100644 t/cypress/plugins/api-client.js > create mode 100644 t/cypress/plugins/auth.js > create mode 100644 t/cypress/plugins/db.js > create mode 100644 t/cypress/plugins/insertData.js > create mode 100755 t/db_dependent/Search_FacetSorting.t > delete mode 100755 t/dummy.t > >diff --git a/C4/Auth.pm b/C4/Auth.pm >index 2fa9e2bed7..affc7ee43a 100644 >--- a/C4/Auth.pm >+++ b/C4/Auth.pm >@@ -1492,10 +1492,6 @@ sub checkauth { > my $auth_template_name = ( $type eq 'opac' ) ? 'opac-auth.tt' : 'auth.tt'; > my $template = C4::Templates::gettemplate( $auth_template_name, $type, $query ); > >- my $borrowernumber = $patron and $patron->borrowernumber; >- my $anonymous_patron = C4::Context->preference('AnonymousPatron'); >- my $is_anonymous_patron = $patron && ( $patron->borrowernumber eq $anonymous_patron ); >- > $template->param( > login => 1, > INPUTS => \@inputs, >@@ -1530,7 +1526,7 @@ sub checkauth { > opac_css_override => $ENV{'OPAC_CSS_OVERRIDE'}, > too_many_login_attempts => ( $patron and $patron->account_locked ), > password_has_expired => ( $patron and $patron->password_expired ), >- is_anonymous_patron => ($is_anonymous_patron), >+ is_anonymous_patron => ( $patron and $patron->is_anonymous ), > password_expiration_date => ( $patron and $patron->password_expiration_date ), > date_enrolled => ( $patron and $patron->dateenrolled ), > auth_error => $auth_error, >diff --git a/C4/Biblio.pm b/C4/Biblio.pm >index d847fe8f61..00f481cf96 100644 >--- a/C4/Biblio.pm >+++ b/C4/Biblio.pm >@@ -62,7 +62,6 @@ BEGIN { > TransformMarcToKoha > TransformHtmlToMarc > TransformHtmlToXml >- prepare_host_field > ); > > # Internal functions >@@ -2998,141 +2997,6 @@ sub ModBiblioMarc { > return $biblionumber; > } > >-=head2 prepare_host_field >- >-$marcfield = prepare_host_field( $hostbiblioitem, $marcflavour ); >-Generate the host item entry for an analytic child entry >- >-=cut >- >-sub prepare_host_field { >- my ( $hostbiblio, $marcflavour ) = @_; >- $marcflavour ||= C4::Context->preference('marcflavour'); >- >- my $biblio = Koha::Biblios->find($hostbiblio); >- my $host = $biblio->metadata->record; >- >- # unfortunately as_string does not 'do the right thing' >- # if field returns undef >- my %sfd; >- my $field; >- my $host_field; >- if ( $marcflavour eq 'MARC21' ) { >- if ( $field = $host->field('100') || $host->field('110') || $host->field('11') ) { >- my $s = $field->as_string('ab'); >- if ($s) { >- $sfd{a} = $s; >- } >- } >- if ( $field = $host->field('245') ) { >- my $s = $field->as_string('a'); >- if ($s) { >- $sfd{t} = $s; >- } >- } >- if ( $field = $host->field('260') ) { >- my $s = $field->as_string('abc'); >- if ($s) { >- $sfd{d} = $s; >- } >- } >- if ( $field = $host->field('240') ) { >- my $s = $field->as_string(); >- if ($s) { >- $sfd{b} = $s; >- } >- } >- if ( $field = $host->field('022') ) { >- my $s = $field->as_string('a'); >- if ($s) { >- $sfd{x} = $s; >- } >- } >- if ( $field = $host->field('020') ) { >- my $s = $field->as_string('a'); >- if ($s) { >- $sfd{z} = $s; >- } >- } >- if ( $field = $host->field('001') ) { >- $sfd{w} = $field->data(),; >- } >- $host_field = MARC::Field->new( 773, '0', ' ', %sfd ); >- return $host_field; >- } elsif ( $marcflavour eq 'UNIMARC' ) { >- >- #author >- if ( $field = $host->field('700') || $host->field('710') || $host->field('720') ) { >- my $s = $field->as_string('ab'); >- if ($s) { >- $sfd{a} = $s; >- } >- } >- >- #title >- if ( $field = $host->field('200') ) { >- my $s = $field->as_string('a'); >- if ($s) { >- $sfd{t} = $s; >- } >- } >- >- #place of publicaton >- if ( $field = $host->field('210') ) { >- my $s = $field->as_string('a'); >- if ($s) { >- $sfd{c} = $s; >- } >- } >- >- #date of publication >- if ( $field = $host->field('210') ) { >- my $s = $field->as_string('d'); >- if ($s) { >- $sfd{d} = $s; >- } >- } >- >- #edition statement >- if ( $field = $host->field('205') ) { >- my $s = $field->as_string(); >- if ($s) { >- $sfd{e} = $s; >- } >- } >- >- #URL >- if ( $field = $host->field('856') ) { >- my $s = $field->as_string('u'); >- if ($s) { >- $sfd{u} = $s; >- } >- } >- >- #ISSN >- if ( $field = $host->field('011') ) { >- my $s = $field->as_string('a'); >- if ($s) { >- $sfd{x} = $s; >- } >- } >- >- #ISBN >- if ( $field = $host->field('010') ) { >- my $s = $field->as_string('a'); >- if ($s) { >- $sfd{y} = $s; >- } >- } >- if ( $field = $host->field('001') ) { >- $sfd{0} = $field->data(),; >- } >- $host_field = MARC::Field->new( 461, '0', ' ', %sfd ); >- return $host_field; >- } >- return; >-} >- > =head2 UpdateTotalIssues > > UpdateTotalIssues($biblionumber, $increase, [$value]) >@@ -3182,7 +3046,7 @@ sub UpdateTotalIssues { > if ( defined $value ) { > $totalissues = $value; > } else { >- $totalissues = $biblioitem->totalissues + $increase; >+ $totalissues = $current_issues + $increase; > } > return 0 if $current_issues == $totalissues; # No need to update if no changes > >diff --git a/C4/Circulation.pm b/C4/Circulation.pm >index bf93c32a59..f9cf48a605 100644 >--- a/C4/Circulation.pm >+++ b/C4/Circulation.pm >@@ -4442,7 +4442,7 @@ sub ProcessOfflineOperation { > if ( $operation->{action} eq 'return' ) { > $report = ProcessOfflineReturn($operation); > } elsif ( $operation->{action} eq 'issue' ) { >- $report = ProcessOfflineIssue($operation); >+ ($report) = ProcessOfflineIssue($operation); > } elsif ( $operation->{action} eq 'payment' ) { > $report = ProcessOfflinePayment($operation); > } >@@ -4513,15 +4513,15 @@ sub ProcessOfflineIssue { > $operation->{timestamp}, > ); > } >- AddIssue( >+ my $checkout = AddIssue( > $patron, >- $operation->{'barcode'}, >- undef, >+ $operation->{barcode}, >+ $operation->{due_date}, > undef, > $operation->{timestamp}, > undef, > ); >- return "Success."; >+ return ( "Success.", $checkout ); > } else { > return "Borrower not found."; > } >diff --git a/C4/ClassSplitRoutine/LCC.pm b/C4/ClassSplitRoutine/LCC.pm >index 12901ba3e3..a2ee632960 100644 >--- a/C4/ClassSplitRoutine/LCC.pm >+++ b/C4/ClassSplitRoutine/LCC.pm >@@ -46,7 +46,7 @@ sub split_callnumber { > # lccn examples: 'HE8700.7 .P6T44 1983', 'BS2545.E8 H39 1996'; > my @lines = Library::CallNumber::LC->new($cn_item)->components(); > unless ( scalar @lines && defined $lines[0] ) { >- Koha::Logger->get->debug( sprintf( 'regexp failed to match string: %s', $cn_item ) ); >+ Koha::Logger->get->debug( sprintf( 'regexp failed to match string: %s', $cn_item // q{} ) ); > @lines = $cn_item; # if no match, just use the whole string. > } > my $LastPiece = pop @lines; >diff --git a/C4/Form/MessagingPreferences.pm b/C4/Form/MessagingPreferences.pm >index 88c8e08fe1..36b0979099 100644 >--- a/C4/Form/MessagingPreferences.pm >+++ b/C4/Form/MessagingPreferences.pm >@@ -148,6 +148,51 @@ PREF: foreach my $option (@$messaging_options) { > $template->param( messaging_preferences => $messaging_options ); > } > >+=head2 restore_form_values >+ >+ C4::Form::MessagingPreferences::restore_form_values( $input, $template ); >+ >+Restores patron message preferences if error occurs while creating a patron. >+ >+C<$input> is the CGI query object. >+ >+C<$template> is the Template::Toolkit object for the response. >+ >+=cut >+ >+sub restore_form_values { >+ my ( $input, $template ) = @_; >+ my $messaging_options = C4::Members::Messaging::GetMessagingOptions(); >+ foreach my $option (@$messaging_options) { >+ $option->{ $option->{'message_name'} } = 1; >+ >+ my $message_attribute_id = $option->{'message_attribute_id'}; >+ if ( $option->{'takes_days'} ) { >+ my $selected_value = $input->param( $message_attribute_id . '-DAYS' ); >+ my $days_in_advance = $selected_value ? $selected_value : 0; >+ $option->{days_in_advance} = $days_in_advance; >+ @{ $option->{'select_days'} } = map { >+ { >+ day => $_, >+ selected => $_ == $days_in_advance >+ } >+ } ( 0 .. MAX_DAYS_IN_ADVANCE ); >+ } >+ >+ my @transport_types = $input->multi_param($message_attribute_id); >+ foreach my $transport_type (@transport_types) { >+ $option->{ 'transports_' . $transport_type } = 1; >+ } >+ >+ if ( $option->{'has_digest'} ) { >+ if ( List::Util::first { $_ == $message_attribute_id } $input->multi_param('digest') ) { >+ $option->{'digest'} = 1; >+ } >+ } >+ } >+ $template->param( messaging_preferences => $messaging_options ); >+} >+ > =head1 TODO > > =over 4 >diff --git a/C4/Items.pm b/C4/Items.pm >index 7f9f3fccdc..2c22518dc5 100644 >--- a/C4/Items.pm >+++ b/C4/Items.pm >@@ -1732,7 +1732,17 @@ sub ToggleNewStatus { > $item->$field($value); > push @{ $report->{$itemnumber} }, $substitution; > } >- $item->store unless $report_only; >+ unless ($report_only) { >+ try { >+ $item->store; >+ } catch { >+ push @{ $report->{$itemnumber} }, { >+ field => 'ERROR', >+ error => 1, >+ value => $_->error, >+ } >+ } >+ } > } > } > >diff --git a/C4/Koha.pm b/C4/Koha.pm >index 13e7a5da7d..bee096db9f 100644 >--- a/C4/Koha.pm >+++ b/C4/Koha.pm >@@ -513,7 +513,7 @@ sub GetAuthorisedValues { > my $opac = shift ? 1 : 0; # normalise to be safe > > # Is this cached already? >- my $branch_limit = C4::Context->userenv ? C4::Context->userenv->{"branch"} : ""; >+ my $branch_limit = C4::Context::mybranch(); > my $cache_key = "AuthorisedValues-$category-$opac-$branch_limit"; > my $cache = Koha::Caches->get_instance(); > my $result = $cache->get_from_cache($cache_key); >diff --git a/C4/Labels/Label.pm b/C4/Labels/Label.pm >index 842cfb051a..dd289083ee 100644 >--- a/C4/Labels/Label.pm >+++ b/C4/Labels/Label.pm >@@ -385,8 +385,12 @@ sub draw_label_text { > my $font = $self->{'font'}; > my $item = _get_label_item( $self->{'item_number'} ); > my $label_fields = _get_text_fields( $self->{'format_string'} ); >- my $biblio = Koha::Biblios->find( $item->{biblionumber} ); >- my $record = $biblio->metadata->record; >+ my $biblio = Koha::Biblios->find( $item->{'biblionumber'} ); >+ my $record; >+ >+ if ( defined $biblio ) { >+ $record = $biblio->metadata->record; >+ } > > # FIXME - returns all items, so you can't get data from an embedded holdings field. > # TODO - add a GetMarcBiblio1item(bibnum,itemnum) or a GetMarcItem(itemnum). >diff --git a/C4/SIP/ILS/Transaction/Checkout.pm b/C4/SIP/ILS/Transaction/Checkout.pm >index 247ba86ca3..4909400493 100644 >--- a/C4/SIP/ILS/Transaction/Checkout.pm >+++ b/C4/SIP/ILS/Transaction/Checkout.pm >@@ -156,13 +156,15 @@ sub do_checkout { > > if ($no_block_due_date) { > $overridden_duedate = $no_block_due_date; >- ProcessOfflineIssue( >+ my ( $msg, $checkout ) = ProcessOfflineIssue( > { > cardnumber => $patron->cardnumber, > barcode => $barcode, >- timestamp => $no_block_due_date, >+ due_date => $no_block_due_date, >+ timestamp => dt_from_string, > } > ); >+ $self->{due} = $self->duedatefromissue( $checkout, $itemnumber ); > } else { > > # can issue >diff --git a/C4/Scrubber.pm b/C4/Scrubber.pm >index f5201418e7..40c0bee3dc 100644 >--- a/C4/Scrubber.pm >+++ b/C4/Scrubber.pm >@@ -29,12 +29,13 @@ use C4::Context; > my %scrubbertypes = ( > default => {}, # place holder, default settings are below as fallbacks in call to constructor > comment => { allow => [qw( br b i em big small strong )], }, >- note => { allow => [qw[ br b i em big small strong u hr span div p ]] }, >+ note => { allow => [qw[ br b i em big small strong u hr span div p ol ul li dl dt dd ]] }, > ); > > sub new { > shift; # ignore our class we are wrapper > my $type = (@_) ? shift : 'default'; >+ $type = 'default' if !defined $type; > if ( !exists $scrubbertypes{$type} ) { > croak "New called with unrecognized type '$type'"; > } >@@ -44,7 +45,6 @@ sub new { > rules => exists $settings->{rules} ? $settings->{rules} : [], > default => exists $settings->{default} ? $settings->{default} : [ 0 => { '*' => 0 } ], > comment => exists $settings->{comment} ? $settings->{comment} : 0, >- note => exists $settings->{note} ? $settings->{note} : 0, > process => 0, > ); > return $scrubber; >diff --git a/C4/Search.pm b/C4/Search.pm >index 08903e7fc5..bf0a485286 100644 >--- a/C4/Search.pm >+++ b/C4/Search.pm >@@ -40,6 +40,8 @@ use URI::Escape; > use Business::ISBN; > use MARC::Record; > use MARC::Field; >+use POSIX qw(setlocale LC_COLLATE); >+use Unicode::Collate::Locale; > > our ( @ISA, @EXPORT_OK ); > >@@ -291,6 +293,40 @@ See verbose embedded documentation. > > =cut > >+=head2 _sort_facets_zebra >+ >+ my $sorted_facets = _sort_facets_zebra($facets, $locale); >+ >+Sorts facets using a configurable locale for Zebra search engine. >+ >+=cut >+ >+sub _sort_facets_zebra { >+ my ( $facets, $locale ) = @_; >+ >+ if ( !$locale ) { >+ >+ # Get locale from system preference, falling back to system LC_COLLATE >+ $locale = C4::Context->preference('FacetSortingLocale') || 'default'; >+ if ( $locale eq 'default' || !$locale ) { >+ >+ #NOTE: When setlocale is run with only the 1st parameter, it is a "get" not a "set" function. >+ $locale = setlocale(LC_COLLATE) || 'default'; >+ } >+ } >+ >+ my $collator = Unicode::Collate::Locale->new( locale => $locale ); >+ if ( $collator && $facets ) { >+ my @sorted_facets = sort { $collator->cmp( $a->{facet_label_value}, $b->{facet_label_value} ) } @{$facets}; >+ if (@sorted_facets) { >+ return \@sorted_facets; >+ } >+ } >+ >+ #NOTE: If there was a problem, at least return the not sorted facets >+ return $facets; >+} >+ > sub getRecords { > my ( > $koha_query, $simple_query, $sort_by_ref, $servers_ref, >@@ -554,8 +590,10 @@ sub getRecords { > if (@facets_loop) { > foreach my $f (@facets_loop) { > if ( C4::Context->preference('FacetOrder') eq 'Alphabetical' ) { >- $f->{facets} = >- [ sort { uc( $a->{facet_label_value} ) cmp uc( $b->{facet_label_value} ) } @{ $f->{facets} } ]; >+ my $sorted_facets = _sort_facets_zebra( $f->{facets} ); >+ if ($sorted_facets) { >+ $f->{facets} = $sorted_facets; >+ } > } > } > } >diff --git a/Koha.pm b/Koha.pm >index 926325f941..2ba56e0ae1 100644 >--- a/Koha.pm >+++ b/Koha.pm >@@ -29,7 +29,7 @@ use vars qw{ $VERSION }; > # - #4 : the developer version. The 4th number is the database subversion. > # used by developers when the database changes. updatedatabase take care of the changes itself > # and is automatically called by Auth.pm when needed. >-$VERSION = "25.06.00.000"; >+$VERSION = "25.06.00.005"; > > sub version { > return $VERSION; >diff --git a/Koha/Acquisition/Order/Claims.pm b/Koha/Acquisition/Order/Claims.pm >index eef991990e..7cee67ff75 100644 >--- a/Koha/Acquisition/Order/Claims.pm >+++ b/Koha/Acquisition/Order/Claims.pm >@@ -25,7 +25,7 @@ use base qw(Koha::Objects); > > =head1 NAME > >-Koha::Cities - Koha Claim Object set class >+Koha::Acquisition::Order::Claims - Koha Claims Object set class > > =head1 API > >diff --git a/Koha/BackgroundJob/BatchUpdateItem.pm b/Koha/BackgroundJob/BatchUpdateItem.pm >index 1158efd568..75250d6877 100644 >--- a/Koha/BackgroundJob/BatchUpdateItem.pm >+++ b/Koha/BackgroundJob/BatchUpdateItem.pm >@@ -113,6 +113,7 @@ sub process { > callback => sub { $self->step; }, > } > ); >+ $report->{errors} = $results->{errors}; > $report->{modified_itemnumbers} = $results->{modified_itemnumbers}; > $report->{modified_fields} = $results->{modified_fields}; > } catch { >@@ -123,7 +124,6 @@ sub process { > > my $data = $self->decoded_data; > $data->{report} = $report; >- > $self->finish($data); > } > >diff --git a/Koha/Biblio.pm b/Koha/Biblio.pm >index 0df2b489a7..4c88bcd8a5 100644 >--- a/Koha/Biblio.pm >+++ b/Koha/Biblio.pm >@@ -1921,164 +1921,195 @@ sub generate_marc_host_field { > > my $marcflavour = C4::Context->preference('marcflavour'); > my $marc_host = $self->metadata->record; >- my %sfd; >+ >+ my @sfd; > my $host_field; > my $link_field; > > if ( $marcflavour eq 'MARC21' ) { > >- # Author >- if ( $host_field = $marc_host->field('100') || $marc_host->field('110') || $marc_host->field('111') ) { >- my $s = $host_field->as_string('ab'); >- if ($s) { >- $sfd{a} = $s; >+ # Attempt to clone the main entry (100, 110, or 111) >+ my $main_entry = $marc_host->field('100') || $marc_host->field('110') || $marc_host->field('111'); >+ $main_entry = $main_entry ? $main_entry->clone : undef; >+ >+ # Clean unwanted subfields based on tag type >+ if ($main_entry) { >+ if ( $main_entry->tag eq '111' ) { >+ $main_entry->delete_subfield( code => qr/[94j]/ ); >+ } else { >+ $main_entry->delete_subfield( code => qr/[94e]/ ); > } > } > >- # Edition >- if ( $host_field = $marc_host->field('250') ) { >- my $s = $host_field->as_string('ab'); >+ # Construct subfield 7 from leader and main entry tag >+ my $s7 = "nn" . substr( $marc_host->leader, 6, 2 ); >+ if ($main_entry) { >+ my $c1 = 'n'; >+ if ( $main_entry->tag =~ /^1[01]/ ) { >+ $c1 = $main_entry->indicator('1'); >+ $c1 = $main_entry->tag eq '100' ? 1 : 2 unless $c1 =~ /\d/; >+ } >+ my $c0 = >+ ( $main_entry->tag eq '100' ) ? 'p' >+ : ( $main_entry->tag eq '110' ) ? 'c' >+ : ( $main_entry->tag eq '111' ) ? 'm' >+ : 'u'; >+ substr( $s7, 0, 2, $c0 . $c1 ); >+ } >+ push @sfd, ( '7' => $s7 ); >+ >+ # Subfield a - cleaned main entry string >+ if ($main_entry) { >+ my $a = $main_entry->as_string; >+ $a =~ s/\.$// unless $a =~ /\b[a-z]{1,2}\.$/i; >+ push @sfd, ( 'a' => $a ); >+ } >+ >+ # Subfield t - title from 245, cleaned >+ if ( my $f245 = $marc_host->field('245') ) { >+ my $f245c = $f245->clone; >+ $f245c->delete_subfield( code => 'c' ); >+ my $t = $f245c->as_string; >+ $t =~ s/[\s\/\\.]+$//; >+ my $nonfiling = $f245c->indicator('2') // 0; >+ $nonfiling = 0 unless $nonfiling =~ /^\d+$/; >+ $t = ucfirst substr( $t, $nonfiling ); >+ push @sfd, ( 't' => $t ); >+ } >+ >+ # Subfield b - edition from 250 >+ if ( my $f250 = $marc_host->field('250') ) { >+ my $b = $f250->as_string; >+ $b =~ s/\.$//; >+ if ($b) { >+ push @sfd, ( 'b' => $b ); >+ } >+ } >+ >+ # Subfield s - uniform title from 240 >+ if ( my $f240 = $marc_host->field('240') ) { >+ my $s = $f240->as_string('a'); > if ($s) { >- $sfd{b} = $s; >+ push @sfd, ( 's' => $s ); > } > } > >- # Publication >+ # Subfield d - publication info from 264/260 >+ my $d; > my @publication_fields = $marc_host->field('264'); > @publication_fields = $marc_host->field('260') unless (@publication_fields); > my $index = 0; >- for my $host_field (@publication_fields) { >+ for my $publication_field (@publication_fields) { > > # Use first entry unless we find a preferred indicator1 = 3 > if ( $index == 0 ) { >- my $s = $host_field->as_string('abc'); >+ my $s = $publication_field->as_string('abc'); >+ $s =~ s/\.$//; > if ($s) { >- $sfd{d} = $s; >+ $d = $s; > } > $index++; > } >- if ( $host_field->indicator(1) && ( $host_field->indicator(1) eq '3' ) ) { >- my $s = $host_field->as_string('abc'); >+ if ( $publication_field->indicator(1) && ( $publication_field->indicator(1) eq '3' ) ) { >+ my $s = $publication_field->as_string('abc'); >+ $s =~ s/\.$//; > if ($s) { >- $sfd{d} = $s; >+ $d = $s; > } > last; > } > } >- >- # Uniform title >- if ( $host_field = $marc_host->field('240') ) { >- my $s = $host_field->as_string('a'); >- if ($s) { >- $sfd{s} = $s; >- } >+ push @sfd, ( d => $d ) if $d; >+ >+ # Subfield k - host info from 800-830 fields >+ for my $f ( $marc_host->field('8[013][01]') ) { >+ my $k = $f->as_string('abcdnjltnp'); >+ $k .= ', ISSN ' . $f->subfield('x') if $f->subfield('x'); >+ $k .= ' ; ' . $f->subfield('v') if $f->subfield('v'); >+ push @sfd, ( 'k' => $k ); > } > >- # Title >- if ( $host_field = $marc_host->field('245') ) { >- my $s = $host_field->as_string('abnp'); >- if ($s) { >- $sfd{t} = $s; >- } >+ # Subfield x - ISSN from 022 >+ for my $f ( $marc_host->field('022') ) { >+ push @sfd, ( 'x' => $f->subfield('a') ) if $f->subfield('a'); > } > >- # ISSN >- if ( $host_field = $marc_host->field('022') ) { >- my $s = $host_field->as_string('a'); >- if ($s) { >- $sfd{x} = $s; >- } >+ # Subfield z - ISBN from 020 >+ for my $f ( $marc_host->field('020') ) { >+ push @sfd, ( 'z' => $f->subfield('a') ) if $f->subfield('a'); > } > >- # ISBN >- if ( $host_field = $marc_host->field('020') ) { >- my $s = $host_field->as_string('a'); >- if ($s) { >- $sfd{z} = $s; >+ # Subfield w - control number (001 and optionally 003) >+ if ( C4::Context->preference('UseControlNumber') ) { >+ if ( my $f001 = $marc_host->field('001') ) { >+ my $w = $f001->data; >+ if ( my $f003 = $marc_host->field('003') ) { >+ $w = '(' . $f003->data . ')' . $w; >+ } >+ push @sfd, ( 'w' => $w ); > } > } >- if ( C4::Context->preference('UseControlNumber') ) { > >- # Control number >- if ( $host_field = $marc_host->field('001') ) { >- $sfd{w} = $host_field->data(); >- } >+ # Construct 773 link field >+ $link_field = MARC::Field->new( 773, '0', ' ', @sfd ); > >- # Control number identifier >- if ( $host_field = $marc_host->field('003') ) { >- $sfd{w} = '(' . $host_field->data() . ')' . $sfd{w}; >- } >- } >- $link_field = MARC::Field->new( 773, '0', ' ', %sfd ); > } elsif ( $marcflavour eq 'UNIMARC' ) { > >- # Author >+ # Author (700/710/720) > if ( $host_field = $marc_host->field('700') || $marc_host->field('710') || $marc_host->field('720') ) { > my $s = $host_field->as_string('ab'); >- if ($s) { >- $sfd{a} = $s; >- } >+ push @sfd, ( 'a' => $s ) if $s; > } > >- # Place of publication >+ # Title (200) >+ if ( $host_field = $marc_host->field('200') ) { >+ my $s = $host_field->as_string('a'); >+ push @sfd, ( 't' => $s ) if $s; >+ } >+ >+ # Place of publication (210$a) > if ( $host_field = $marc_host->field('210') ) { > my $s = $host_field->as_string('a'); >- if ($s) { >- $sfd{c} = $s; >- } >+ push @sfd, ( 'c' => $s ) if $s; > } > >- # Date of publication >+ # Date of publication (210$d) > if ( $host_field = $marc_host->field('210') ) { > my $s = $host_field->as_string('d'); >- if ($s) { >- $sfd{d} = $s; >- } >+ push @sfd, ( 'd' => $s ) if $s; > } > >- # Edition statement >+ # Edition statement (205) > if ( $host_field = $marc_host->field('205') ) { >- my $s = $host_field->as_string(); >- if ($s) { >- $sfd{e} = $s; >- } >+ my $s = $host_field->as_string; >+ push @sfd, ( 'e' => $s ) if $s; > } > >- # Title >- if ( $host_field = $marc_host->field('200') ) { >- my $s = $host_field->as_string('a'); >- if ($s) { >- $sfd{t} = $s; >- } >- } >- >- #URL >+ # URL (856$u) > if ( $host_field = $marc_host->field('856') ) { > my $s = $host_field->as_string('u'); >- if ($s) { >- $sfd{u} = $s; >- } >+ push @sfd, ( u => $s ) if ($s); > } > >- # ISSN >+ # ISSN (011$a) > if ( $host_field = $marc_host->field('011') ) { > my $s = $host_field->as_string('a'); >- if ($s) { >- $sfd{x} = $s; >- } >+ push @sfd, ( x => $s ) if ($s); > } > >- # ISBN >+ # ISBN (010$a) > if ( $host_field = $marc_host->field('010') ) { > my $s = $host_field->as_string('a'); >- if ($s) { >- $sfd{y} = $s; >- } >+ push @sfd, ( y => $s ) if ($s); > } >+ >+ # Control number (001) > if ( $host_field = $marc_host->field('001') ) { >- $sfd{0} = $host_field->data(); >+ push @sfd, ( 0 => $host_field->data() ); > } >- $link_field = MARC::Field->new( 461, '0', ' ', %sfd ); >+ >+ # Construct 461 link field >+ $link_field = MARC::Field->new( 461, '0', ' ', @sfd ); > } > > return $link_field; >diff --git a/Koha/CookieManager.pm b/Koha/CookieManager.pm >index 3b655fa12c..2c5ae33516 100644 >--- a/Koha/CookieManager.pm >+++ b/Koha/CookieManager.pm >@@ -25,7 +25,28 @@ use CGI::Cookie; > > use C4::Context; > >-use constant DENY_LIST_VAR => 'do_not_remove_cookie'; >+# The cookies on the following list are removed unless koha-conf.xml indicates otherwise. >+# koha-conf.xml may also contain additional cookies to be removed. >+# Not including here: always_show_holds, catalogue_editor_, issues-table-load-immediately-circulation, >+# and ItemEditorSessionTemplateId. >+# TODO Not sure about branch (rotatingcollections), patronSessionConfirmation (circulation.pl) >+use constant MANAGED_COOKIE_PREFIXES => qw{ >+ bib_list >+ CGISESSID >+ holdfor holdforclub >+ intranet_bib_list >+ JWT >+ KohaOpacLanguage >+ LastCreatedItem >+ marctagstructure_selectdisplay >+ search_path_code >+ searchToOrder >+}; >+use constant PATH_EXCEPTIONS => { >+ always_show_holds => '/cgi-bin/koha/reserve', >+}; >+use constant KEEP_COOKIE_CONF_VAR => 'do_not_remove_cookie'; >+use constant REMOVE_COOKIE_CONF_VAR => 'remove_cookie'; > > our $cookies; > >@@ -41,18 +62,22 @@ Koha::CookieManager - Object for unified handling of cookies in Koha > # Replace cookies > $cookie_list = $mgr->replace_in_list( [ $cookie1, $cookie2_old ], $cookie2_new ); > >- # Clear cookies (governed by deny list entries in koha-conf) >+ # Clear cookies > $cookie_list = $mgr->clear_unless( $cookie1, $cookie2, $cookie3_name ); > > =head1 DESCRIPTION > >-The current object allows you to clear cookies in a list based on the deny list >-in koha-conf.xml. It also offers a method to replace the old version of a cookie >+The current object allows you to remove cookies on the hardcoded list >+in this module, refined by 'keep' or 'remove' entries in koha-conf.xml. >+Note that a keep entry overrules a remove. >+ >+This module also offers a method to replace the old version of a cookie > by a new one. > >-It could be extended by (gradually) routing cookie creation through it in order >-to consistently fill cookie parameters like httponly, secure and samesite flag, >-etc. And could serve to register all our cookies in a central location. >+The module could be extended by (gradually) routing cookie creation >+through it in order to consistently fill cookie parameters like httponly, >+secure and samesite flag, etc. And could serve to register all our cookies >+in a central location. > > =head1 METHODS > >@@ -64,11 +89,15 @@ etc. And could serve to register all our cookies in a central location. > > sub new { > my ( $class, $params ) = @_; >- my $self = bless $params // {}, $class; >- my $denied = C4::Context->config(DENY_LIST_VAR) || []; # expecting scalar or arrayref >- $denied = [$denied] if ref($denied) eq q{}; >- $self->{_remove_unless} = { map { $_ => 1 } @$denied }; >- $self->{_secure} = C4::Context->https_enabled; >+ my $self = bless $params // {}, $class; >+ >+ # Get keep and remove list from koha-conf (scalar or arrayref) >+ my $keep_list = C4::Context->config(KEEP_COOKIE_CONF_VAR) || []; >+ $self->{_keep_list} = ref($keep_list) ? $keep_list : [$keep_list]; >+ my $remove_list = C4::Context->config(REMOVE_COOKIE_CONF_VAR) || []; >+ $self->{_remove_list} = ref($remove_list) ? $remove_list : [$remove_list]; >+ >+ $self->{_secure} = C4::Context->https_enabled; > return $self; > } > >@@ -80,9 +109,9 @@ sub new { > Note: in the example above $query->cookie is a list of cookie names as returned > by the CGI object. > >- Returns an arrayref of cookie objects: empty, expired cookies for those passed >- by name or objects that are not on the deny list, together with the remaining >- (untouched) cookie objects that are on the deny list. >+ Returns an arrayref of cookie objects: empty, expired cookies for >+ cookies on the remove list, together with the remaining (untouched) >+ cookie objects. > > =cut > >@@ -102,17 +131,15 @@ sub clear_unless { > } > next if !$name; > >- if ( $self->_should_be_cleared($name) ) { >+ if ( $self->_should_be_removed($name) ) { > next if $seen->{$name}; >- push @rv, CGI::Cookie->new( >- >- # -expires explicitly omitted to create shortlived 'session' cookie >- # -HttpOnly explicitly set to 0: not really needed here for the >- # cleared httponly cookies, while the js cookies should be 0 >- -name => $name, -value => q{}, -HttpOnly => 0, >- $self->{_secure} ? ( -secure => 1 ) : (), >- ); > $seen->{$name} = 1; # prevent duplicates >+ if ($type) { >+ $c->max_age(0); >+ push @rv, _correct_path($c); >+ } else { >+ push @rv, _correct_path( CGI::Cookie->new( -name => $name, -value => q{}, '-max-age' => 0 ) ); >+ } > } elsif ( $type eq 'CGI::Cookie' ) { # keep the last occurrence > @rv = @{ $self->replace_in_list( \@rv, $c ) }; > } >@@ -120,22 +147,6 @@ sub clear_unless { > return \@rv; > } > >-sub _should_be_cleared { # when it is not on the deny list in koha-conf >- my ( $self, $name ) = @_; >- >- return if $self->{_remove_unless}->{$name}; # exact match >- >- # Now try the entries as regex >- foreach my $k ( keys %{ $self->{_remove_unless} } ) { >- my $reg = $self->{_remove_unless}->{$k}; >- >- # The entry in koha-conf should match the complete string >- # So adding a ^ and $ >- return if $name =~ qr/^${k}$/; >- } >- return 1; >-} >- > =head2 replace_in_list > > $list2 = $mgr->replace_in_list( $list1, $cookie ); >@@ -162,7 +173,44 @@ sub replace_in_list { > return \@result; > } > >-=head1 INTERNAL ROUTINES >+# INTERNAL ROUTINES >+ >+sub _should_be_removed { >+ my ( $self, $name ) = @_; >+ >+ # Is this a controlled cookie? Or is it added as 'keep' or 'remove' in koha-conf.xml? >+ # The conf entries are treated as prefix (no longer as regex). >+ return unless grep { $name =~ /^$_/ } MANAGED_COOKIE_PREFIXES(), @{ $self->{_remove_list} }; >+ return !grep { $name =~ /^$_/ } @{ $self->{_keep_list} }; >+} >+ >+sub _correct_path { >+ my $cookie_object = shift; >+ my $path = PATH_EXCEPTIONS->{ $cookie_object->name } or return $cookie_object; >+ $cookie_object->path($path); >+ return $cookie_object; >+} >+ >+=head1 ADDITIONAL COMMENTS >+ >+ How do the keep or remove lines in koha-conf.xml work? >+ >+ <do_not_remove_cookie>some_cookie</do_not_remove_cookie> >+ The name some_cookie should refer here to a cookie that is on the >+ hardcoded list in this module. If you do not want it to be cleared >+ (removed) on logout, include this line. >+ You might want to do this e.g. for KohaOpacLanguage. >+ >+ <remove_cookie>another_cookie</remove_cookie> >+ The name another_cookie refers here to a cookie that is not on the >+ hardcoded list but you want this cookie to be cleared/removed on logout. >+ It could be a custom cookie. >+ >+ Note that both directives use the cookie name as a prefix. So if you >+ add a remove line for cookie1, it also affects cookie12, etc. >+ Since a keep line overrules a remove line, this allows you to add >+ lines for removing cookie1 and not removing cookie12 in order to >+ remove cookie1, cookie11, cookie13 but not cookie12, etc. > > =cut > >diff --git a/Koha/Course.pm b/Koha/Course.pm >index 6b67a2b592..e4b800f043 100644 >--- a/Koha/Course.pm >+++ b/Koha/Course.pm >@@ -2,18 +2,18 @@ package Koha::Course; > > # This file is part of Koha. > # >-# Koha 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 3 of the License, or (at your option) any later >-# version. >+# Koha 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 3 of the License, or >+# (at your option) any later version. > # >-# Koha 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. >+# Koha 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 Koha; if not, write to the Free Software Foundation, Inc., >-# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. >+# You should have received a copy of the GNU General Public License >+# along with Koha; if not, see <http://www.gnu.org/licenses>. > > use Modern::Perl; > >diff --git a/Koha/Course/Instructor.pm b/Koha/Course/Instructor.pm >index 4b6f8a262d..da9864fe17 100644 >--- a/Koha/Course/Instructor.pm >+++ b/Koha/Course/Instructor.pm >@@ -2,18 +2,18 @@ package Koha::Course::Instructor; > > # This file is part of Koha. > # >-# Koha 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 3 of the License, or (at your option) any later >-# version. >+# Koha 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 3 of the License, or >+# (at your option) any later version. > # >-# Koha 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. >+# Koha 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 Koha; if not, write to the Free Software Foundation, Inc., >-# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. >+# You should have received a copy of the GNU General Public License >+# along with Koha; if not, see <http://www.gnu.org/licenses>. > > use Modern::Perl; > >diff --git a/Koha/Course/Instructors.pm b/Koha/Course/Instructors.pm >index 82a0191b34..95e13bb9c3 100644 >--- a/Koha/Course/Instructors.pm >+++ b/Koha/Course/Instructors.pm >@@ -2,18 +2,18 @@ package Koha::Course::Instructors; > > # This file is part of Koha. > # >-# Koha 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 3 of the License, or (at your option) any later >-# version. >+# Koha 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 3 of the License, or >+# (at your option) any later version. > # >-# Koha 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. >+# Koha 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 Koha; if not, write to the Free Software Foundation, Inc., >-# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. >+# You should have received a copy of the GNU General Public License >+# along with Koha; if not, see <http://www.gnu.org/licenses>. > > use Modern::Perl; > >diff --git a/Koha/Course/Item.pm b/Koha/Course/Item.pm >index d13be0ba55..6d6f3b626a 100644 >--- a/Koha/Course/Item.pm >+++ b/Koha/Course/Item.pm >@@ -2,18 +2,18 @@ package Koha::Course::Item; > > # This file is part of Koha. > # >-# Koha 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 3 of the License, or (at your option) any later >-# version. >+# Koha 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 3 of the License, or >+# (at your option) any later version. > # >-# Koha 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. >+# Koha 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 Koha; if not, write to the Free Software Foundation, Inc., >-# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. >+# You should have received a copy of the GNU General Public License >+# along with Koha; if not, see <http://www.gnu.org/licenses>. > > use Modern::Perl; > >diff --git a/Koha/Course/Items.pm b/Koha/Course/Items.pm >index f563998515..b6feda4f77 100644 >--- a/Koha/Course/Items.pm >+++ b/Koha/Course/Items.pm >@@ -2,18 +2,18 @@ package Koha::Course::Items; > > # This file is part of Koha. > # >-# Koha 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 3 of the License, or (at your option) any later >-# version. >+# Koha 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 3 of the License, or >+# (at your option) any later version. > # >-# Koha 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. >+# Koha 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 Koha; if not, write to the Free Software Foundation, Inc., >-# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. >+# You should have received a copy of the GNU General Public License >+# along with Koha; if not, see <http://www.gnu.org/licenses>. > > use Modern::Perl; > >diff --git a/Koha/Course/Reserve.pm b/Koha/Course/Reserve.pm >index 6493ba90a3..0fb1906c9b 100644 >--- a/Koha/Course/Reserve.pm >+++ b/Koha/Course/Reserve.pm >@@ -2,18 +2,18 @@ package Koha::Course::Reserve; > > # This file is part of Koha. > # >-# Koha 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 3 of the License, or (at your option) any later >-# version. >+# Koha 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 3 of the License, or >+# (at your option) any later version. > # >-# Koha 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. >+# Koha 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 Koha; if not, write to the Free Software Foundation, Inc., >-# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. >+# You should have received a copy of the GNU General Public License >+# along with Koha; if not, see <http://www.gnu.org/licenses>. > > use Modern::Perl; > >diff --git a/Koha/Course/Reserves.pm b/Koha/Course/Reserves.pm >index b824a78da2..5bd38a2fe7 100644 >--- a/Koha/Course/Reserves.pm >+++ b/Koha/Course/Reserves.pm >@@ -2,18 +2,18 @@ package Koha::Course::Reserves; > > # This file is part of Koha. > # >-# Koha 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 3 of the License, or (at your option) any later >-# version. >+# Koha 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 3 of the License, or >+# (at your option) any later version. > # >-# Koha 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. >+# Koha 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 Koha; if not, write to the Free Software Foundation, Inc., >-# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. >+# You should have received a copy of the GNU General Public License >+# along with Koha; if not, see <http://www.gnu.org/licenses>. > > use Modern::Perl; > >diff --git a/Koha/Courses.pm b/Koha/Courses.pm >index 6a3f396daa..7604deab0b 100644 >--- a/Koha/Courses.pm >+++ b/Koha/Courses.pm >@@ -2,18 +2,18 @@ package Koha::Courses; > > # This file is part of Koha. > # >-# Koha 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 3 of the License, or (at your option) any later >-# version. >+# Koha 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 3 of the License, or >+# (at your option) any later version. > # >-# Koha 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. >+# Koha 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 Koha; if not, write to the Free Software Foundation, Inc., >-# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. >+# You should have received a copy of the GNU General Public License >+# along with Koha; if not, see <http://www.gnu.org/licenses>. > > use Modern::Perl; > >diff --git a/Koha/CoverImages.pm b/Koha/CoverImages.pm >index 774d36ada8..26e9fc5e5e 100644 >--- a/Koha/CoverImages.pm >+++ b/Koha/CoverImages.pm >@@ -25,7 +25,7 @@ use base qw(Koha::Objects); > > =head1 NAME > >-Koha::Cities - Koha CoverImage Object set class >+Koha::CoverImages - Koha CoverImage Object set class > > =head1 API > >diff --git a/Koha/Devel/Files.pm b/Koha/Devel/Files.pm >new file mode 100644 >index 0000000000..df1bba620b >--- /dev/null >+++ b/Koha/Devel/Files.pm >@@ -0,0 +1,192 @@ >+package Koha::Devel::Files; >+ >+use Modern::Perl; >+our ( @ISA, @EXPORT_OK ); >+ >+=head1 NAME >+ >+Koha::Devel::Files - A utility module for managing and filtering file lists in the Koha codebase >+ >+=head1 SYNOPSIS >+ >+ use Koha::Devel::Files; >+ >+ my $file_manager = Koha::Devel::Files->new({ context => 'tidy' }); >+ >+ my @perl_files = $file_manager->ls_perl_files($git_range); >+ my @js_files = $file_manager->ls_js_files(); >+ my @tt_files = $file_manager->ls_tt_files(); >+ >+ my $filetype = $file_manager->get_filetype($filename); >+ >+=head1 DESCRIPTION >+ >+Koha::Devel::Files is a utility module designed to assist in managing and filtering lists of files in the Koha codebase. It provides methods to list Perl, JavaScript, and Template Toolkit files, with options to exclude specific files based on a given context. >+ >+=head1 EXCEPTIONS >+ >+The module defines a set of exceptions for different file types and contexts. These exceptions are used to exclude specific files or directories from the file listings. >+ >+=cut >+ >+my $exceptions = { >+ pl => { >+ tidy => [ >+ qw( >+ Koha/Schema/Result >+ Koha/Schema.pm >+ ) >+ ], >+ valid => [ >+ qw( >+ Koha/Account/Credit.pm >+ Koha/Account/Debit.pm >+ Koha/Old/Hold.pm >+ misc/translator/TmplTokenizer.pm >+ ) >+ ], >+ codespell => [ >+ qw( >+ installer/data/mysql/updatedatabase.pl >+ installer/data/mysql/update22to30.pl >+ installer/data/mysql/db_revs/241200035.pl >+ misc/cronjobs/build_browser_and_cloud.pl >+ ) >+ ], >+ }, >+ js => { >+ tidy => [ >+ qw( >+ koha-tmpl/intranet-tmpl/lib >+ koha-tmpl/intranet-tmpl/js/Gettext.js >+ koha-tmpl/opac-tmpl/lib >+ Koha/ILL/Backend/ >+ ) >+ ], >+ codespell => [ >+ qw( >+ koha-tmpl/intranet-tmpl/lib >+ koha-tmpl/intranet-tmpl/js/Gettext.js >+ koha-tmpl/opac-tmpl/lib >+ koha-tmpl/opac-tmpl/bootstrap/js/Gettext.js >+ ) >+ ], >+ }, >+ tt => { >+ tidy => [ >+ qw( >+ Koha/ILL/Backend/ >+ *doc-head-open.inc >+ misc/cronjobs/rss >+ ) >+ ], >+ codespell => [], >+ }, >+}; >+ >+=head1 METHODS >+ >+=cut >+ >+=head2 new >+ >+ my $file_manager = Koha::Devel::Files->new({ context => 'tidy' }); >+ >+Creates a new instance of Koha::Devel::Files. The constructor accepts a hash reference with a 'context' key, which specifies the context for file exclusions. >+ >+=cut >+ >+sub new { >+ my ( $class, $args ) = @_; >+ my $self = { context => $args->{context} }; >+ bless $self, $class; >+ return $self; >+} >+ >+=head2 build_git_exclude >+ >+ my $exclude_pattern = $file_manager->build_git_exclude($filetype); >+ >+Builds a Git exclude pattern for a given file type based on the context provided during object creation. >+ >+=cut >+ >+sub build_git_exclude { >+ my ( $self, $filetype ) = @_; >+ return $self->{context} && exists $exceptions->{$filetype}->{ $self->{context} } >+ ? join( " ", map( "':(exclude)$_'", @{ $exceptions->{$filetype}->{ $self->{context} } } ) ) >+ : q{}; >+} >+ >+=head2 ls_perl_files >+ >+ my @perl_files = $file_manager->ls_perl_files($git_range); >+ >+Lists Perl files (with extensions .pl, .PL, .pm, .t) that have been modified within a specified Git range. If no range is provided, it lists all Perl files, excluding those specified in the exceptions. >+ >+=cut >+ >+sub ls_perl_files { >+ my ($self) = @_; >+ my $cmd = sprintf q{git ls-files '*.pl' '*.PL' '*.pm' '*.t' svc opac/svc debian/build-git-snapshot %s}, >+ $self->build_git_exclude('pl'); >+ my @files = qx{$cmd}; >+ chomp for @files; >+ return @files; >+} >+ >+=head2 ls_js_files >+ >+ my @js_files = $file_manager->ls_js_files(); >+ >+Lists JavaScript and TypeScript files (with extensions .js, .ts, .vue) in the repository, excluding those specified in the exceptions. >+ >+=cut >+ >+sub ls_js_files { >+ my ($self) = @_; >+ my $cmd = sprintf q{git ls-files '*.js' '*.ts' '*.vue' %s}, $self->build_git_exclude('js'); >+ my @files = qx{$cmd}; >+ chomp for @files; >+ return @files; >+} >+ >+=head2 ls_tt_files >+ >+ my @tt_files = $file_manager->ls_tt_files(); >+ >+Lists Template Toolkit files (with extensions .tt, .inc) in the repository, excluding those specified in the exceptions. >+ >+=cut >+ >+sub ls_tt_files { >+ my ($self) = @_; >+ my $cmd = sprintf q{git ls-files '*.tt' '*.inc' %s}, $self->build_git_exclude('tt'); >+ my @files = qx{$cmd}; >+ chomp for @files; >+ return @files; >+} >+ >+=head2 get_filetype >+ >+ my $filetype = $file_manager->get_filetype($filename); >+ >+Determines the file type of a given file based on its extension or path. Returns 'pl' for Perl files, 'js' for JavaScript/TypeScript files, and 'tt' for Template Toolkit files. Dies with an error message if the file type cannot be determined. >+ >+=cut >+ >+sub get_filetype { >+ my ( $self, $file ) = @_; >+ return 'pl' if $file =~ m{^svc} || $file =~ m{^opac/svc}; >+ return 'pl' if $file =~ m{\.pl$} || $file =~ m{\.pm} || $file =~ m{\.t$}; >+ return 'pl' if $file =~ m{\.PL$}; >+ return 'pl' if $file =~ m{debian/build-git-snapshot}; >+ >+ return 'js' if $file =~ m{\.js$} || $file =~ m{\.ts$} || $file =~ m{\.vue$}; >+ >+ return 'tt' if $file =~ m{\.inc$} || $file =~ m{\.tt$}; >+ >+ die sprintf 'Cannot guess filetype for %s', $file; >+} >+ >+1; >diff --git a/Koha/Encryption.pm b/Koha/Encryption.pm >index b18f75a9d4..4cecb6e282 100644 >--- a/Koha/Encryption.pm >+++ b/Koha/Encryption.pm >@@ -61,8 +61,9 @@ sub new { > ); > } > return $class->SUPER::new( >- -key => $encryption_key, >- -cipher => 'Cipher::AES' >+ -key => $encryption_key, >+ -cipher => 'Cipher::AES', >+ -nodeprecate => 1, > ); > } > >diff --git a/Koha/Exceptions/MarcOverlayRule.pm b/Koha/Exceptions/MarcOverlayRule.pm >index d2e44970af..2cedb3526a 100644 >--- a/Koha/Exceptions/MarcOverlayRule.pm >+++ b/Koha/Exceptions/MarcOverlayRule.pm >@@ -2,18 +2,18 @@ package Koha::Exceptions::MarcOverlayRule; > > # This file is part of Koha. > # >-# Koha 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 3 of the License, or (at your option) any later >-# version. >+# Koha 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 3 of the License, or >+# (at your option) any later version. > # >-# Koha 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. >+# Koha 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 Koha; if not, write to the Free Software Foundation, Inc., >-# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. >+# You should have received a copy of the GNU General Public License >+# along with Koha; if not, see <http://www.gnu.org/licenses>. > > use Modern::Perl; > >diff --git a/Koha/Filter/MARC/TrimFields.pm b/Koha/Filter/MARC/TrimFields.pm >index 99a5f0dfa3..00244cab29 100644 >--- a/Koha/Filter/MARC/TrimFields.pm >+++ b/Koha/Filter/MARC/TrimFields.pm >@@ -60,9 +60,15 @@ sub filter { > my $value = $subfield->[1]; > $value =~ s/[\n\r]+/ /g; > $value =~ s/^\s+|\s+$//g; >- $field->add_subfields( $key => $value ); # add subfield to the end of the subfield list >- $field->delete_subfield( pos => 0 ); # delete the subfield at the top of the subfield list >+ $field->add_subfields( $key => $value ) >+ if $value ne q{} >+ ; # add subfield to the end of the subfield list, but only if there is still a non empty value there >+ $field->delete_subfield( pos => 0 ); # delete the subfield at the top of the subfield list > } >+ >+ # if it happed that all existing subfields had whitespaces only, >+ # the field would be empty now and should be removed from the record >+ $record->delete_fields($field) unless scalar( $field->subfields ); > } > } > return $record; >diff --git a/Koha/Hold.pm b/Koha/Hold.pm >index 8f0cd75ac2..59b498a7d2 100644 >--- a/Koha/Hold.pm >+++ b/Koha/Hold.pm >@@ -972,6 +972,9 @@ sub store { > } > } > } >+ if ( exists $updated_columns{branchcode} ) { >+ Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue->new->enqueue( { biblio_ids => [ $self->biblionumber ] } ); >+ } > } > > $self = $self->SUPER::store; >diff --git a/Koha/I18N.pm b/Koha/I18N.pm >index c957e9df40..3fbbabe685 100644 >--- a/Koha/I18N.pm >+++ b/Koha/I18N.pm >@@ -53,8 +53,51 @@ our @EXPORT = qw( > N__np > ); > >+our @EXPORT_OK = qw( >+ available_locales >+); >+ > our $textdomain = 'Koha'; > >+=head1 NAME >+ >+Koha::I18N - Internationalization functions for Koha >+ >+=head1 SYNOPSIS >+ >+ use Koha::I18N; >+ >+ # Basic translation functions >+ my $translated = __('Hello world'); >+ my $with_vars = __x('Hello {name}', name => 'World'); >+ my $plural = __n('one item', '{count} items', $count, count => $count); >+ >+ # Context-aware translations >+ my $context = __p('menu', 'File'); >+ >+ # Get available system locales (explicitly imported) >+ use Koha::I18N qw(available_locales); >+ my $locales = available_locales(); >+ >+=head1 DESCRIPTION >+ >+This module provides internationalization (i18n) functions for Koha using the >+GNU gettext system. It handles locale setup, message translation, and provides >+utility functions for working with system locales. >+ >+The module automatically initializes the locale environment and provides a set >+of translation functions that support variable substitution, plural forms, and >+contextual translations. >+ >+=head1 FUNCTIONS >+ >+=head2 init >+ >+Initializes the internationalization system by setting up locale environment >+variables and configuring gettext. This is called automatically when needed. >+ >+=cut >+ > sub init { > my $cache = Koha::Cache::Memory::Lite->get_instance(); > my $cache_key = 'i18n:initialized'; >@@ -92,6 +135,14 @@ sub init { > } > } > >+=head2 __ >+ >+ my $translated = __('Text to translate'); >+ >+Basic translation function. Returns the translated text for the given message ID. >+ >+=cut >+ > sub __ { > my ($msgid) = @_; > >@@ -100,6 +151,15 @@ sub __ { > return _gettext( \&gettext, [$msgid] ); > } > >+=head2 __x >+ >+ my $translated = __x('Hello {name}', name => 'World'); >+ >+Translation with variable substitution. Variables in {brackets} are replaced >+with the corresponding values from the provided hash. >+ >+=cut >+ > sub __x { > my ( $msgid, %vars ) = @_; > >@@ -108,6 +168,14 @@ sub __x { > return _gettext( \&gettext, [$msgid], %vars ); > } > >+=head2 __n >+ >+ my $translated = __n('one item', '{count} items', $count); >+ >+Plural-aware translation. Returns singular or plural form based on the count. >+ >+=cut >+ > sub __n { > my ( $msgid, $msgid_plural, $count ) = @_; > >@@ -117,6 +185,14 @@ sub __n { > return _gettext( \&ngettext, [ $msgid, $msgid_plural, $count ] ); > } > >+=head2 __nx >+ >+ my $translated = __nx('one item', '{count} items', $count, count => $count); >+ >+Plural-aware translation with variable substitution. >+ >+=cut >+ > sub __nx { > my ( $msgid, $msgid_plural, $count, %vars ) = @_; > >@@ -126,10 +202,25 @@ sub __nx { > return _gettext( \&ngettext, [ $msgid, $msgid_plural, $count ], %vars ); > } > >+=head2 __xn >+ >+Alias for __nx. >+ >+=cut >+ > sub __xn { > return __nx(@_); > } > >+=head2 __p >+ >+ my $translated = __p('context', 'Text to translate'); >+ >+Context-aware translation. Allows the same text to be translated differently >+based on context (e.g., 'File' in a menu vs 'File' as a document type). >+ >+=cut >+ > sub __p { > my ( $msgctxt, $msgid ) = @_; > >@@ -139,6 +230,14 @@ sub __p { > return _gettext( \&pgettext, [ $msgctxt, $msgid ] ); > } > >+=head2 __px >+ >+ my $translated = __px('context', 'Hello {name}', name => 'World'); >+ >+Context-aware translation with variable substitution. >+ >+=cut >+ > sub __px { > my ( $msgctxt, $msgid, %vars ) = @_; > >@@ -148,6 +247,14 @@ sub __px { > return _gettext( \&pgettext, [ $msgctxt, $msgid ], %vars ); > } > >+=head2 __np >+ >+ my $translated = __np('context', 'one item', '{count} items', $count); >+ >+Context-aware plural translation. >+ >+=cut >+ > sub __np { > my ( $msgctxt, $msgid, $msgid_plural, $count ) = @_; > >@@ -158,6 +265,14 @@ sub __np { > return _gettext( \&npgettext, [ $msgctxt, $msgid, $msgid_plural, $count ] ); > } > >+=head2 __npx >+ >+ my $translated = __npx('context', 'one item', '{count} items', $count, count => $count); >+ >+Context-aware plural translation with variable substitution. >+ >+=cut >+ > sub __npx { > my ( $msgctxt, $msgid, $msgid_plural, $count, %vars ) = @_; > >@@ -168,18 +283,51 @@ sub __npx { > return _gettext( \&npgettext, [ $msgctxt, $msgid, $msgid_plural, $count ], %vars ); > } > >+=head2 N__ >+ >+ my $msgid = N__('Text for later translation'); >+ >+No-operation translation marker. Returns the original text unchanged but marks >+it for extraction by translation tools. >+ >+=cut >+ > sub N__ { > return $_[0]; > } > >+=head2 N__n >+ >+ my $msgid = N__n('singular', 'plural'); >+ >+No-operation plural translation marker. >+ >+=cut >+ > sub N__n { > return $_[0]; > } > >+=head2 N__p >+ >+ my $msgid = N__p('context', 'Text for later translation'); >+ >+No-operation context translation marker. >+ >+=cut >+ > sub N__p { > return $_[1]; > } > >+=head2 N__np >+ >+ my $msgid = N__np('context', 'singular', 'plural'); >+ >+No-operation context plural translation marker. >+ >+=cut >+ > sub N__np { > return $_[1]; > } >@@ -224,4 +372,118 @@ sub _expand { > return $text; > } > >+=head2 available_locales >+ >+ my $locales = Koha::I18N::available_locales(); >+ >+Returns an arrayref of available system locales for use in system preferences. >+ >+Each locale is a hashref with: >+ - C<value>: The locale identifier (e.g., 'en_US.utf8', 'default') >+ - C<text>: Human-readable description (e.g., 'English (United States) - en_US.utf8') >+ >+Always includes 'default' as the first option. Additional locales are detected >+from the system using C<locale -a> and filtered for UTF-8 locales. >+ >+=cut >+ >+sub available_locales { >+ my @available_locales = (); >+ >+ # Always include default option >+ push @available_locales, { >+ value => 'default', >+ text => 'Default Unicode collation' >+ }; >+ >+ # Get system locales using the same approach as init() >+ my @system_locales = grep { chomp; not( /^C/ || $_ eq 'POSIX' ) } qx/locale -a/; >+ >+ my @filtered_locales = (); >+ for my $locale (@system_locales) { >+ >+ # Filter for useful locales (UTF-8 ones and common patterns) >+ if ( $locale =~ /^[a-z]{2}_[A-Z]{2}\.utf8?$/i || $locale =~ /^[a-z]{2}_[A-Z]{2}$/i ) { >+ >+ # Create friendly display names >+ my $display_name = $locale; >+ if ( $locale =~ /^([a-z]{2})_([A-Z]{2})/ ) { >+ my %lang_names = ( >+ 'en' => 'English', >+ 'fr' => 'French', >+ 'de' => 'German', >+ 'es' => 'Spanish', >+ 'it' => 'Italian', >+ 'pt' => 'Portuguese', >+ 'nl' => 'Dutch', >+ 'pl' => 'Polish', >+ 'fi' => 'Finnish', >+ 'sv' => 'Swedish', >+ 'da' => 'Danish', >+ 'no' => 'Norwegian', >+ 'ru' => 'Russian', >+ 'ja' => 'Japanese', >+ 'zh' => 'Chinese', >+ 'ar' => 'Arabic', >+ 'hi' => 'Hindi' >+ ); >+ my %country_names = ( >+ 'US' => 'United States', >+ 'GB' => 'United Kingdom', >+ 'FR' => 'France', >+ 'DE' => 'Germany', >+ 'ES' => 'Spain', >+ 'IT' => 'Italy', >+ 'PT' => 'Portugal', >+ 'BR' => 'Brazil', >+ 'NL' => 'Netherlands', >+ 'PL' => 'Poland', >+ 'FI' => 'Finland', >+ 'SE' => 'Sweden', >+ 'DK' => 'Denmark', >+ 'NO' => 'Norway', >+ 'RU' => 'Russia', >+ 'JP' => 'Japan', >+ 'CN' => 'China', >+ 'TW' => 'Taiwan', >+ 'SA' => 'Saudi Arabia', >+ 'IN' => 'India' >+ ); >+ my $lang = $lang_names{$1} || uc($1); >+ my $country = $country_names{$2} || $2; >+ $display_name = "$lang ($country) - $locale"; >+ } >+ push @filtered_locales, { >+ value => $locale, >+ text => $display_name >+ }; >+ } >+ } >+ >+ # Sort locales by display name and add to available list >+ @filtered_locales = sort { $a->{text} cmp $b->{text} } @filtered_locales; >+ push @available_locales, @filtered_locales; >+ >+ return \@available_locales; >+} >+ >+=head1 AUTHOR >+ >+Koha Development Team >+ >+=head1 COPYRIGHT >+ >+Copyright 2012-2014 BibLibre >+ >+=head1 LICENSE >+ >+This file is part of Koha. >+ >+Koha 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 3 of the License, or (at your option) any later >+version. >+ >+=cut >+ > 1; >diff --git a/Koha/ILL/Backend/Standard.pm b/Koha/ILL/Backend/Standard.pm >index 5962cd1765..444c0074d9 100644 >--- a/Koha/ILL/Backend/Standard.pm >+++ b/Koha/ILL/Backend/Standard.pm >@@ -1058,8 +1058,6 @@ sub _openurl_to_ill { > volume => 'volume', > isbn => 'isbn', > issn => 'issn', >- rft_id => 'doi', >- id => 'doi', > doi => 'doi', > year => 'year', > title => 'title', >@@ -1097,8 +1095,19 @@ sub _openurl_to_ill { > # Otherwise, pass it through untransformed and maybe move it > # to our custom parameters array > if ( !exists $ignore->{$meta_key} ) { >- push @{$custom_key}, $meta_key; >- push @{$custom_value}, $params->{other}->{$meta_key}; >+ if ( $meta_key eq 'id' || $meta_key eq 'rft_id' ) { >+ if ( $params->{other}->{$meta_key} =~ /:/ ) { >+ my ( $k, $v ) = split /:/, $params->{other}->{$meta_key}, 2; >+ if ( defined $k && defined $v ) { >+ $return->{ lc $k } = $v; >+ } >+ } else { >+ $return->{doi} = $params->{other}->{$meta_key}; >+ } >+ } else { >+ push @{$custom_key}, $meta_key; >+ push @{$custom_value}, $params->{other}->{$meta_key}; >+ } > } else { > $return->{$meta_key} = $params->{other}->{$meta_key}; > } >diff --git a/Koha/ILL/Request.pm b/Koha/ILL/Request.pm >index 9b151e3924..4c8453d4dd 100644 >--- a/Koha/ILL/Request.pm >+++ b/Koha/ILL/Request.pm >@@ -1776,6 +1776,8 @@ sub send_patron_notice { > ); > my @transports = keys %{ $borrower_preferences->{transports} }; > >+ return { result => { fail => [ 'email', 'sms' ], success => [] } } unless @transports; >+ > # Notice should come from the library where the request was placed, > # not the patrons home library > my $branch = Koha::Libraries->find( $self->branchcode ); >diff --git a/Koha/Import/OAI/Authorities.pm b/Koha/Import/OAI/Authorities.pm >index cdff207854..9b78afefb4 100644 >--- a/Koha/Import/OAI/Authorities.pm >+++ b/Koha/Import/OAI/Authorities.pm >@@ -13,6 +13,7 @@ package Koha::Import::OAI::Authorities; > # GNU General Public License for more details. > # > # You should have received a copy of the GNU General Public License >+# along with Koha; if not, see <http://www.gnu.org/licenses>. > > use Modern::Perl; > >diff --git a/Koha/Import/OAI/Authority.pm b/Koha/Import/OAI/Authority.pm >index f4e3a0d31f..965851d39c 100644 >--- a/Koha/Import/OAI/Authority.pm >+++ b/Koha/Import/OAI/Authority.pm >@@ -13,6 +13,7 @@ package Koha::Import::OAI::Authority; > # GNU General Public License for more details. > # > # You should have received a copy of the GNU General Public License >+# along with Koha; if not, see <http://www.gnu.org/licenses>. > > use Modern::Perl; > >diff --git a/Koha/Import/OAI/Biblio.pm b/Koha/Import/OAI/Biblio.pm >index 42ad046e72..3478a348eb 100644 >--- a/Koha/Import/OAI/Biblio.pm >+++ b/Koha/Import/OAI/Biblio.pm >@@ -13,6 +13,7 @@ package Koha::Import::OAI::Biblio; > # GNU General Public License for more details. > # > # You should have received a copy of the GNU General Public License >+# along with Koha; if not, see <http://www.gnu.org/licenses>. > > use Modern::Perl; > >diff --git a/Koha/Import/OAI/Biblios.pm b/Koha/Import/OAI/Biblios.pm >index 59ceba9cd3..4a7a04cf79 100644 >--- a/Koha/Import/OAI/Biblios.pm >+++ b/Koha/Import/OAI/Biblios.pm >@@ -13,6 +13,7 @@ package Koha::Import::OAI::Biblios; > # GNU General Public License for more details. > # > # You should have received a copy of the GNU General Public License >+# along with Koha; if not, see <http://www.gnu.org/licenses>. > > use Modern::Perl; > >diff --git a/Koha/Installer.pm b/Koha/Installer.pm >index 235eac3247..48a50df2b6 100644 >--- a/Koha/Installer.pm >+++ b/Koha/Installer.pm >@@ -29,7 +29,7 @@ sub needs_update { > my $koha_version = Koha->version; > my $code_version = TransformToNum($koha_version); > >- if ( $db_version == $code_version ) { >+ if ( $db_version && $db_version == $code_version ) { > $needs_update = 0; > } > >diff --git a/Koha/Item.pm b/Koha/Item.pm >index 87e6dd8c31..eb36e62b06 100644 >--- a/Koha/Item.pm >+++ b/Koha/Item.pm >@@ -188,6 +188,24 @@ sub store { > } > } > >+ my $prevent_withdrawing_of = C4::Context->preference('PreventWithdrawingItemsStatus'); >+ my @statuses_to_prevent = defined $prevent_withdrawing_of ? split( ',', $prevent_withdrawing_of ) : (); >+ my $prevent_onloan = grep { $_ eq 'checkedout' } @statuses_to_prevent; >+ my $prevent_intransit = grep { $_ eq 'intransit' } @statuses_to_prevent; >+ >+ if ( exists $updated_columns{withdrawn} && $updated_columns{withdrawn} ) { >+ my $transfer = $pre_mod_item->get_transfer; >+ if ( $pre_mod_item->onloan && $prevent_onloan ) { >+ Koha::Exceptions::Item::Transfer::OnLoan->throw( error => "onloan_cannot_withdraw" ); >+ return $self->SUPER::store; >+ } >+ >+ if ( defined $transfer && $transfer->in_transit && $prevent_intransit ) { >+ Koha::Exceptions::Item::Transfer::InTransit->throw( error => "intransit_cannot_withdraw" ); >+ return $self->SUPER::store; >+ } >+ } >+ > if ( exists $updated_columns{itemcallnumber} > or exists $updated_columns{cn_source} ) > { >diff --git a/Koha/Items.pm b/Koha/Items.pm >index 99429456be..7ac9f034ae 100644 >--- a/Koha/Items.pm >+++ b/Koha/Items.pm >@@ -459,6 +459,7 @@ sub batch_update { > > my ( @modified_itemnumbers, $modified_fields ); > my $i; >+ my @errors; > my $schema = Koha::Database->new->schema; > while ( my $item = $self->next ) { > >@@ -578,6 +579,9 @@ sub batch_update { > } > ) > } catch { >+ push @errors, { >+ error => eval { $_->{error} } || "$_", >+ }; > warn $_ > }; > >@@ -600,7 +604,10 @@ sub batch_update { > } > } > >- return ( { modified_itemnumbers => \@modified_itemnumbers, modified_fields => $modified_fields }, $self ); >+ return ( >+ { modified_itemnumbers => \@modified_itemnumbers, modified_fields => $modified_fields, errors => \@errors }, >+ $self >+ ); > } > > =head2 apply_regex >diff --git a/Koha/Manual.pm b/Koha/Manual.pm >index fe5cc2c5b3..e811c64867 100644 >--- a/Koha/Manual.pm >+++ b/Koha/Manual.pm >@@ -117,6 +117,7 @@ our $mapping = { > 'admin/preferences#logs' => '/logspreferences.html', > 'admin/preferences#opac' => '/opacpreferences.html', > 'admin/preferences#patrons' => '/patronspreferences.html', >+ 'admin/preferences#reports' => '/reportspreferences.html', > 'admin/preferences#searching' => '/searchingpreferences.html', > 'admin/preferences#serials' => '/serialspreferences.html', > 'admin/preferences#staff_interface' => '/staffclientpreferences.html', >diff --git a/Koha/Patron.pm b/Koha/Patron.pm >index dcfff7aeb0..cfa570e0f0 100644 >--- a/Koha/Patron.pm >+++ b/Koha/Patron.pm >@@ -388,7 +388,7 @@ sub store { > > if ( C4::Context->preference('ChildNeedsGuarantor') > and ( $self->is_child or $self->category->can_be_guarantee ) >- and $self->contactname eq "" >+ and ( !defined $self->contactname || $self->contactname eq "" ) > and !@$guarantors ) > { > Koha::Exceptions::Patron::Relationship::NoGuarantor->throw(); >@@ -473,9 +473,8 @@ ListOwnershipUponPatronDeletion pref, but entries from the borrower to other lis > sub delete { > my ($self) = @_; > >- my $anonymous_patron = C4::Context->preference("AnonymousPatron"); > Koha::Exceptions::Patron::FailedDeleteAnonymousPatron->throw() >- if $anonymous_patron && $self->id eq $anonymous_patron; >+ if $self->is_anonymous; > > # Check if patron is protected > Koha::Exceptions::Patron::FailedDeleteProtectedPatron->throw() if defined $self->protected && $self->protected == 1; >@@ -736,8 +735,7 @@ sub siblings { > sub merge_with { > my ( $self, $patron_ids ) = @_; > >- my $anonymous_patron = C4::Context->preference("AnonymousPatron"); >- return if $anonymous_patron && $self->id eq $anonymous_patron; >+ return if $self->is_anonymous; > > # Do not merge other patrons into a protected patron > return if $self->protected; >@@ -755,12 +753,12 @@ sub merge_with { > sub { > foreach my $patron_id (@patron_ids) { > >- next if $anonymous_patron && $patron_id eq $anonymous_patron; >- > my $patron = Koha::Patrons->find($patron_id); > > next unless $patron; > >+ next if $patron->is_anonymous; >+ > # Do not merge protected patrons into other patrons > next if $patron->protected; > >@@ -1318,6 +1316,158 @@ sub move_to_deleted { > return Koha::Database->new->schema->resultset('Deletedborrower')->create($patron_infos); > } > >+=head3 can_place_holds >+ >+ my $result = $patron->can_place_holds(); >+ my $result = $patron->can_place_holds( >+ { >+ overrides => { debt_limit => 1, card_lost => 1 }, >+ no_short_circuit => 1 >+ } >+ ); >+ >+ if ( $patron->can_place_holds() ) { >+ # patron can place holds >+ } else { >+ my @messages = $result->messages; >+ # handle error messages >+ } >+ >+Checks if a patron is allowed to place holds based on various patron conditions. >+ >+=head4 Parameters >+ >+=over 4 >+ >+=item * C<$options> (optional) - Hashref with the following keys: >+ >+=over 8 >+ >+=item * C<overrides> - Hashref of checks to skip. Keys should match error message codes. >+ >+=item * C<no_short_circuit> - Boolean. If true, performs all checks and collects all error messages instead of stopping at first failure. Default: false. >+ >+=back >+ >+=back >+ >+=head4 Returns >+ >+Koha::Result::Boolean object - true if patron can place holds, false otherwise. >+When false, the result object contains error messages with details about why >+holds are blocked. >+ >+=head4 Error Messages >+ >+The following error message codes may be returned: >+ >+=over 4 >+ >+=item * C<expired> - Patron account has expired and expired patrons are blocked from placing holds >+ >+=item * C<debt_limit> - Patron owes more than the maximum allowed outstanding amount >+ >+=item * C<bad_address> - Patron's address is marked as incorrect >+ >+=item * C<card_lost> - Patron's library card is marked as lost >+ >+=item * C<restricted> - Patron account is restricted/debarred >+ >+=item * C<hold_limit> - Patron has reached the maximum number of allowed holds >+ >+=back >+ >+Error messages may include additional payload data with relevant details >+(amounts, limits, counts, etc.). >+ >+=cut >+ >+sub can_place_holds { >+ my ( $self, $options ) = @_; >+ $options //= {}; >+ >+ my $overrides = $options->{overrides} // {}; >+ my $no_short_circuit = $options->{no_short_circuit} // 0; >+ >+ my $result = Koha::Result::Boolean->new(1); >+ >+ # expired patron check >+ unless ( $overrides->{expired} ) { >+ if ( $self->is_expired && $self->category->effective_BlockExpiredPatronOpacActions_contains('hold') ) { >+ $result->set_value(0); >+ $result->add_message( { message => 'expired', type => 'error' } ); >+ >+ return $result unless $no_short_circuit; >+ } >+ } >+ >+ # debt check >+ unless ( $overrides->{debt_limit} ) { >+ my $max_outstanding = C4::Context->preference("maxoutstanding"); >+ my $outstanding = $self->account->balance; >+ >+ if ( $max_outstanding && $outstanding && ( $outstanding > $max_outstanding ) ) { >+ $result->set_value(0); >+ $result->add_message( >+ { >+ message => 'debt_limit', type => 'error', >+ payload => { total_outstanding => $outstanding, max_outstanding => $max_outstanding } >+ } >+ ); >+ >+ return $result unless $no_short_circuit; >+ } >+ } >+ >+ # check address marked as incorrect >+ unless ( $overrides->{bad_address} ) { >+ if ( $self->gonenoaddress ) { >+ $result->set_value(0); >+ $result->add_message( { message => 'bad_address', type => 'error' } ); >+ >+ return $result unless $no_short_circuit; >+ } >+ } >+ >+ # check lost card >+ unless ( $overrides->{card_lost} ) { >+ if ( $self->lost ) { >+ $result->set_value(0); >+ $result->add_message( { message => 'card_lost', type => 'error' } ); >+ return $result unless $no_short_circuit; >+ } >+ } >+ >+ # check restrictions >+ unless ( $overrides->{restricted} ) { >+ if ( $self->is_debarred ) { >+ $result->set_value(0); >+ $result->add_message( { message => 'restricted', type => 'error' } ); >+ >+ return $result unless $no_short_circuit; >+ } >+ } >+ >+ # check max reserves >+ unless ( $overrides->{hold_limit} ) { >+ my $max_holds = C4::Context->preference("maxreserves"); >+ my $holds_count = $self->holds->count; >+ if ( $max_holds && ( $holds_count >= $max_holds ) ) { >+ $result->set_value(0); >+ $result->add_message( >+ { >+ message => 'hold_limit', type => 'error', >+ payload => { total_holds => $holds_count, max_holds => $max_holds } >+ } >+ ); >+ >+ return $result unless $no_short_circuit; >+ } >+ } >+ >+ return $result; >+} >+ > =head3 can_request_article > > if ( $patron->can_request_article( $library->id ) ) { ... } >@@ -2525,6 +2675,8 @@ sub _anonymize_column { > $val = $nullable ? undef : 0; > } elsif ( $type =~ /date|time/ ) { > $val = $nullable ? undef : dt_from_string; >+ } elsif ( $type eq 'enum' ) { >+ $val = $nullable ? undef : $col_info->{default_value}; > } > $self->$col($val); > } >@@ -2894,11 +3046,9 @@ This method tells if the Koha:Patron object can be deleted. Possible return valu > sub safe_to_delete { > my ($self) = @_; > >- my $anonymous_patron = C4::Context->preference('AnonymousPatron'); >- > my $error; > >- if ( $anonymous_patron && $self->id eq $anonymous_patron ) { >+ if ( $self->is_anonymous ) { > $error = 'is_anonymous_patron'; > } elsif ( $self->checkouts->count ) { > $error = 'has_checkouts'; >@@ -3251,6 +3401,20 @@ sub is_patron_inside_charge_limits { > return $patron_charge_limits; > } > >+=head3 is_anonymous >+ >+my $is_anonymous_patron= $patron->is_anonymous(); >+ >+Returns true if the patron is the anonymous patron (AnonymousPatron) >+ >+=cut >+ >+sub is_anonymous { >+ my ($self) = @_; >+ my $anonymous_patron = C4::Context->preference('AnonymousPatron'); >+ return ( $anonymous_patron && $self->borrowernumber eq $anonymous_patron ) ? 1 : 0; >+} >+ > =head2 Internal methods > > =head3 _type >diff --git a/Koha/Patron/Discharge.pm b/Koha/Patron/Discharge.pm >index 7da95ab7c3..1d4b41e84b 100644 >--- a/Koha/Patron/Discharge.pm >+++ b/Koha/Patron/Discharge.pm >@@ -14,6 +14,24 @@ use Koha::DateUtils qw( dt_from_string output_pref ); > use Koha::Patrons; > use Koha::Patron::Debarments qw( AddDebarment ); > >+=head1 NAME >+ >+Koha::Patron::Discharge - Koha Discharge object class >+ >+=head1 API >+ >+=head2 Class Methods >+ >+=cut >+ >+=head3 count >+ >+ Koha::Patron:Discharge->count; >+ >+Return the number of discharges corresponding to the asked criteria >+ >+=cut >+ > sub count { > my ($params) = @_; > my $values = {}; >@@ -31,6 +49,14 @@ sub count { > return search_limited($values)->count; > } > >+=head3 can_be_discharged >+ >+ my $can_be_discharged = Koha::Patron:Discharge->can_be_discharged({borrowernumber => $borrowernumber}); >+ >+Return true if the borrower can be discharged >+ >+=cut >+ > sub can_be_discharged { > my ($params) = @_; > return unless $params->{borrowernumber}; >@@ -56,6 +82,14 @@ sub can_be_discharged { > return ( $can_be_discharged, $problems ); > } > >+=head3 is_discharged >+ >+ my $is_discharged = Koha::Patron:Discharge->is_discharged({borrowernumber => $borrowernumber}); >+ >+Return true if the borrower is discharged >+ >+=cut >+ > sub is_discharged { > my ($params) = @_; > return unless $params->{borrowernumber}; >@@ -71,6 +105,14 @@ sub is_discharged { > } > } > >+=head3 request >+ >+ my $request = Koha::Patron:Discharge->request({borrowernumber => $borrowernumber}); >+ >+Place a discharge request on a given borrower after checking the borrower has the right to be discharged. >+ >+=cut >+ > sub request { > my ($params) = @_; > my $borrowernumber = $params->{borrowernumber}; >@@ -87,6 +129,14 @@ sub request { > ); > } > >+=head3 discharge >+ >+ my $request = Koha::Patron:Discharge->discharge({borrowernumber => $borrowernumber}); >+ >+Place a discharge request on a given borrower, if a discharge was requested, update the status to discharged and place a suspension on the user. >+ >+=cut >+ > sub discharge { > my ($params) = @_; > my $borrowernumber = $params->{borrowernumber}; >@@ -124,6 +174,14 @@ sub discharge { > } > } > >+=head3 generate_as_pdf >+ >+ my $request = Koha::Patron:Discharge->generate_as_pdf({borrowernumber => $borrowernumber}); >+ >+Create a pdf from an existing discharge associated to the borrowernumber. >+ >+=cut >+ > sub generate_as_pdf { > my ($params) = @_; > return unless $params->{borrowernumber}; >@@ -175,6 +233,17 @@ sub generate_as_pdf { > return $pdf_path; > } > >+=head3 get_pendings >+ >+ my $rs = Koha::Patron:Discharge->get_pendings({ >+ borrowernumber => $borrowernumber >+ branchcode => $branchcode >+ }); >+ >+Get all pending discharges associated to a borrowernumber and/or a given branch >+ >+=cut >+ > sub get_pendings { > my ($params) = @_; > my $branchcode = $params->{branchcode}; >@@ -190,6 +259,17 @@ sub get_pendings { > return search_limited($cond); > } > >+=head3 get_validated >+ >+ my $rs = Koha::Patron:Discharge->get_validated({ >+ borrowernumber => $borrowernumber >+ branchcode => $branchcode >+ }); >+ >+Get all validated discharges associated to a borrowernumber and/or a given branch >+ >+=cut >+ > sub get_validated { > my ($params) = @_; > my $branchcode = $params->{branchcode}; >@@ -205,6 +285,18 @@ sub get_validated { > } > > # TODO This module should be based on Koha::Object[s] >+ >+=head3 search_limited >+ >+ my $rs = Koha::Patron:Discharge->search_limited({ >+ borrower.branchcode => $branchcode >+ }, >+ $attributes); >+ >+Search all discharges that can be seen by the user and fitting the given conditions >+ >+=cut >+ > sub search_limited { > my ( $params, $attributes ) = @_; > my $userenv = C4::Context->userenv; >diff --git a/Koha/Patrons/Import.pm b/Koha/Patrons/Import.pm >index 23b6f57803..f36495d76a 100644 >--- a/Koha/Patrons/Import.pm >+++ b/Koha/Patrons/Import.pm >@@ -267,7 +267,10 @@ LINE: while ( my $borrowerline = <$handle> ) { > > # Remove warning for int datatype that cannot be null > # Argument "" isn't numeric in numeric eq (==) at /usr/share/perl5/DBIx/Class/Row.pm line 1018 >- for my $field (qw( privacy privacy_guarantor_fines privacy_guarantor_checkouts anonymized login_attempts )) { >+ for my $field ( >+ qw( privacy privacy_guarantor_fines privacy_guarantor_checkouts anonymized login_attempts checkprevcheckout autorenew_checkouts ) >+ ) >+ { > delete $borrower{$field} > if exists $borrower{$field} and $borrower{$field} eq ""; > } >@@ -304,7 +307,8 @@ LINE: while ( my $borrowerline = <$handle> ) { > next if $col eq 'password' && !$overwrite_passwords; > next if $col eq 'dateexpiry' && $update_dateexpiry; > >- $borrower{$col} = $member->{$col} if $col eq 'dateexpiry' && !$columns[ $csvkeycol{$col} ]; >+ $borrower{$col} = $member->{$col} >+ if $col eq 'dateexpiry' && ( !$csvkeycol{$col} || !$columns[ $csvkeycol{$col} ] ); > > unless ( exists( $csvkeycol{$col} ) || $defaults->{$col} ) { > $borrower{$col} = $member->{$col} if ( $member->{$col} ); >diff --git a/Koha/Quotes.pm b/Koha/Quotes.pm >index 95eccd50d4..9dadb5de8d 100644 >--- a/Koha/Quotes.pm >+++ b/Koha/Quotes.pm >@@ -71,7 +71,7 @@ sub get_daily_quote { > my $dt = $dtf->format_date(dt_from_string); > $quote = $self->search( > { >- timestamp => { -between => => [ "$dt 00:00:00", "$dt 23:59:59" ] }, >+ timestamp => { -between => [ "$dt 00:00:00", "$dt 23:59:59" ] }, > }, > { > order_by => { -desc => 'timestamp' }, >diff --git a/Koha/REST/V1/CirculationRules.pm b/Koha/REST/V1/CirculationRules.pm >index 74c54b813d..81bd51fc81 100644 >--- a/Koha/REST/V1/CirculationRules.pm >+++ b/Koha/REST/V1/CirculationRules.pm >@@ -2,18 +2,18 @@ package Koha::REST::V1::CirculationRules; > > # This file is part of Koha. > # >-# Koha 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 3 of the License, or (at your option) any later >-# version. >+# Koha 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 3 of the License, or >+# (at your option) any later version. > # >-# Koha 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. >+# Koha 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 Koha; if not, write to the Free Software Foundation, Inc., >-# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. >+# You should have received a copy of the GNU General Public License >+# along with Koha; if not, see <http://www.gnu.org/licenses>. > > use Modern::Perl; > >diff --git a/Koha/REST/V1/Holds.pm b/Koha/REST/V1/Holds.pm >index 7f8764e6cb..33c885774c 100644 >--- a/Koha/REST/V1/Holds.pm >+++ b/Koha/REST/V1/Holds.pm >@@ -88,7 +88,9 @@ sub add { > my $non_priority = $body->{non_priority}; > > my $overrides = $c->stash('koha.overrides'); >- my $can_override = $overrides->{any} && C4::Context->preference('AllowHoldPolicyOverride'); >+ my $can_override = C4::Context->preference('AllowHoldPolicyOverride') // 0; >+ >+ my $override_all = $overrides->{any} && C4::Context->preference('AllowHoldPolicyOverride') ? 1 : 0; > > if ( !C4::Context->preference('AllowHoldDateInFuture') && $hold_date ) { > return $c->render( >@@ -142,7 +144,7 @@ sub add { > } > > # If the hold is being forced, no need to validate >- unless ($can_override) { >+ unless ($override_all) { > > # Validate pickup location > my $valid_pickup_location; >@@ -161,21 +163,28 @@ sub add { > openapi => { error => 'The supplied pickup location is not valid' } > ) unless $valid_pickup_location; > >- my $can_place_hold = >+ my $can_place_holds = $patron->can_place_holds( { overrides => $overrides } ); >+ >+ if ( !$can_place_holds ) { >+ my $error_code = $can_place_holds->messages->[0]->message; >+ return $c->render( >+ status => 409, >+ openapi => { >+ error => 'Hold cannot be placed. Reason: ' . $error_code, >+ error_code => $error_code, >+ } >+ ); >+ } >+ >+ my $can_hold_be_placed = > $item > ? C4::Reserves::CanItemBeReserved( $patron, $item ) > : C4::Reserves::CanBookBeReserved( $patron_id, $biblio_id ); > >- if ( C4::Context->preference('maxreserves') >- && $patron->holds->count + 1 > C4::Context->preference('maxreserves') ) >- { >- $can_place_hold->{status} = 'tooManyReserves'; >- } >- >- unless ( $can_place_hold->{status} eq 'OK' ) { >+ unless ( $can_hold_be_placed->{status} eq 'OK' ) { > return $c->render( > status => 403, >- openapi => { error => "Hold cannot be placed. Reason: " . $can_place_hold->{status} } >+ openapi => { error => "Hold cannot be placed. Reason: " . $can_hold_be_placed->{status} } > ); > } > } >diff --git a/Koha/REST/V1/Suggestions.pm b/Koha/REST/V1/Suggestions.pm >index 9c3a0a0d50..442a36a8ec 100644 >--- a/Koha/REST/V1/Suggestions.pm >+++ b/Koha/REST/V1/Suggestions.pm >@@ -2,17 +2,18 @@ package Koha::REST::V1::Suggestions; > > # This file is part of Koha. > # >-# Koha 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 3 of the License, or (at your option) any later >-# version. >+# Koha 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 3 of the License, or >+# (at your option) any later version. > # >-# Koha 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. >+# Koha 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 Koha; if not, see <http:°www.gnu.org/licenses>. >+# along with Koha; if not, see <http://www.gnu.org/licenses>. > > use Modern::Perl; > >diff --git a/Koha/Report.pm b/Koha/Report.pm >index 54511cdc4c..8c292a16a8 100644 >--- a/Koha/Report.pm >+++ b/Koha/Report.pm >@@ -20,9 +20,13 @@ use Modern::Perl; > use Koha::Database; > use Koha::Reports; > >+use Koha::Object; >+use Koha::Object::Limit::Library; >+ >+use base qw(Koha::Object Koha::Object::Limit::Library); >+ > #use Koha::DateUtils qw( dt_from_string output_pref ); > >-use base qw(Koha::Object); > # > # FIXME We could only return an error code instead of the arrayref > # Only 1 error is returned >@@ -268,4 +272,18 @@ sub _type { > return 'SavedSql'; > } > >+=head3 _library_limits >+ >+Configurable library limits >+ >+=cut >+ >+sub _library_limits { >+ return { >+ class => "ReportsBranch", >+ id => "report_id", >+ library => "branchcode", >+ }; >+} >+ > 1; >diff --git a/Koha/Reports.pm b/Koha/Reports.pm >index 7d99232b9b..387c72abb1 100644 >--- a/Koha/Reports.pm >+++ b/Koha/Reports.pm >@@ -21,7 +21,7 @@ use Koha::Database; > > use Koha::Report; > >-use base qw(Koha::Objects); >+use base qw(Koha::Objects Koha::Objects::Limit::Library); > > =head1 NAME > >@@ -33,6 +33,33 @@ Koha::Reports - Koha Report Object set class > > =cut > >+=head3 search_with_localization >+ >+my $itemtypes = Koha::ItemTypes->search_with_localization >+ >+=cut >+ >+sub search_with_localization { >+ my ( $self, $params, $attributes ) = @_; >+ >+ my $language = C4::Languages::getlanguage(); >+ $Koha::Schema::Result::Itemtype::LANGUAGE = $language; >+ $attributes->{order_by} = 'translated_description' unless exists $attributes->{order_by}; >+ $attributes->{join} = 'localization'; >+ $attributes->{'+select'} = [ >+ { >+ coalesce => [qw( localization.translation me.description )], >+ -as => 'translated_description' >+ } >+ ]; >+ if ( defined $params->{branchcode} ) { >+ my $branchcode = delete $params->{branchcode}; >+ $self->search_with_library_limits( $params, $attributes, $branchcode ); >+ } else { >+ $self->SUPER::search( $params, $attributes ); >+ } >+} >+ > =head3 _type > > Returns name of corresponding DBIC resultset >diff --git a/Koha/Schema/Result/Borrower.pm b/Koha/Schema/Result/Borrower.pm >index 2e1a5e9d51..1e1fa11cd4 100644 >--- a/Koha/Schema/Result/Borrower.pm >+++ b/Koha/Schema/Result/Borrower.pm >@@ -582,10 +582,10 @@ controls if relatives can see this patron's checkouts > > =head2 checkprevcheckout > >- data_type: 'varchar' >+ data_type: 'enum' > default_value: 'inherit' >+ extra: {list => ["yes","no","inherit"]} > is_nullable: 0 >- size: 7 > > produce a warning for this patron if this item has previously been checked out to this patron if 'yes', not if 'no', defer to category setting if 'inherit'. > >@@ -832,10 +832,10 @@ __PACKAGE__->add_columns( > { data_type => "tinyint", default_value => 0, is_nullable => 0 }, > "checkprevcheckout", > { >- data_type => "varchar", >+ data_type => "enum", > default_value => "inherit", >+ extra => { list => ["yes", "no", "inherit"] }, > is_nullable => 0, >- size => 7, > }, > "updated_on", > { >@@ -2197,8 +2197,8 @@ Composing rels: L</user_permissions> -> permission > __PACKAGE__->many_to_many("permissions", "user_permissions", "permission"); > > >-# Created by DBIx::Class::Schema::Loader v0.07051 @ 2025-04-28 16:41:47 >-# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:J1oaW0hFRihEJZ6U+XSwag >+# Created by DBIx::Class::Schema::Loader v0.07051 @ 2025-07-10 07:11:31 >+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:XFOe2X4k2DUziaNS5pWd/Q > > __PACKAGE__->belongs_to( > "library", >diff --git a/Koha/Schema/Result/Category.pm b/Koha/Schema/Result/Category.pm >index 62c6e3fb4d..9cb9622295 100644 >--- a/Koha/Schema/Result/Category.pm >+++ b/Koha/Schema/Result/Category.pm >@@ -136,10 +136,10 @@ Default privacy setting for this patron category > > =head2 checkprevcheckout > >- data_type: 'varchar' >+ data_type: 'enum' > default_value: 'inherit' >+ extra: {list => ["yes","no","inherit"]} > is_nullable: 0 >- size: 7 > > produce a warning for this patron category if this item has previously been checked out to this patron if 'yes', not if 'no', defer to syspref setting if 'inherit'. > >@@ -274,10 +274,10 @@ __PACKAGE__->add_columns( > }, > "checkprevcheckout", > { >- data_type => "varchar", >+ data_type => "enum", > default_value => "inherit", >+ extra => { list => ["yes", "no", "inherit"] }, > is_nullable => 0, >- size => 7, > }, > "can_place_ill_in_opac", > { data_type => "tinyint", default_value => 1, is_nullable => 0 }, >@@ -410,8 +410,8 @@ __PACKAGE__->has_many( > ); > > >-# Created by DBIx::Class::Schema::Loader v0.07051 @ 2024-12-03 15:46:23 >-# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:jiQq3bW+ZfdYpUpfZq1Rzw >+# Created by DBIx::Class::Schema::Loader v0.07051 @ 2025-07-10 07:11:31 >+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:ADt+iDjteg9Jb81L2FMIvg > > # You can replace this text with custom code or comments, and it will be preserved on regeneration > >diff --git a/Koha/Schema/Result/Deletedborrower.pm b/Koha/Schema/Result/Deletedborrower.pm >index b2decd87d2..5d0f10facb 100644 >--- a/Koha/Schema/Result/Deletedborrower.pm >+++ b/Koha/Schema/Result/Deletedborrower.pm >@@ -579,10 +579,10 @@ controls if relatives can see this patron's checkouts > > =head2 checkprevcheckout > >- data_type: 'varchar' >+ data_type: 'enum' > default_value: 'inherit' >+ extra: {list => ["yes","no","inherit"]} > is_nullable: 0 >- size: 7 > > produce a warning for this patron if this item has previously been checked out to this patron if 'yes', not if 'no', defer to category setting if 'inherit'. > >@@ -817,10 +817,10 @@ __PACKAGE__->add_columns( > { data_type => "tinyint", default_value => 0, is_nullable => 0 }, > "checkprevcheckout", > { >- data_type => "varchar", >+ data_type => "enum", > default_value => "inherit", >+ extra => { list => ["yes", "no", "inherit"] }, > is_nullable => 0, >- size => 7, > }, > "updated_on", > { >@@ -857,8 +857,8 @@ __PACKAGE__->add_columns( > ); > > >-# Created by DBIx::Class::Schema::Loader v0.07051 @ 2025-04-28 16:41:47 >-# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:5QA+/eORyEKhkG++0f3Hvw >+# Created by DBIx::Class::Schema::Loader v0.07051 @ 2025-07-10 07:11:31 >+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:vAkNNQagJcYfZ0/6mDOw8g > > __PACKAGE__->add_columns( > '+anonymized' => { is_boolean => 1 }, >diff --git a/Koha/Schema/Result/ReportsBranch.pm b/Koha/Schema/Result/ReportsBranch.pm >new file mode 100644 >index 0000000000..970c25eabc >--- /dev/null >+++ b/Koha/Schema/Result/ReportsBranch.pm >@@ -0,0 +1,85 @@ >+use utf8; >+ >+package Koha::Schema::Result::ReportsBranch; >+ >+# Created by DBIx::Class::Schema::Loader >+# DO NOT MODIFY THE FIRST PART OF THIS FILE >+ >+=head1 NAME >+ >+Koha::Schema::Result::ReportsBranch >+ >+=cut >+ >+use strict; >+use warnings; >+ >+use base 'DBIx::Class::Core'; >+ >+=head1 TABLE: C<reports_branches> >+ >+=cut >+ >+__PACKAGE__->table("reports_branches"); >+ >+=head1 ACCESSORS >+ >+=head2 report_id >+ >+ data_type: 'integer' >+ is_foreign_key: 1 >+ is_nullable: 0 >+ >+=head2 branchcode >+ >+ data_type: 'varchar' >+ is_foreign_key: 1 >+ is_nullable: 0 >+ size: 10 >+ >+=cut >+ >+__PACKAGE__->add_columns( >+ "report_id", >+ { data_type => "integer", is_foreign_key => 1, is_nullable => 0 }, >+ "branchcode", >+ { data_type => "varchar", is_foreign_key => 1, is_nullable => 0, size => 10 }, >+); >+ >+=head1 RELATIONS >+ >+=head2 branchcode >+ >+Type: belongs_to >+ >+Related object: L<Koha::Schema::Result::Branch> >+ >+=cut >+ >+__PACKAGE__->belongs_to( >+ "branchcode", >+ "Koha::Schema::Result::Branch", >+ { branchcode => "branchcode" }, >+ { is_deferrable => 1, on_delete => "CASCADE", on_update => "RESTRICT" }, >+); >+ >+=head2 report >+ >+Type: belongs_to >+ >+Related object: L<Koha::Schema::Result::SavedSql> >+ >+=cut >+ >+__PACKAGE__->belongs_to( >+ "report", >+ "Koha::Schema::Result::SavedSql", >+ { id => "report_id" }, >+ { is_deferrable => 1, on_delete => "CASCADE", on_update => "RESTRICT" }, >+); >+ >+# Created by DBIx::Class::Schema::Loader v0.07051 @ 2025-07-06 17:02:11 >+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:SJqUxMgLJHAjbX3GJlpB+A >+ >+# You can replace this text with custom code or comments, and it will be preserved on regeneration >+1; >diff --git a/Koha/Schema/Result/SavedSql.pm b/Koha/Schema/Result/SavedSql.pm >index 6f75ae3b31..4f063e8d67 100644 >--- a/Koha/Schema/Result/SavedSql.pm >+++ b/Koha/Schema/Result/SavedSql.pm >@@ -185,6 +185,221 @@ __PACKAGE__->add_columns( > > __PACKAGE__->set_primary_key("id"); > >+=head1 RELATIONS >+ >+=head2 reports_branches >+ >+Type: has_many >+ >+Related object: L<Koha::Schema::Result::ReportsBranch> >+ >+=cut >+ >+__PACKAGE__->has_many( >+ "reports_branches", >+ "Koha::Schema::Result::ReportsBranch", >+ { "foreign.report_id" => "self.id" }, >+ { cascade_copy => 0, cascade_delete => 0 }, >+); >+ >+ >+# Created by DBIx::Class::Schema::Loader v0.07051 @ 2025-07-06 17:02:11 >+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:qFqDaOxgdm2I12cJEiCieg >+# These lines were loaded from '/kohadevbox/koha/Koha/Schema/Result/SavedSql.pm' found in @INC. >+# They are now part of the custom portion of this file >+# for you to hand-edit. If you do not either delete >+# this section or remove that file from @INC, this section >+# will be repeated redundantly when you re-create this >+# file again via Loader! See skip_load_external to disable >+# this feature. >+ >+use utf8; >+package Koha::Schema::Result::SavedSql; >+ >+# Created by DBIx::Class::Schema::Loader >+# DO NOT MODIFY THE FIRST PART OF THIS FILE >+ >+=head1 NAME >+ >+Koha::Schema::Result::SavedSql >+ >+=cut >+ >+use strict; >+use warnings; >+ >+use base 'DBIx::Class::Core'; >+ >+=head1 TABLE: C<saved_sql> >+ >+=cut >+ >+__PACKAGE__->table("saved_sql"); >+ >+=head1 ACCESSORS >+ >+=head2 id >+ >+ data_type: 'integer' >+ is_auto_increment: 1 >+ is_nullable: 0 >+ >+unique id and primary key assigned by Koha >+ >+=head2 borrowernumber >+ >+ data_type: 'integer' >+ is_nullable: 1 >+ >+the staff member who created this report (borrowers.borrowernumber) >+ >+=head2 date_created >+ >+ data_type: 'datetime' >+ datetime_undef_if_invalid: 1 >+ is_nullable: 1 >+ >+the date this report was created >+ >+=head2 last_modified >+ >+ data_type: 'datetime' >+ datetime_undef_if_invalid: 1 >+ is_nullable: 1 >+ >+the date this report was last edited >+ >+=head2 savedsql >+ >+ data_type: 'mediumtext' >+ is_nullable: 1 >+ >+the SQL for this report >+ >+=head2 last_run >+ >+ data_type: 'datetime' >+ datetime_undef_if_invalid: 1 >+ is_nullable: 1 >+ >+=head2 report_name >+ >+ data_type: 'varchar' >+ default_value: (empty string) >+ is_nullable: 0 >+ size: 255 >+ >+the name of this report >+ >+=head2 type >+ >+ data_type: 'varchar' >+ is_nullable: 1 >+ size: 255 >+ >+always 1 for tabular >+ >+=head2 notes >+ >+ data_type: 'mediumtext' >+ is_nullable: 1 >+ >+the notes or description given to this report >+ >+=head2 cache_expiry >+ >+ data_type: 'integer' >+ default_value: 300 >+ is_nullable: 0 >+ >+=head2 public >+ >+ data_type: 'tinyint' >+ default_value: 0 >+ is_nullable: 0 >+ >+=head2 report_area >+ >+ data_type: 'varchar' >+ is_nullable: 1 >+ size: 6 >+ >+=head2 report_group >+ >+ data_type: 'varchar' >+ is_nullable: 1 >+ size: 80 >+ >+=head2 report_subgroup >+ >+ data_type: 'varchar' >+ is_nullable: 1 >+ size: 80 >+ >+=head2 mana_id >+ >+ data_type: 'integer' >+ is_nullable: 1 >+ >+=cut >+ >+__PACKAGE__->add_columns( >+ "id", >+ { data_type => "integer", is_auto_increment => 1, is_nullable => 0 }, >+ "borrowernumber", >+ { data_type => "integer", is_nullable => 1 }, >+ "date_created", >+ { >+ data_type => "datetime", >+ datetime_undef_if_invalid => 1, >+ is_nullable => 1, >+ }, >+ "last_modified", >+ { >+ data_type => "datetime", >+ datetime_undef_if_invalid => 1, >+ is_nullable => 1, >+ }, >+ "savedsql", >+ { data_type => "mediumtext", is_nullable => 1 }, >+ "last_run", >+ { >+ data_type => "datetime", >+ datetime_undef_if_invalid => 1, >+ is_nullable => 1, >+ }, >+ "report_name", >+ { data_type => "varchar", default_value => "", is_nullable => 0, size => 255 }, >+ "type", >+ { data_type => "varchar", is_nullable => 1, size => 255 }, >+ "notes", >+ { data_type => "mediumtext", is_nullable => 1 }, >+ "cache_expiry", >+ { data_type => "integer", default_value => 300, is_nullable => 0 }, >+ "public", >+ { data_type => "tinyint", default_value => 0, is_nullable => 0 }, >+ "report_area", >+ { data_type => "varchar", is_nullable => 1, size => 6 }, >+ "report_group", >+ { data_type => "varchar", is_nullable => 1, size => 80 }, >+ "report_subgroup", >+ { data_type => "varchar", is_nullable => 1, size => 80 }, >+ "mana_id", >+ { data_type => "integer", is_nullable => 1 }, >+); >+ >+=head1 PRIMARY KEY >+ >+=over 4 >+ >+=item * L</id> >+ >+=back >+ >+=cut >+ >+__PACKAGE__->set_primary_key("id"); >+ > > # Created by DBIx::Class::Schema::Loader v0.07049 @ 2021-01-21 13:39:29 > # DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:198dNG9DGQzop+s5IHy7sw >@@ -214,3 +429,8 @@ sub koha_objects_class { > } > > 1; >+# End of lines loaded from '/kohadevbox/koha/Koha/Schema/Result/SavedSql.pm' >+ >+ >+# You can replace this text with custom code or comments, and it will be preserved on regeneration >+1; >\ No newline at end of file >diff --git a/Koha/SearchEngine/Elasticsearch/Search.pm b/Koha/SearchEngine/Elasticsearch/Search.pm >index c88345bb58..e81d1df84e 100644 >--- a/Koha/SearchEngine/Elasticsearch/Search.pm >+++ b/Koha/SearchEngine/Elasticsearch/Search.pm >@@ -54,6 +54,9 @@ use MARC::File::XML; > use MIME::Base64 qw( decode_base64 ); > use JSON; > >+use POSIX qw(setlocale LC_COLLATE); >+use Unicode::Collate::Locale; >+ > Koha::SearchEngine::Elasticsearch::Search->mk_accessors(qw( store )); > > =head2 search >@@ -453,6 +456,42 @@ sub max_result_window { > return $max_result_window; > } > >+=head2 _sort_facets >+ >+ my $facets = _sort_facets($facets); >+ >+Sorts facets using a locale. >+ >+=cut >+ >+sub _sort_facets { >+ my ( $self, $args ) = @_; >+ my $facets = $args->{facets}; >+ my $locale = $args->{locale}; >+ >+ if ( !$locale ) { >+ >+ # Get locale from system preference, falling back to system LC_COLLATE >+ $locale = C4::Context->preference('FacetSortingLocale') || 'default'; >+ if ( $locale eq 'default' || !$locale ) { >+ >+ #NOTE: When setlocale is run with only the 1st parameter, it is a "get" not a "set" function. >+ $locale = setlocale(LC_COLLATE) || 'default'; >+ } >+ } >+ >+ my $collator = Unicode::Collate::Locale->new( locale => $locale ); >+ if ( $collator && $facets ) { >+ my @sorted_facets = sort { $collator->cmp( $a->{facet_label_value}, $b->{facet_label_value} ) } @{$facets}; >+ if (@sorted_facets) { >+ return \@sorted_facets; >+ } >+ } >+ >+ #NOTE: If there was a problem, at least return the not sorted facets >+ return $facets; >+} >+ > =head2 _convert_facets > > my $koha_facets = _convert_facets($es_facets); >@@ -526,14 +565,16 @@ sub _convert_facets { > facet_count => $c, > facet_link_value => $t, > facet_title_value => $t, >- facet_label_value => $label, # TODO either truncate this, >- # or make the template do it like it should anyway >+ facet_label_value => $label || q{}, # TODO either truncate this, >+ # or make the template do it like it should anyway > type_link_value => $type, > }; > } > if ( C4::Context->preference('FacetOrder') eq 'Alphabetical' ) { >- @{ $facet->{facets} } = >- sort { $a->{facet_label_value} cmp $b->{facet_label_value} } @{ $facet->{facets} }; >+ my $sorted_facets = $self->_sort_facets( { facets => $facet->{facets} } ); >+ if ($sorted_facets) { >+ $facet->{facets} = $sorted_facets; >+ } > } > push @facets, $facet if exists $facet->{facets}; > } >diff --git a/acqui/basket.pl b/acqui/basket.pl >index 791acd3250..57621b4e7a 100755 >--- a/acqui/basket.pl >+++ b/acqui/basket.pl >@@ -613,16 +613,20 @@ sub edi_close_and_order { > } > exit; > } else { >+ >+ my $ean_description = $query->param('ean_description'); >+ my $ean_branch = $query->param('ean_branch'); >+ > $template->param( > edi_confirm => 1, > booksellerid => $booksellerid, > basketno => $basket->{basketno}, > basketname => $basket->{basketname}, > basketgroupname => $basket->{basketname}, >+ ean => $ean ? $ean : '', >+ ean_description => $ean_description ? $ean_description : '', >+ ean_branch => $ean_branch ? $ean_branch : '', > ); >- if ($ean) { >- $template->param( ean => $ean ); >- } > > } > return; >diff --git a/acqui/newordersuggestion.pl b/acqui/newordersuggestion.pl >index e41381f4f4..344cf79148 100755 >--- a/acqui/newordersuggestion.pl >+++ b/acqui/newordersuggestion.pl >@@ -93,7 +93,7 @@ use Modern::Perl; > use CGI qw ( -utf8 ); > use C4::Auth qw( get_template_and_user ); > use C4::Output qw( output_html_with_http_headers ); >-use C4::Suggestions qw( ConnectSuggestionAndBiblio ); >+use C4::Suggestions qw( ConnectSuggestionAndBiblio ModSuggestion ); > use C4::Budgets; > > use Koha::Acquisition::Booksellers; >@@ -111,6 +111,7 @@ my $op = $input->param('op'); > my $suggestionid = $input->param('suggestionid'); > my $duplicateNumber = $input->param('duplicateNumber'); > my $uncertainprice = $input->param('uncertainprice'); >+my $link_order = $input->param('link_order'); > > $op = 'else' unless $op; > >@@ -128,6 +129,28 @@ if ( $op eq 'connectDuplicate' ) { > ConnectSuggestionAndBiblio( $suggestionid, $duplicateNumber ); > } > >+if ( $op eq 'cud-link_order' and $link_order ) { >+ my $order = Koha::Acquisition::Orders->find($link_order); >+ >+ if ( $order->biblionumber ) { >+ ModSuggestion( >+ { >+ suggestionid => $suggestionid, >+ biblionumber => $order->biblionumber, >+ STATUS => 'ORDERED', >+ } >+ ); >+ if ( C4::Context->preference('PlaceHoldsOnOrdersFromSuggestions') ) { >+ my $suggestion = Koha::Suggestions->find($suggestionid); >+ if ($suggestion) { >+ $suggestion->place_hold(); >+ } >+ } >+ } >+ >+ print $input->redirect( "/cgi-bin/koha/acqui/basket.pl?basketno=" . $basketno ); >+} >+ > my $suggestions = [ > Koha::Suggestions->search_limited( > { >@@ -147,6 +170,7 @@ $template->param( > booksellerid => $booksellerid, > name => $vendor->name, > "op_$op" => 1, >+ link_order => $link_order, > ); > > output_html_with_http_headers $input, $cookie, $template->output; >diff --git a/admin/columns_settings.yml b/admin/columns_settings.yml >index f482000815..21e8e8a618 100644 >--- a/admin/columns_settings.yml >+++ b/admin/columns_settings.yml >@@ -736,6 +736,8 @@ modules: > columnname: withdrawn_status > - > columnname: damaged_status >+ - >+ columnname: dateaccessioned > - > columnname: checkouts > - >@@ -1445,6 +1447,8 @@ modules: > columnname: itemtype > - > columnname: status >+ - >+ columnname: notes > > checkouthistory: > checkouthistory-table: >@@ -1453,6 +1457,7 @@ modules: > - columnname: type > cannot_be_toggled: 1 > cannot_be_modified: 1 >+ is_hidden: 1 > - > columnname: date > - >@@ -2580,4 +2585,4 @@ modules: > - > columnname: actions > cannot_be_toggled: 1 >- cannot_be_modified: 1 >\ No newline at end of file >+ cannot_be_modified: 1 >diff --git a/admin/preferences.pl b/admin/preferences.pl >index 34ddca9497..bf67c87e74 100755 >--- a/admin/preferences.pl >+++ b/admin/preferences.pl >@@ -122,6 +122,18 @@ sub _get_chunk { > } > $chunk->{'languages'} = getTranslatedLanguages( $interface, $theme ); > $chunk->{'type'} = 'languages'; >+ } elsif ( $options{'type'} && $options{'type'} eq 'locale-list' ) { >+ >+ # Dynamic locale detection for facet sorting using Koha::I18N >+ require Koha::I18N; >+ my $locales = Koha::I18N::available_locales(); >+ foreach my $locale (@$locales) { >+ if ( $locale->{value} && $value && $locale->{value} eq $value ) { >+ $locale->{selected} = 1; >+ } >+ } >+ $chunk->{'CHOICES'} = $locales; >+ $chunk->{'type'} = 'select'; > } elsif ( $options{'choices'} ) { > my $add_blank; > if ( $options{'choices'} && ref( $options{'choices'} ) eq '' ) { >diff --git a/api/v1/swagger/definitions/item.yaml b/api/v1/swagger/definitions/item.yaml >index fd1bbd11c9..42dcd36e3e 100644 >--- a/api/v1/swagger/definitions/item.yaml >+++ b/api/v1/swagger/definitions/item.yaml >@@ -12,6 +12,7 @@ properties: > - string > - "null" > description: The item's barcode >+ maxLength: 20 > acquisition_date: > type: > - string >@@ -36,6 +37,7 @@ properties: > - string > - "null" > description: Internal library id for the library the item belongs to >+ maxLength: 10 > purchase_price: > type: > - number >@@ -103,11 +105,13 @@ properties: > - string > - "null" > description: Call number for this item >+ maxLength: 255 > coded_location_qualifier: > type: > - string > - "null" > description: Coded location qualifier >+ maxLength: 10 > checkouts_count: > type: > - integer >@@ -148,6 +152,7 @@ properties: > - string > - "null" > description: Library that is currently in possession item >+ maxLength: 10 > timestamp: > type: string > format: date-time >@@ -157,6 +162,7 @@ properties: > - string > - "null" > description: Authorized value for the shelving location for this item >+ maxLength: 80 > permanent_location: > type: > - string >@@ -164,6 +170,7 @@ properties: > description: > Linked to the CART and PROC temporary locations feature, stores the > permanent shelving location >+ maxLength: 80 > checked_out_date: > type: > - string >@@ -177,16 +184,19 @@ properties: > - string > - "null" > description: Classification source used on this item >+ maxLength: 10 > call_number_sort: > type: > - string > - "null" > description: "?" >+ maxLength: 255 > collection_code: > type: > - string > - "null" > description: Authorized value for the collection code associated with this item >+ maxLength: 80 > materials_notes: > type: > - string >@@ -207,6 +217,7 @@ properties: > - string > - "null" > description: Itemtype defining the type for this item >+ maxLength: 10 > effective_item_type_id: > type: > - string >@@ -227,16 +238,19 @@ properties: > - string > - "null" > description: Copy number >+ maxLength: 32 > inventory_number: > type: > - string > - "null" > description: Inventory number >+ maxLength: 80 > new_status: > type: > - string > - "null" > description: "'new' value, whatever free-text information." >+ maxLength: 32 > exclude_from_local_holds_priority: > type: boolean > description: Exclude this item from local holds priority. >diff --git a/api/v1/swagger/definitions/library.yaml b/api/v1/swagger/definitions/library.yaml >index 0fc7967358..9039fbb847 100644 >--- a/api/v1/swagger/definitions/library.yaml >+++ b/api/v1/swagger/definitions/library.yaml >@@ -29,6 +29,7 @@ properties: > - string > - "null" > description: the postal code of the library >+ maxLength: 25 > city: > type: > - string >@@ -84,6 +85,7 @@ properties: > - string > - "null" > description: the IP address for your library or branch >+ maxLength: 15 > notes: > type: > - string >@@ -94,6 +96,7 @@ properties: > - string > - "null" > description: geolocation of your library >+ maxLength: 255 > marc_org_code: > type: > - string >@@ -101,6 +104,7 @@ properties: > description: MARC Organization Code, see > http://www.loc.gov/marc/organizations/orgshome.html, when empty defaults > to syspref MARCOrgCode >+ maxLength: 16 > pickup_location: > type: boolean > description: If the library can act as a pickup location >diff --git a/api/v1/swagger/definitions/patron.yaml b/api/v1/swagger/definitions/patron.yaml >index 4b61ae2d46..72f64fac15 100644 >--- a/api/v1/swagger/definitions/patron.yaml >+++ b/api/v1/swagger/definitions/patron.yaml >@@ -9,6 +9,7 @@ properties: > - string > - "null" > description: library assigned user identifier >+ maxLength: 32 > surname: > type: > - string >@@ -178,9 +179,11 @@ properties: > library_id: > type: string > description: Internal identifier for the patron's home library >+ maxLength: 10 > category_id: > type: string > description: Internal identifier for the patron's category >+ maxLength: 10 > date_enrolled: > type: > - string >@@ -227,16 +230,19 @@ properties: > - string > - "null" > description: used for children to include the relationship to their guarantor >+ maxLength: 100 > gender: > type: > - string > - "null" > description: patron's gender >+ maxLength: 1 > userid: > type: > - string > - "null" > description: patron's login >+ maxLength: 75 > opac_notes: > type: > - string >@@ -247,16 +253,19 @@ properties: > - string > - "null" > description: a note related to patron's alternate address >+ maxLength: 255 > statistics_1: > type: > - string > - "null" > description: a field that can be used for any information unique to the library >+ maxLength: 80 > statistics_2: > type: > - string > - "null" > description: a field that can be used for any information unique to the library >+ maxLength: 80 > autorenew_checkouts: > type: boolean > description: indicate whether auto-renewal is allowed for patron >@@ -311,6 +320,7 @@ properties: > - "null" > description: the mobile phone number where the patron would like to receive notices (if > SMS turned on) >+ maxLength: 50 > sms_provider_id: > type: > - integer >@@ -327,6 +337,10 @@ properties: > description: controls if relatives can see this patron's fines > check_previous_checkout: > type: string >+ enum: >+ - yes >+ - no >+ - inherit > description: produce a warning for this patron if this item has previously been checked > out to this patron if 'yes', not if 'no', defer to category setting if > 'inherit' >@@ -344,6 +358,7 @@ properties: > lang: > type: string > description: lang to use to send notices to this patron >+ maxLength: 25 > login_attempts: > type: > - integer >diff --git a/api/v1/swagger/definitions/patron_category.yaml b/api/v1/swagger/definitions/patron_category.yaml >index 7f2d4a3288..da548a6980 100644 >--- a/api/v1/swagger/definitions/patron_category.yaml >+++ b/api/v1/swagger/definitions/patron_category.yaml >@@ -4,6 +4,7 @@ properties: > patron_category_id: > type: string > description: Internal patron category identifier >+ maxLength: 10 > name: > type: > - string >@@ -56,9 +57,11 @@ properties: > category_type: > type: string > description: Type of Koha patron (Adult, Child, Professional, Organizational, Statistical, Staff) >+ maxLength: 1 > block_expired_patron_opac_actions: > type: string > description: Specific actions expired patrons of this category are blocked from performing. OPAC actions blocked based on the patron category take priority over this preference >+ maxLength: 128 > default_privacy: > type: string > enum: >@@ -68,6 +71,10 @@ properties: > description: Default privacy setting for this patron category > check_prev_checkout: > type: string >+ enum: >+ - yes >+ - no >+ - inherit > description: Produce a warning for this patron category if this item has previously been checked out to this patron if ''yes'', not if ''no'', defer to syspref setting if ''inherit''.' > can_place_ill_in_opac: > type: boolean >diff --git a/api/v1/swagger/paths/holds.yaml b/api/v1/swagger/paths/holds.yaml >index cac6627018..4fe775fb36 100644 >--- a/api/v1/swagger/paths/holds.yaml >+++ b/api/v1/swagger/paths/holds.yaml >@@ -221,6 +221,12 @@ > type: string > enum: > - any >+ - bad_address >+ - card_lost >+ - debt_limit >+ - expired >+ - hold_limit >+ - restricted > collectionFormat: csv > consumes: > - application/json >@@ -247,6 +253,18 @@ > description: Borrower not found > schema: > $ref: "../swagger.yaml#/definitions/error" >+ "409": >+ description: | >+ Conflict. Possible `error_code` attribute values: >+ >+ * `bad_address` >+ * `card_lost` >+ * `debt_limit` >+ * `expired` >+ * `hold_limit` >+ * `restricted` >+ schema: >+ $ref: "../swagger.yaml#/definitions/error" > "500": > description: | > Internal server error. Possible `error_code` attribute values: >diff --git a/catalogue/moredetail.pl b/catalogue/moredetail.pl >index 3e848377bc..78688a925d 100755 >--- a/catalogue/moredetail.pl >+++ b/catalogue/moredetail.pl >@@ -81,9 +81,10 @@ my $title = $query->param('title'); > my $bi = $query->param('bi'); > $bi = $biblionumber unless $bi; > $itemnumber = $query->param('itemnumber'); >-my $data = &GetBiblioData($biblionumber); >-my $dewey = $data->{'dewey'}; >-my $showallitems = $query->param('showallitems'); >+my $data = &GetBiblioData($biblionumber); >+my $dewey = $data->{'dewey'}; >+my $showallitems = $query->param('showallitems'); >+my $withdraw_error = $query->param('nowithdraw'); > > #coping with subscriptions > my $subscriptionsnumber = CountSubscriptionFromBiblionumber($biblionumber); >@@ -325,6 +326,7 @@ $template->param( > ); > > $template->param( >+ withdraw_error => $withdraw_error, > ITEM_DATA => \@item_data, > moredetailview => 1, > loggedinuser => $loggedinuser, >@@ -341,4 +343,3 @@ my $holds = $biblio->holds; > $template->param( holdcount => $holds->count ); > > output_html_with_http_headers $query, $cookie, $template->output; >- >diff --git a/catalogue/updateitem.pl b/catalogue/updateitem.pl >index 350f4b14e4..d02fe66951 100755 >--- a/catalogue/updateitem.pl >+++ b/catalogue/updateitem.pl >@@ -90,9 +90,13 @@ if ( $op eq "cud-set_non_public_note" ) { > print $cgi->redirect("moredetail.pl?biblionumber=$biblionumber&itemnumber=$itemnumber#item$itemnumber"); > exit; > } >- >-$item->store; >- >+eval { $item->store; }; >+if ($@) { >+ my $error_message = $@->message; >+ print $cgi->redirect( >+ "moredetail.pl?biblionumber=$biblionumber&itemnumber=$itemnumber&nowithdraw=$error_message#item$itemnumber"); >+ exit; >+} > LostItem( $itemnumber, 'moredetail' ) if $op eq "cud-set_lost"; > > print $cgi->redirect( >diff --git a/cataloguing/addbiblio.pl b/cataloguing/addbiblio.pl >index 58f555d6de..51fbb1a31d 100755 >--- a/cataloguing/addbiblio.pl >+++ b/cataloguing/addbiblio.pl >@@ -33,7 +33,6 @@ use C4::Biblio qw( > GetMarcStructure > GetUsedMarcStructure > ModBiblio >- prepare_host_field > PrepHostMarcField > TransformHtmlToMarc > ApplyMarcOverlayRules >@@ -645,8 +644,10 @@ if ($hostbiblionumber) { > if ($parentbiblio) { > my $marcflavour = C4::Context->preference('marcflavour'); > $record = MARC::Record->new(); >+ $record->leader(' naa a22 i 4500'); > SetMarcUnicodeFlag( $record, $marcflavour ); >- my $hostfield = prepare_host_field( $parentbiblio, $marcflavour ); >+ my $parent = Koha::Biblios->find($parentbiblio); >+ my $hostfield = $parent->generate_marc_host_field; > if ($hostfield) { > $record->append_fields($hostfield); > } >diff --git a/cataloguing/additem.pl b/cataloguing/additem.pl >index fecd4fa6a8..c1067f44bf 100755 >--- a/cataloguing/additem.pl >+++ b/cataloguing/additem.pl >@@ -50,6 +50,7 @@ use MARC::File::XML; > use MIME::Base64 qw( decode_base64url encode_base64url ); > use Storable qw( freeze thaw ); > use URI::Escape qw( uri_escape_utf8 ); >+use Try::Tiny qw( catch try ); > > our $dbh = C4::Context->dbh; > >@@ -641,7 +642,11 @@ if ( $op eq "cud-additem" ) { > if ( $newitemlost && $newitemlost ge '1' && !$olditemlost ) { > LostItem( $item->itemnumber, 'additem' ); > } >- $item->store; >+ try { >+ $item->store; >+ } catch { >+ push @errors, $_->error; >+ } > } > > $nextop = "cud-additem"; >diff --git a/circ/circulation.pl b/circ/circulation.pl >index 5d432b4f80..52871d12b9 100755 >--- a/circ/circulation.pl >+++ b/circ/circulation.pl >@@ -626,7 +626,7 @@ if ($patron) { > $template->param( is_debarred => 1 ); > $noissues = 1; > } >- if ( $patron->borrowernumber eq C4::Context->preference("AnonymousPatron") ) { >+ if ( $patron->is_anonymous ) { > $template->param( is_anonymous => 1 ); > $noissues = 1; > } >diff --git a/cpanfile b/cpanfile >index c0fc0ef00b..84d6e4ede0 100644 >--- a/cpanfile >+++ b/cpanfile >@@ -107,6 +107,7 @@ requires 'Storable', '2.20'; > requires 'String::Random', '0.22'; > requires 'Template', '>= 2.27, != 3.008'; > requires 'Template::Plugin::HtmlToText', '0.03'; >+requires 'Template::Plugin::JSON', '0.08', > requires 'Template::Plugin::JSON::Escape', '0.02'; > requires 'Term::ANSIColor', '1.1'; > requires 'Test', '1.25'; >diff --git a/cypress.config.ts b/cypress.config.ts >index e90da4e019..b7bba5408c 100644 >--- a/cypress.config.ts >+++ b/cypress.config.ts >@@ -12,16 +12,13 @@ export default defineConfig({ > return require("./t/cypress/plugins/index.js")(on, config); > }, > experimentalStudio: true, >- baseUrl: "http://localhost:8081", >+ baseUrl: process.env.KOHA_INTRANET_URL || "http://localhost:8081", > specPattern: "t/cypress/integration/**/*.*", > supportFile: "t/cypress/support/e2e.js", > env: { >- db: { >- host: "db", >- user: "koha_kohadev", >- password: "password", >- database: "koha_kohadev", >- }, >+ opacBaseUrl: process.env.KOHA_OPAC_URL || "http://localhost:8080", >+ apiUsername: "koha", >+ apiPassword: "koha", > }, > }, > }); >diff --git a/debian/build-git-snapshot b/debian/build-git-snapshot >index 7000abc9eb..a426507fef 100755 >--- a/debian/build-git-snapshot >+++ b/debian/build-git-snapshot >@@ -24,38 +24,45 @@ > use Modern::Perl; > > use Getopt::Long qw(:config no_ignore_case); >-use POSIX qw/strftime/; >+use POSIX qw/strftime/; > > my $basetgz; > my $buildresult; >-my $distribution='squeeze-dev'; >-my $git_checks='all'; >-my $version='16.06~git'; >-my $auto_version=1; >+my $distribution = 'testing'; >+my $git_checks = 'all'; >+my $version = '16.06~git'; >+my $auto_version = 1; >+my $auto_changelog = 1; > my $need_help; >+my $incr; >+my $urgency = 'medium'; > my $debug; > >+my $release_str; >+ > GetOptions( > 'basetgz|b=s' => \$basetgz, >- 'buildresult|r=s' => \$buildresult, >- 'distribution|D=s' => \$distribution, >- 'git-checks|g=s' => \$git_checks, >- 'version|v=s' => \$version, >- 'autoversion!' => \$auto_version, >- 'help|h' => \$need_help, >- 'debug|d' => \$debug, >+ 'buildresult|r=s' => \$buildresult, >+ 'distribution|D=s' => \$distribution, >+ 'git-checks|g=s' => \$git_checks, >+ 'version|v=s' => \$version, >+ 'incr|i=s' => \$incr, >+ 'urgency|u=s' => \$urgency, >+ 'autoversion!' => \$auto_version, >+ 'autochangelog!' => \$auto_changelog, >+ 'help|h' => \$need_help, >+ 'debug|d' => \$debug, > ); > > help_and_exit() if $need_help; > >- > sub sys_command_output { > my ($command) = @_; > > print "$command\n" if $debug; > my $command_output; >- open($command_output, "-|", "$command ") >- or die qq{Cannot execute "$command": $!"}; >+ open( $command_output, "-|", "$command " ) >+ or die qq{Cannot execute "$command": $!"}; > return map { chomp; $_ } <$command_output>; > } > >@@ -70,17 +77,17 @@ sub everything_is_committed { > my $filter; > for ($git_checks) { > $_ eq "none" >- and return 1; >+ and return 1; > > $_ eq "modified" >- and $filter = "no", >- last; >+ and $filter = "no", >+ last; > > $_ eq "all" >- and $filter = "normal", >- last; >+ and $filter = "normal", >+ last; > >- help_and_exit("$0: --git-checks/-g must be one of 'all', 'modified', or 'none'"); >+ help_and_exit("$0: --git-checks/-g must be one of 'all', 'modified', or 'none'"); > } > my $has_changes = grep /^xxx/, sys_command_output("git status --porcelain -u${filter}"); > >@@ -88,13 +95,13 @@ sub everything_is_committed { > } > > sub help_and_exit { >- my $msg = shift; >- if ($msg) { >- print "$msg\n\n"; >+ my $msg = shift; >+ if ($msg) { >+ print "$msg\n\n"; > } > print <<EOH; > This builds Koha deb packages, from a git snapshot. It's not suitable for >-making upstreamable verions, but handy for your own local packages. >+making upstreamable versions, but handy for your own local packages. > > Options: > --buildresult, -r >@@ -102,7 +109,7 @@ Options: > Default is whatever pdebuild uses. > --distribution, -D > the distribution value to set in the changelog when editing it. Default >- is 'squeeze-dev'. >+ is 'testing'. > --git-checks, -g > what level of git checks are run to determine if the working copy is > clean enough. One of 'all' (any changes are bad), 'modified' (only >@@ -110,9 +117,16 @@ Options: > (checking git status is skipped totally.) Default is 'all'. > --version, -v > the version string for the resulting package. Default is '$version'. >+ --urgency, -u >+ the urgency string for the resulting package. Default is '$urgency'. >+ --incr, -i >+ set debian revision (default = '-1') > --(no)autoversion > whether or not to use the date and git commit ID in the version value. > Default is to include it. >+ --(no)autochangelog >+ whether or not to update the debian/changelog file. >+ Default is to update it. > --debug, -d > EOH > exit; >@@ -124,33 +138,39 @@ sub latest_sha1 { > > sub adjust_debian_changelog { > my ($newversion) = @_; >- # debian revision >- $newversion .= "-1"; >+ $newversion .= $incr ? "-$incr" : "-1"; >+ >+ $release_str = "New upstream "; >+ $release_str .= 'SECURITY ' if $urgency eq 'high'; >+ $release_str .= "release ($version)"; > >- sys_command_output( qq{dch --force-distribution -D "$distribution" -v "$newversion" "Building git snapshot."} ); >- sys_command_output( qq{dch -r "Building git snapshot."} ); >+ sys_command_output( >+ qq{dch --urgency $urgency -b --force-distribution -D "$distribution" -v "$newversion" "$release_str"}); >+ sys_command_output(qq{dch -r "$release_str"}); > } > > sub reset_debian_changelog { >- sys_command_output( qq{git checkout -- debian/changelog} ); >+ sys_command_output(qq{git checkout -- debian/changelog}); > } > > sub build_package { > my ($newversion) = @_; >- sys_command_output( qq{git archive --format=tar --prefix="koha-$newversion/" HEAD | gzip -9 > "../koha_$newversion.orig.tar.gz"} ); >+ sys_command_output( >+ qq{git archive --format=tar --prefix="koha-$newversion/" HEAD | gzip -9 > "../koha_$newversion.orig.tar.gz"}); > >- my $pdebuildopts = $buildresult ? "--buildresult $buildresult" : ""; >- my $pdebuildbasetgz = $basetgz ? "-- --use-network yes --basetgz /var/cache/pbuilder/" . $basetgz . ".tgz" : ""; >- sys_command_output_screen( "pdebuild $pdebuildbasetgz $pdebuildopts" ); >+ my $pdebuildopts = $buildresult ? "--buildresult $buildresult" : ""; >+ my $pdebuildbasetgz = $basetgz ? "-- --use-network yes --basetgz /var/cache/pbuilder/" . $basetgz . ".tgz" : ""; >+ sys_command_output_screen("pdebuild --debbuildopts -sa $pdebuildbasetgz $pdebuildopts"); > } > >-everything_is_committed() or die "cannot build: uncommited changes"; >+everything_is_committed() or die "cannot build: uncommitted changes"; > >-my $newversion = $auto_version >- ? sprintf ('%s%s.%s', $version, strftime("+%Y%m%d%H%M%S", localtime), latest_sha1()) >- : $version; >+my $newversion = >+ $auto_version >+ ? sprintf( '%s%s.%s', $version, strftime( "+%Y%m%d%H%M%S", localtime ), latest_sha1() ) >+ : $version; > >-adjust_debian_changelog( $newversion ); >-build_package( $newversion ); >+adjust_debian_changelog($newversion) if $auto_changelog; >+build_package($newversion); > reset_debian_changelog(); > >diff --git a/debian/control b/debian/control >index 9ff48a08aa..1e13743535 100644 >--- a/debian/control >+++ b/debian/control >@@ -137,6 +137,7 @@ Build-Depends: libalgorithm-checkdigits-perl, > libtemplate-plugin-gettext-perl, > libtemplate-plugin-htmltotext-perl, > libtemplate-plugin-json-escape-perl, >+ libtemplate-plugin-json-perl, > libtemplate-plugin-stash-perl, > libtest-deep-perl, > libtest-exception-perl, >@@ -370,6 +371,7 @@ Depends: libalgorithm-checkdigits-perl, > libpdf-table-perl, > libplack-middleware-logwarn-perl, > libplack-middleware-reverseproxy-perl, >+ libpod-coverage-perl, > libreadonly-perl, > libscalar-list-utils-perl, > libschedule-at-perl, >@@ -386,6 +388,7 @@ Depends: libalgorithm-checkdigits-perl, > libtemplate-plugin-gettext-perl, > libtemplate-plugin-htmltotext-perl, > libtemplate-plugin-json-escape-perl, >+ libtemplate-plugin-json-perl, > libtemplate-plugin-stash-perl, > libtest-deep-perl, > libtest-exception-perl, >diff --git a/debian/templates/koha-conf-site.xml.in b/debian/templates/koha-conf-site.xml.in >index 1f034a26c5..afc23a98cc 100644 >--- a/debian/templates/koha-conf-site.xml.in >+++ b/debian/templates/koha-conf-site.xml.in >@@ -474,11 +474,9 @@ __END_SRU_PUBLICSERVER__ > </background_jobs_worker> > > <do_not_remove_cookie>__KEEP_COOKIE__</do_not_remove_cookie> >- <do_not_remove_cookie>catalogue_editor_\d+</do_not_remove_cookie> >- <!-- Uncomment lines like hereunder to not clear cookies at logout: >- The cookie name is case sensitive. >- NOTE: You may use regex constructions like the example above. >- <do_not_remove_cookie>KohaOpacLanguage</do_not_remove_cookie> >+ <!-- Example lines. See Koha/CookieManager.pm for more details. >+ <do_not_remove_cookie>some_cookie</do_not_remove_cookie> >+ <remove_cookie>another_cookie</remove_cookie> > --> > > <message_domain_limits> >diff --git a/debian/templates/zebra-authorities-dom-site.cfg.in b/debian/templates/zebra-authorities-dom-site.cfg.in >index be2e0c3ae6..01d446b8d6 100644 >--- a/debian/templates/zebra-authorities-dom-site.cfg.in >+++ b/debian/templates/zebra-authorities-dom-site.cfg.in >@@ -8,7 +8,7 @@ profilePath:/etc/koha/sites/__KOHASITE__:/etc/koha/zebradb/authorities/etc:/etc/ > > encoding: UTF-8 > # modulePath - where to look for loadable zebra modules >-modulePath: /usr/lib/idzebra-2.0/modules:/usr/lib/x86_64-linux-gnu/idzebra-2.0/modules:/usr/lib/i386-linux-gnu/idzebra-2.0/modules:/usr/lib/aarch64-linux-gnu/idzebra-2.0/modules:/usr/lib/arm-linux-gnueabi/idzebra-2.0/modules:/usr/lib/arm-linux-gnueabihf/idzebra-2.0/modules:/usr/lib/mips-linux-gnu/idzebra-2.0/modules:/usr/lib/mipsel-linux-gnu/idzebra-2.0/modules:/usr/lib/powerpc-linux-gnu/idzebra-2.0/modules:/usr/lib/powerpc64le-linux-gnu/idzebra-2.0/modules:/usr/lib/s390x-linux-gnu/idzebra-2.0/modules >+modulePath: /usr/lib64/idzebra-2.0/modules:/usr/lib/idzebra-2.0/modules:/usr/lib/x86_64-linux-gnu/idzebra-2.0/modules:/usr/lib/i386-linux-gnu/idzebra-2.0/modules:/usr/lib/aarch64-linux-gnu/idzebra-2.0/modules:/usr/lib/arm-linux-gnueabi/idzebra-2.0/modules:/usr/lib/arm-linux-gnueabihf/idzebra-2.0/modules:/usr/lib/mips-linux-gnu/idzebra-2.0/modules:/usr/lib/mipsel-linux-gnu/idzebra-2.0/modules:/usr/lib/powerpc-linux-gnu/idzebra-2.0/modules:/usr/lib/powerpc64le-linux-gnu/idzebra-2.0/modules:/usr/lib/s390x-linux-gnu/idzebra-2.0/modules > > # Files that describe the attribute sets supported. > attset: bib1.att >diff --git a/debian/templates/zebra-biblios-dom-site.cfg.in b/debian/templates/zebra-biblios-dom-site.cfg.in >index 7b5e52c153..29bf9a8473 100644 >--- a/debian/templates/zebra-biblios-dom-site.cfg.in >+++ b/debian/templates/zebra-biblios-dom-site.cfg.in >@@ -5,7 +5,7 @@ > # Where are the config files located? > profilePath:/etc/koha/sites/__KOHASITE__:/etc/koha/zebradb/biblios/etc:/etc/koha/zebradb/etc:/etc/koha/zebradb/marc_defs/__ZEBRA_MARC_FORMAT__/biblios:/etc/koha/zebradb/lang_defs/__ZEBRA_LANGUAGE__:/etc/koha/zebradb/xsl > # modulePath - where to look for loadable zebra modules >-modulePath: /usr/lib/idzebra-2.0/modules:/usr/lib/x86_64-linux-gnu/idzebra-2.0/modules:/usr/lib/i386-linux-gnu/idzebra-2.0/modules:/usr/lib/aarch64-linux-gnu/idzebra-2.0/modules:/usr/lib/arm-linux-gnueabi/idzebra-2.0/modules:/usr/lib/arm-linux-gnueabihf/idzebra-2.0/modules:/usr/lib/mips-linux-gnu/idzebra-2.0/modules:/usr/lib/mipsel-linux-gnu/idzebra-2.0/modules:/usr/lib/powerpc-linux-gnu/idzebra-2.0/modules:/usr/lib/powerpc64le-linux-gnu/idzebra-2.0/modules:/usr/lib/s390x-linux-gnu/idzebra-2.0/modules >+modulePath: /usr/lib64/idzebra-2.0/modules:/usr/lib/idzebra-2.0/modules:/usr/lib/x86_64-linux-gnu/idzebra-2.0/modules:/usr/lib/i386-linux-gnu/idzebra-2.0/modules:/usr/lib/aarch64-linux-gnu/idzebra-2.0/modules:/usr/lib/arm-linux-gnueabi/idzebra-2.0/modules:/usr/lib/arm-linux-gnueabihf/idzebra-2.0/modules:/usr/lib/mips-linux-gnu/idzebra-2.0/modules:/usr/lib/mipsel-linux-gnu/idzebra-2.0/modules:/usr/lib/powerpc-linux-gnu/idzebra-2.0/modules:/usr/lib/powerpc64le-linux-gnu/idzebra-2.0/modules:/usr/lib/s390x-linux-gnu/idzebra-2.0/modules > > encoding: UTF-8 > # Files that describe the attribute sets supported. >diff --git a/docs/teams.yaml b/docs/teams.yaml >index 5233e291ad..37a984fdf2 100644 >--- a/docs/teams.yaml >+++ b/docs/teams.yaml >@@ -573,8 +573,8 @@ team: > translation: > name: Bernardo González Kriegel > packaging: >- name: Mirko Tietgen >- name: Mason James >+ - name: Mirko Tietgen >+ - name: Mason James > ci: > - name: Tomás Cohen Arazi > maintainer: >@@ -624,8 +624,8 @@ team: > translation: > name: Bernardo González Kriegel > packaging: >- name: Mirko Tietgen >- name: Mason James >+ - name: Mirko Tietgen >+ - name: Mason James (17.05) > ci: > - name: Tomás Cohen Arazi > maintainer: >@@ -680,8 +680,8 @@ team: > - name: Indranil Das Gupta > - name: Bernardo González Kriegel > packaging: >- name: Mirko Tietgen >- name: Mason James >+ - name: Mirko Tietgen >+ - name: Mason James > ci: > - name: Tomás Cohen Arazi > - name: Jonathan Druart >@@ -1008,6 +1008,8 @@ team: > name: Owen Leonard > - area: Accounts > name: Martin Renvoize >+ - area: Elasticsearch >+ name: Fridolin Somers > - area: Zebra > name: Fridolin Somers > bugwrangler: >@@ -1027,6 +1029,8 @@ team: > - name: Mason James > ci: > - name: Mason James >+ wiki: >+ - name: Thomas Dukleth > accessibility_advocates: > - name: Henry Bolshaw > - name: Wainui Witika-Park >@@ -1050,16 +1054,16 @@ team: > qa_manager: > name: Katrin Fischer > qa: >- - name: Tomás Cohen Arazi >- - name: Nick Clemens >+ - name: Andrew Nugged > - name: Jonathan Druart >- - name: Victor Grousset >- - name: Kyle M Hall > - name: Joonas Kylmälä >- - name: Andrew Nugged >- - name: Martin Renvoize >+ - name: Kyle M Hall > - name: Marcel de Rooy >+ - name: Martin Renvoize >+ - name: Nick Clemens > - name: Petro Vashchuk >+ - name: Tomás Cohen Arazi >+ - name: Victor Grousset > te: > - area: UI Design > name: Owen Leonard >@@ -1076,11 +1080,11 @@ team: > name: David Nind > documentation_team: > - name: Aude Charillon >- - name: Rocio Lopez >- - name: Kelly McElligott >- - name: Martin Renvoize > - name: Caroline Cyr La Rose >+ - name: Kelly McElligott > - name: Lucy Vaux-Harvey >+ - name: Martin Renvoize >+ - name: Rocio Lopez > meeting_facilitator: > - name: Jessica Zairo > translations: >@@ -1126,6 +1130,7 @@ team: > - name: Marcel de Rooy > - name: Fridolin Somers > - name: Petro Vashchuk >+ - name: David Cook > te: > - area: UI Design > name: Owen Leonard >@@ -1193,9 +1198,7 @@ team: > - area: REST API > name: Martin Renvoize > - area: ERM >- name: Pedro Amorim >- - area: ERM >- name: AgustÃn Moyano >+ name: Pedro Amorim, AgustÃn Moyano > bugwrangler: > - name: Aleisha Amohia > - name: Indranil Das Gupta >@@ -1243,17 +1246,17 @@ team: > name: Katrin Fischer > qa: > - name: Aleisha Amohia >- - name: Nick Clemens >+ - name: Andrii Nugged > - name: David Cook >+ - name: Emily Lamancusa > - name: Jonathan Druart >- - name: Lucas Gass >- - name: Victor Grousset > - name: Kyle M Hall >- - name: Emily Lamancusa >- - name: Andrii Nugged >- - name: Martin Renvoize >+ - name: Lucas Gass > - name: Marcel de Rooy >+ - name: Martin Renvoize >+ - name: Nick Clemens > - name: Petro Vashchuk >+ - name: Victor Grousset > te: > - area: UI Design > name: Owen Leonard >@@ -1363,9 +1366,10 @@ team: > ktd: > - name: Tomás Cohen Arazi > wiki: >- - name: Thomas Dukleth > - name: George Williams >+ - name: Katrin Fischer > - name: Mason James >+ - name: Thomas Dukleth > website: > - name: Liz Rea > newsletter: >@@ -1390,23 +1394,23 @@ team: > qa_manager: > name: Martin Renvoize > qa: >- - name: Marcel de Rooy >- - name: Kyle M Hall >- - name: Emily Lamancusa >- - name: Nick Clemens >- - name: Lucas Gass >- - name: Tomás Cohen Arazi >- - name: Julian Maurice >- - name: Victor Grousset > - name: Aleisha Amohia >+ - name: Chris Cormack > - name: David Cook >- - name: Laura Escamilla >+ - name: Emily Lamancusa > - name: Jonathan Druart >- - name: Pedro Amorim >+ - name: Julian Maurice >+ - name: Kyle M Hall >+ - name: Laura Escamilla >+ - name: Lucas Gass >+ - name: Marcel de Rooy > - name: Matt Blenkinsop >- - name: Thomas Klausner >+ - name: Nick Clemens > - name: Paul Derscheid >- - name: Chris Cormack >+ - name: Pedro Amorim >+ - name: Thomas Klausner >+ - name: Tomás Cohen Arazi >+ - name: Victor Grousset > te: > - area: UI Design > name: Owen Leonard >@@ -1463,33 +1467,38 @@ team: > - version: 24.11 > name: Paul Derscheid > - version: 24.05 >- name: Wainui Witika-Park >+ name: Catalyst (Wainui Witika-Park, Alex Buckley, Aleisha Amoha) > - version: 23.11 > name: Fridolin Somers > - version: 22.11 >- - name: Laura Escamilla >+ name: Jesse Maseto > qa_manager: > name: Martin Renvoize > qa: >- - name: Victor Grousset >- - name: Lisette Scheer >- - name: Emily Lamancusa >+ - name: Aleisha Amohia >+ - name: Baptiste Wojtowski >+ - name: Brendan Lawlor > - name: David Cook >+ - name: David Nind >+ - name: Emily Lamancusa > - name: Jonathan Druart > - name: Julian Maurice >- - name: Baptiste Wojtowski >- - name: Paul Derscheid >- - name: Aleisha Amohia >- - name: Laura Escamilla >- - name: Tomás Cohen Arazi > - name: Kyle M Hall >- - name: Nick Clemens >+ - name: Laura Escamilla >+ - name: Lisette Scheer > - name: Lucas Gass >+ - name: Magnus Enger > - name: Marcel de Rooy >+ - name: Martin Renvoize > - name: Matt Blenkinsop >+ - name: Nick Clemens >+ - name: Owen Leonard >+ - name: Paul Derscheid > - name: Pedro Amorim >- - name: Brendan Lawlor > - name: Thomas Klausner >+ - name: Tomás Cohen Arazi >+ - name: Victor Grousset >+ - name: Wainui Witika-Park > security_manager: > - name: Tomás Cohen Arazi > te: >@@ -1508,7 +1517,7 @@ team: > documentation_team: > - name: Aude Charillon > - name: David Nind >- - name: Caroline Cyr La Rose >+ - name: Caroline Cyr la Rose > translation: > name: Jonathan Druart > meeting_facilitator: >@@ -1531,3 +1540,65 @@ team: > - name: Lauren Purton > newsletter: > name: Michael Kuhn >+ accessibility_advocate: >+ name: David Nind >+ 25.11: >+ release_date: 1763855999 >+ manager: >+ name: Lucas Gass >+ maintainer: >+ - version: 25.05 >+ name: Paul Derscheid >+ - version: 24.11 >+ name: Fridolin Somers >+ - version: 24.05 >+ name: Jesse Maseto >+ - version: 22.11 >+ name: "Catalyst IT (Wainui, Alex, Aleisha)" >+ maintainer_assistants: >+ - version: 25.05 >+ name: Martin Renvoize >+ - version: 24.11 >+ name: Baptiste Wojtkowski >+ - version: 24.05 >+ name: Laura Escamilla >+ qa_manager: >+ name: Martin Renvoize >+ qa: >+ - name: Andrew Fuerste-Henry >+ - name: Andrii Nugged >+ - name: Baptiste Wojtkowski >+ - name: Brendan Lawlor >+ - name: David Cook >+ - name: Emily Lamancusa >+ - name: Jonathan Druart >+ - name: Julian Maurice >+ - name: Kyle Hall >+ - name: Laura Escamilla >+ - name: Lisette Scheer >+ - name: Marcel de Rooy >+ - name: Nick Clemens >+ - name: Paul Derscheid >+ - name: Petro V >+ - name: Tomás Cohen Arazi >+ - name: Victor Grousset >+ documentation: >+ name: David Nind >+ documentation_team: >+ - name: Aude Charillon >+ - name: Caroline Cyr La Rose >+ - name: Donna Bachowski >+ - name: Heather Hernandez >+ - name: Kristi Krueger >+ - name: Philip Orr >+ translation: >+ name: Jonathan Druart >+ website: >+ name: David Nind >+ wiki: >+ - name: George Williams >+ - name: Thomas Dukleth >+ social_media: >+ name: David Nind >+ newsletter: >+ name: Michael Kuhn >diff --git a/etc/koha-conf.xml b/etc/koha-conf.xml >index b162219f9c..33768161a2 100644 >--- a/etc/koha-conf.xml >+++ b/etc/koha-conf.xml >@@ -285,11 +285,9 @@ > <max_processes>1</max_processes> > </background_jobs_worker> > >- <do_not_remove_cookie>catalogue_editor_\d+</do_not_remove_cookie> >- <!-- Uncomment lines like hereunder to not clear cookies at logout: >- The cookie name is case sensitive. >- NOTE: You may use regex constructions like the example above. >- <do_not_remove_cookie>KohaOpacLanguage</do_not_remove_cookie> >+ <!-- Example lines. See Koha/CookieManager.pm for more details. >+ <do_not_remove_cookie>some_cookie</do_not_remove_cookie> >+ <remove_cookie>another_cookie</remove_cookie> > --> > > <message_domain_limits> >diff --git a/etc/zebradb/zebra-authorities-dom.cfg b/etc/zebradb/zebra-authorities-dom.cfg >index f3b267b4fe..f1b5a6a9e1 100644 >--- a/etc/zebradb/zebra-authorities-dom.cfg >+++ b/etc/zebradb/zebra-authorities-dom.cfg >@@ -8,7 +8,7 @@ profilePath:__ZEBRA_CONF_DIR__/authorities/etc:__ZEBRA_CONF_DIR__/etc:__ZEBRA_CO > > encoding: UTF-8 > # modulePath - where to look for loadable zebra modules >-modulePath: /usr/lib/idzebra-2.0/modules:/usr/lib64/idzebra-2.0/modules:/usr/lib/x86_64-linux-gnu/idzebra-2.0/modules/ >+modulePath: /usr/lib64/idzebra-2.0/modules:/usr/lib/idzebra-2.0/modules:/usr/lib/x86_64-linux-gnu/idzebra-2.0/modules:/usr/lib/i386-linux-gnu/idzebra-2.0/modules:/usr/lib/aarch64-linux-gnu/idzebra-2.0/modules:/usr/lib/arm-linux-gnueabi/idzebra-2.0/modules:/usr/lib/arm-linux-gnueabihf/idzebra-2.0/modules:/usr/lib/mips-linux-gnu/idzebra-2.0/modules:/usr/lib/mipsel-linux-gnu/idzebra-2.0/modules:/usr/lib/powerpc-linux-gnu/idzebra-2.0/modules:/usr/lib/powerpc64le-linux-gnu/idzebra-2.0/modules:/usr/lib/s390x-linux-gnu/idzebra-2.0/modules > > # Files that describe the attribute sets supported. > attset: bib1.att >diff --git a/etc/zebradb/zebra-biblios-dom.cfg b/etc/zebradb/zebra-biblios-dom.cfg >index 76ed216990..494666e82c 100644 >--- a/etc/zebradb/zebra-biblios-dom.cfg >+++ b/etc/zebradb/zebra-biblios-dom.cfg >@@ -5,7 +5,7 @@ > # Where are the config files located? > profilePath:__ZEBRA_CONF_DIR__/biblios/etc:__ZEBRA_CONF_DIR__/etc:__ZEBRA_CONF_DIR__/marc_defs/__ZEBRA_MARC_FORMAT__/biblios:__ZEBRA_CONF_DIR__/lang_defs/__ZEBRA_LANGUAGE__:__ZEBRA_CONF_DIR__/xsl > # modulePath - where to look for loadable zebra modules >-modulePath: /usr/lib/idzebra-2.0/modules:/usr/lib64/idzebra-2.0/modules:/usr/lib/x86_64-linux-gnu/idzebra-2.0/modules/ >+modulePath: /usr/lib64/idzebra-2.0/modules:/usr/lib/idzebra-2.0/modules:/usr/lib/x86_64-linux-gnu/idzebra-2.0/modules:/usr/lib/i386-linux-gnu/idzebra-2.0/modules:/usr/lib/aarch64-linux-gnu/idzebra-2.0/modules:/usr/lib/arm-linux-gnueabi/idzebra-2.0/modules:/usr/lib/arm-linux-gnueabihf/idzebra-2.0/modules:/usr/lib/mips-linux-gnu/idzebra-2.0/modules:/usr/lib/mipsel-linux-gnu/idzebra-2.0/modules:/usr/lib/powerpc-linux-gnu/idzebra-2.0/modules:/usr/lib/powerpc64le-linux-gnu/idzebra-2.0/modules:/usr/lib/s390x-linux-gnu/idzebra-2.0/modules > > encoding: UTF-8 > # Files that describe the attribute sets supported. >diff --git a/ill/ill-requests.pl b/ill/ill-requests.pl >index 0db9c09b1e..8bb8c255c6 100755 >--- a/ill/ill-requests.pl >+++ b/ill/ill-requests.pl >@@ -117,7 +117,7 @@ if ($backends_available) { > $template->param( > notices => $notices, > request => $request, >- ( $params->{tran_error} ? ( tran_error => $params->{tran_error} ) : () ), >+ ( $params->{tran_fail} ? ( tran_fail => $params->{tran_fail} ) : () ), > ( $params->{tran_success} ? ( tran_success => $params->{tran_success} ) : () ), > ); > >@@ -478,7 +478,7 @@ if ($backends_available) { > $append .= '&tran_success=' . join( ',', @{ $ret->{result}->{success} } ); > } > if ( $ret->{result} && scalar @{ $ret->{result}->{fail} } > 0 ) { >- $append .= '&tran_fail=' . join( ',', @{ $ret->{result}->{fail} } . join(',') ); >+ $append .= '&tran_fail=' . join( ',', @{ $ret->{result}->{fail} } ); > } > > # Redirect to view the whole request >diff --git a/installer/data/mysql/db_revs/250600001.pl b/installer/data/mysql/db_revs/250600001.pl >new file mode 100755 >index 0000000000..f279bd5021 >--- /dev/null >+++ b/installer/data/mysql/db_revs/250600001.pl >@@ -0,0 +1,20 @@ >+use Modern::Perl; >+use Koha::Installer::Output qw(say_warning say_success say_info); >+ >+return { >+ bug_number => "23010", >+ description => "Add new PreventWithdrawingItemsStatus system preference", >+ up => sub { >+ my ($args) = @_; >+ my ( $dbh, $out ) = @$args{qw(dbh out)}; >+ >+ $dbh->do( >+ q{ >+ INSERT IGNORE INTO systempreferences (`variable`,`value`,`options`,`explanation`,`type`) >+ VALUES ('PreventWithdrawingItemsStatus','','','Prevent the withdrawing of items based on statuses','multiple') >+ } >+ ); >+ >+ say_success( $out, "Added new system preference 'PreventWithdrawingItemsStatus'" ); >+ }, >+}; >diff --git a/installer/data/mysql/db_revs/250600002.pl b/installer/data/mysql/db_revs/250600002.pl >new file mode 100755 >index 0000000000..078ce54072 >--- /dev/null >+++ b/installer/data/mysql/db_revs/250600002.pl >@@ -0,0 +1,40 @@ >+use Modern::Perl; >+ >+return { >+ bug_number => "34563", >+ description => "Move StaffReportsHomeHTML to HTML customizations", >+ up => sub { >+ my ($args) = @_; >+ my ( $dbh, $out ) = @$args{qw(dbh out)}; >+ >+ # Get any existing value from the IntranetReportsHomeHTML system preference >+ my ($staffreportshome) = $dbh->selectrow_array( >+ q| >+ SELECT value FROM systempreferences WHERE variable='IntranetReportsHomeHTML'; >+ | >+ ); >+ if ($staffreportshome) { >+ >+ $dbh->do( >+ "INSERT INTO additional_contents ( category, code, location, branchcode, published_on ) VALUES ('html_customizations', 'StaffReportsHome', 'StaffReportsHome', NULL, CAST(NOW() AS date) )" >+ ); >+ >+ my ($insert_id) = $dbh->selectrow_array( >+ "SELECT id FROM additional_contents WHERE category = 'html_customizations' AND code = 'StaffReportsHome' AND location = 'StaffReportsHome' LIMIT 1", >+ {} >+ ); >+ >+ $dbh->do( >+ "INSERT INTO additional_contents_localizations ( additional_content_id, title, content, lang ) VALUES ( ?, 'StaffReportsHome default', ?, 'default' )", >+ undef, $insert_id, $staffreportshome >+ ); >+ >+ say $out "Added 'StaffReportsHome' HTML customization"; >+ } >+ >+ # Remove old system preference >+ $dbh->do("DELETE FROM systempreferences WHERE variable='IntranetReportsHomeHTML'"); >+ say $out "Removed system preference 'IntranetReportsHomeHTML'"; >+ >+ }, >+}; >diff --git a/installer/data/mysql/db_revs/250600003.pl b/installer/data/mysql/db_revs/250600003.pl >new file mode 100755 >index 0000000000..951097d56d >--- /dev/null >+++ b/installer/data/mysql/db_revs/250600003.pl >@@ -0,0 +1,32 @@ >+use Modern::Perl; >+use Koha::Installer::Output qw(say_warning say_success say_info); >+ >+return { >+ bug_number => "40337", >+ description => "Define checkprevcheckout as ENUM", >+ up => sub { >+ my ($args) = @_; >+ my ( $dbh, $out ) = @$args{qw(dbh out)}; >+ >+ $dbh->do( >+ q{ >+ ALTER TABLE borrowers >+ MODIFY COLUMN `checkprevcheckout` enum('yes', 'no', 'inherit') NOT NULL DEFAULT 'inherit' COMMENT 'produce a warning for this patron if this item has previously been checked out to this patron if ''yes'', not if ''no'', defer to category setting if ''inherit''.' >+ } >+ ); >+ >+ $dbh->do( >+ q{ >+ ALTER TABLE categories >+ MODIFY COLUMN `checkprevcheckout` enum('yes', 'no', 'inherit') NOT NULL DEFAULT 'inherit' COMMENT 'produce a warning for this patron category if this item has previously been checked out to this patron if ''yes'', not if ''no'', defer to syspref setting if ''inherit''.' >+ } >+ ); >+ >+ $dbh->do( >+ q{ >+ ALTER TABLE deletedborrowers >+ MODIFY COLUMN `checkprevcheckout` enum('yes', 'no', 'inherit') NOT NULL DEFAULT 'inherit' COMMENT 'produce a warning for this patron if this item has previously been checked out to this patron if ''yes'', not if ''no'', defer to category setting if ''inherit''.' >+ } >+ ); >+ }, >+}; >diff --git a/installer/data/mysql/db_revs/250600004.pl b/installer/data/mysql/db_revs/250600004.pl >new file mode 100755 >index 0000000000..7f871c7388 >--- /dev/null >+++ b/installer/data/mysql/db_revs/250600004.pl >@@ -0,0 +1,22 @@ >+use Modern::Perl; >+use Koha::Installer::Output qw(say_warning say_success say_info); >+ >+return { >+ bug_number => "36947", >+ description => "Add FacetSortingLocale system preference for locale-based facet sorting", >+ up => sub { >+ my ($args) = @_; >+ my ( $dbh, $out ) = @$args{qw(dbh out)}; >+ >+ $dbh->do( >+ q{ >+ INSERT IGNORE INTO systempreferences (`variable`,`value`,`options`,`explanation`,`type`) >+ VALUES ('FacetSortingLocale','default','', >+ 'Choose the locale for sorting facet names when FacetOrder is set to Alphabetical. This enables proper Unicode-aware sorting of accented characters and locale-specific alphabetical ordering.', >+ 'Choice') >+ } >+ ); >+ >+ say_success( $out, "Added new system preference 'FacetSortingLocale'" ); >+ }, >+}; >diff --git a/installer/data/mysql/en/optional/sample_news.yml b/installer/data/mysql/en/optional/sample_news.yml >index 988d927005..39432d9d7d 100644 >--- a/installer/data/mysql/en/optional/sample_news.yml >+++ b/installer/data/mysql/en/optional/sample_news.yml >@@ -80,12 +80,12 @@ tables: > content: > - "Now that you've installed Koha, what's next? Here are some suggestions:" > - "<ul>" >- - "<li><a href=\"http://koha-community.org/documentation/\">Read Koha Documentation</a></li>" >- - "<li><a href=\"http://wiki.koha-community.org\">Read/Write to the Koha Wiki</a></li>" >- - "<li><a href=\"http://koha-community.org/support/\">Read and Contribute to Discussions</a></li>" >- - "<li><a href=\"http://bugs.koha-community.org\">Report Koha Bugs</a></li>" >- - "<li><a href=\"http://wiki.koha-community.org/wiki/Version_Control_Using_Git\">Submit Patches to Koha using Git (Version Control System)</a></li>" >- - "<li><a href=\"http://koha-community.org/support/\">Chat with Koha users and developers</a></li>" >+ - "<li><a href=\"https://koha-community.org/documentation/\">Read Koha Documentation</a></li>" >+ - "<li><a href=\"https://wiki.koha-community.org\">Read/Write to the Koha Wiki</a></li>" >+ - "<li><a href=\"https://koha-community.org/support/\">Read and Contribute to Discussions</a></li>" >+ - "<li><a href=\"https://bugs.koha-community.org\">Report Koha Bugs</a></li>" >+ - "<li><a href=\"https://wiki.koha-community.org/wiki/Version_Control_Using_Git\">Submit Patches to Koha using Git (Version Control System)</a></li>" >+ - "<li><a href=\"https://koha-community.org/support/\">Chat with Koha users and developers</a></li>" > - "</ul>" > - "" > lang: "default" >diff --git a/installer/data/mysql/kohastructure.sql b/installer/data/mysql/kohastructure.sql >index 5d7139383c..a9537d2150 100644 >--- a/installer/data/mysql/kohastructure.sql >+++ b/installer/data/mysql/kohastructure.sql >@@ -1598,7 +1598,7 @@ CREATE TABLE `borrowers` ( > `privacy` int(11) NOT NULL DEFAULT 1 COMMENT 'patron/borrower''s privacy settings related to their checkout history', > `privacy_guarantor_fines` tinyint(1) NOT NULL DEFAULT 0 COMMENT 'controls if relatives can see this patron''s fines', > `privacy_guarantor_checkouts` tinyint(1) NOT NULL DEFAULT 0 COMMENT 'controls if relatives can see this patron''s checkouts', >- `checkprevcheckout` varchar(7) NOT NULL DEFAULT 'inherit' COMMENT 'produce a warning for this patron if this item has previously been checked out to this patron if ''yes'', not if ''no'', defer to category setting if ''inherit''.', >+ `checkprevcheckout` enum('yes', 'no', 'inherit') NOT NULL DEFAULT 'inherit' COMMENT 'produce a warning for this patron if this item has previously been checked out to this patron if ''yes'', not if ''no'', defer to category setting if ''inherit''.', > `updated_on` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp() COMMENT 'time of last change could be useful for synchronization with external systems (among others)', > `lastseen` datetime DEFAULT NULL COMMENT 'last time a patron has been seen (connected at the OPAC or staff interface)', > `lang` varchar(25) NOT NULL DEFAULT 'default' COMMENT 'lang to use to send notices to this patron', >@@ -1808,7 +1808,7 @@ CREATE TABLE `categories` ( > `category_type` varchar(1) NOT NULL DEFAULT 'A' COMMENT 'type of Koha patron (Adult, Child, Professional, Organizational, Statistical, Staff)', > `BlockExpiredPatronOpacActions` varchar(128) NOT NULL DEFAULT 'follow_syspref_BlockExpiredPatronOpacActions' COMMENT 'specific actions expired patrons of this category are blocked from performing or if the BlockExpiredPatronOpacActions system preference is to be followed', > `default_privacy` enum('default','never','forever') NOT NULL DEFAULT 'default' COMMENT 'Default privacy setting for this patron category', >- `checkprevcheckout` varchar(7) NOT NULL DEFAULT 'inherit' COMMENT 'produce a warning for this patron category if this item has previously been checked out to this patron if ''yes'', not if ''no'', defer to syspref setting if ''inherit''.', >+ `checkprevcheckout` enum('yes', 'no', 'inherit') NOT NULL DEFAULT 'inherit' COMMENT 'produce a warning for this patron category if this item has previously been checked out to this patron if ''yes'', not if ''no'', defer to syspref setting if ''inherit''.', > `can_place_ill_in_opac` tinyint(1) NOT NULL DEFAULT 1 COMMENT 'can this patron category place interlibrary loan requests', > `can_be_guarantee` tinyint(1) NOT NULL DEFAULT 0 COMMENT 'if patrons of this category can be guarantees', > `reset_password` tinyint(1) DEFAULT NULL COMMENT 'if patrons of this category can do the password reset flow,', >@@ -2760,7 +2760,7 @@ CREATE TABLE `deletedborrowers` ( > `privacy` int(11) NOT NULL DEFAULT 1 COMMENT 'patron/borrower''s privacy settings related to their checkout history KEY `borrowernumber` (`borrowernumber`),', > `privacy_guarantor_fines` tinyint(1) NOT NULL DEFAULT 0 COMMENT 'controls if relatives can see this patron''s fines', > `privacy_guarantor_checkouts` tinyint(1) NOT NULL DEFAULT 0 COMMENT 'controls if relatives can see this patron''s checkouts', >- `checkprevcheckout` varchar(7) NOT NULL DEFAULT 'inherit' COMMENT 'produce a warning for this patron if this item has previously been checked out to this patron if ''yes'', not if ''no'', defer to category setting if ''inherit''.', >+ `checkprevcheckout` enum('yes', 'no', 'inherit') NOT NULL DEFAULT 'inherit' COMMENT 'produce a warning for this patron if this item has previously been checked out to this patron if ''yes'', not if ''no'', defer to category setting if ''inherit''.', > `updated_on` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp() COMMENT 'time of last change could be useful for synchronization with external systems (among others)', > `lastseen` datetime DEFAULT NULL COMMENT 'last time a patron has been seen (connected at the OPAC or staff interface)', > `lang` varchar(25) NOT NULL DEFAULT 'default' COMMENT 'lang to use to send notices to this patron', >@@ -5772,6 +5772,19 @@ CREATE TABLE `saved_reports` ( > ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; > /*!40101 SET character_set_client = @saved_cs_client */; > >+DROP TABLE IF EXISTS `reports_branches`; >+/*!40101 SET @saved_cs_client = @@character_set_client */; >+/*!40101 SET character_set_client = utf8mb4 */; >+CREATE TABLE `reports_branches` ( >+ `report_id` int(11) NOT NULL, >+ `branchcode` varchar(10) NOT NULL, >+ KEY `report_id` (`report_id`), >+ KEY `branchcode` (`branchcode`), >+ CONSTRAINT `reports_branches_ibfk_1` FOREIGN KEY (`report_id`) REFERENCES `saved_sql` (`id`) ON DELETE CASCADE, >+ CONSTRAINT `reports_branches_ibfk_2` FOREIGN KEY (`branchcode`) REFERENCES `branches` (`branchcode`) ON DELETE CASCADE >+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; >+/*!40101 SET character_set_client = @saved_cs_client */; >+ > -- > -- Table structure for table `saved_sql` > -- >diff --git a/installer/data/mysql/mandatory/sysprefs.sql b/installer/data/mysql/mandatory/sysprefs.sql >index 09b8afd6fd..f2b6d940cc 100644 >--- a/installer/data/mysql/mandatory/sysprefs.sql >+++ b/installer/data/mysql/mandatory/sysprefs.sql >@@ -278,6 +278,7 @@ INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, ` > ('FacetLabelTruncationLength','20',NULL,'Specify the facet max length in OPAC','Integer'), > ('FacetMaxCount','20',NULL,'Specify the max facet count for each category','Integer'), > ('FacetOrder','Alphabetical','Alphabetical|Usage','Specify the order of facets within each category','Choice'), >+('FacetSortingLocale','default','','Choose the locale for sorting facet names when FacetOrder is set to Alphabetical. This enables proper Unicode-aware sorting of accented characters and locale-specific alphabetical ordering.','Choice'), > ('FailedLoginAttempts','','','Number of login attempts before lockout the patron account','Integer'), > ('FallbackToSMSIfNoEmail', 0, 'Enable|Disable', 'Send messages by SMS if no patron email is defined', 'YesNo'), > ('FeeOnChangePatronCategory','1','','If set, when a patron changes to a category with enrolment fee, a fee is charged','YesNo'), >@@ -357,7 +358,6 @@ INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, ` > ('IntranetNumbersPreferPhrase','0',NULL,'Control the use of phr operator in callnumber and standard number staff interface searches','YesNo'), > ('intranetreadinghistory','1','','If ON, Checkout history is enabled for all patrons','YesNo'), > ('IntranetReadingHistoryHolds', 1, '', 'If ON, Holds history is enabled for all patrons','YesNo'), >-('IntranetReportsHomeHTML', '', NULL, 'Show the following HTML in a div on the bottom of the reports home page', 'Free'), > ('IntranetSlipPrinterJS','','','Use this JavaScript for printing slips. Define at least function printThenClose(). For use e.g. with Firefox PlugIn jsPrintSetup, see http://jsprintsetup.mozdev.org/','Free'), > ('intranetstylesheet','','50','Enter a complete URL to use an alternate layout stylesheet in Intranet','free'), > ('IntranetUserCSS','',NULL,'Add CSS to be included in the intranet in an embedded <style> tag.','free'), >@@ -507,7 +507,7 @@ INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, ` > ('OPACFineNoRenewalsIncludeCredits','1',NULL,'If enabled the value specified in OPACFineNoRenewals should include any unapplied account credits in the calculation','YesNo'), > ('OPACFinesTab','1','','If OFF the patron fines tab in the OPAC is disabled.','YesNo'), > ('OPACFRBRizeEditions','0','','If ON, the OPAC will query one or more ISBN web services for associated ISBNs and display an Editions tab on the details pages','YesNo'), >-('OpacHiddenItems','','','This syspref allows to define custom rules for hiding specific items at the OPAC. See http://wiki.koha-community.org/wiki/OpacHiddenItems for more information.','Textarea'), >+('OpacHiddenItems','','','This syspref allows to define custom rules for hiding specific items at the OPAC. See https://wiki.koha-community.org/wiki/OpacHiddenItems for more information.','Textarea'), > ('OpacHiddenItemsExceptions','',NULL,'List of borrower categories, separated by comma, that can see items otherwise hidden by OpacHiddenItems','Textarea'), > ('OpacHiddenItemsHidesRecord','1','','Hide biblio record when all its items are hidden because of OpacHiddenItems','YesNo'), > ('OpacHighlightedWords','1','','If Set, then queried words are higlighted in OPAC','YesNo'), >@@ -645,6 +645,7 @@ INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, ` > ('PreservationNotForLoanDefaultTrainIn', '', '', 'Not for loan to apply to items removed from the preservation waiting list', 'TextArea'), > ('PreservationNotForLoanWaitingListIn', '', '', 'Not for loan to apply to items added to the preservation waiting list', 'TextArea'), > ('PreserveSerialNotes','1','','When a new "Expected" issue is generated, should it be prefilled with last created issue notes?','YesNo'), >+('PreventWithdrawingItemsStatus', '', '', 'Prevent the withdrawing of items based on certain statuses' , 'multiple'), > ('previousIssuesDefaultSortOrder','asc','asc|desc','Specify the sort order of Previous Issues on the circulation page','Choice'), > ('PrintNoticesMaxLines','0','','If greater than 0, sets the maximum number of lines an overdue notice will print. If the number of items is greater than this number, the notice will end with a warning asking the borrower to check their online account for a full list of overdue items.','Integer'), > ('PrivacyPolicyConsent','','Enforced|Permissive|Disabled','Data privacy policy consent in the OPAC', 'Choice'), >diff --git a/installer/data/mysql/updatedatabase.pl b/installer/data/mysql/updatedatabase.pl >index adbd5e4c50..b234d7cfad 100755 >--- a/installer/data/mysql/updatedatabase.pl >+++ b/installer/data/mysql/updatedatabase.pl >@@ -29455,6 +29455,22 @@ if ( CheckVersion($DBversion) ) { > NewVersion( $DBversion, 28108, "Add new systempreference OpacHiddenItemsHidesRecord" ); > } > >+$DBversion = '25.06.00.003'; >+if ( C4::Context->preference("Version") < TransformToNum($DBversion) ) { >+ $dbh->do( >+ "CREATE TABLE `reports_branches` ( >+ `report_id` int(11) NOT NULL, >+ `branchcode` varchar(10) NOT NULL, >+ KEY `report_id` (`report_id`), >+ KEY `branchcode` (`branchcode`), >+ CONSTRAINT `reports_branches_ibfk_1` FOREIGN KEY (`report_id`) REFERENCES `saved_sql` (`id`) ON DELETE CASCADE, >+ CONSTRAINT `reports_branches_ibfk_2` FOREIGN KEY (`branchcode`) REFERENCES `branches` (`branchcode`) ON DELETE CASCADE >+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;" >+ ); >+ print "Upgrade to $DBversion done (reports_branches added)\n"; >+ SetVersion($DBversion); >+} >+ > $DBversion = '21.05.00.000'; > if ( CheckVersion($DBversion) ) { > NewVersion( $DBversion, "", "Koha 21.05.00 release" ); >diff --git a/koha-tmpl/intranet-tmpl/lib/jsdiff/jsdiff.js b/koha-tmpl/intranet-tmpl/lib/jsdiff/jsdiff.js >index 3e3cc47c23..4df57d4eeb 100644 >--- a/koha-tmpl/intranet-tmpl/lib/jsdiff/jsdiff.js >+++ b/koha-tmpl/intranet-tmpl/lib/jsdiff/jsdiff.js >@@ -59,7 +59,7 @@ function diffString( o, n ) { > for (n = out.n[i].row + 1; n < out.o.length && out.o[n].text == null; n++ ) { > pre += '<del>' + escape(out.o[n]) + oSpace[n] + "</del>"; > } >- str += " " + out.n[i].text + nSpace[i] + pre; >+ str += " " + escape(out.n[i].text) + nSpace[i] + pre; > } > } > } >diff --git a/koha-tmpl/intranet-tmpl/lib/jsdiff/jsdiff.min.js b/koha-tmpl/intranet-tmpl/lib/jsdiff/jsdiff.min.js >index 9420f1a67c..f2a45290d5 100644 >--- a/koha-tmpl/intranet-tmpl/lib/jsdiff/jsdiff.min.js >+++ b/koha-tmpl/intranet-tmpl/lib/jsdiff/jsdiff.min.js >@@ -7,4 +7,4 @@ > * > * More Info: > * http://ejohn.org/projects/javascript-diff-algorithm/ >- */function escape(a){var b=a;return b=b.replace(/&/g,"&"),b=b.replace(/</g,"<"),b=b.replace(/>/g,">"),b=b.replace(/"/g,"""),b}function diffString(a,b){a=a.replace(/\s+$/,""),b=b.replace(/\s+$/,"");var c=diff(a==""?[]:a.split(/\s+/),b==""?[]:b.split(/\s+/)),d="",e=a.match(/\s+/g);e==null?e=["\n"]:e.push("\n");var f=b.match(/\s+/g);f==null?f=["\n"]:f.push("\n");if(c.n.length==0)for(var g=0;g<c.o.length;g++)d+="<del>"+escape(c.o[g])+e[g]+"</del>";else{if(c.n[0].text==null)for(b=0;b<c.o.length&&c.o[b].text==null;b++)d+="<del>"+escape(c.o[b])+e[b]+"</del>";for(var g=0;g<c.n.length;g++)if(c.n[g].text==null)d+="<ins>"+escape(c.n[g])+f[g]+"</ins>";else{var h="";for(b=c.n[g].row+1;b<c.o.length&&c.o[b].text==null;b++)h+="<del>"+escape(c.o[b])+e[b]+"</del>";d+=" "+c.n[g].text+f[g]+h}}return d}function randomColor(){return"rgb("+Math.random()*100+"%, "+Math.random()*100+"%, "+Math.random()*100+"%)"}function diffString2(a,b){a=a.replace(/\s+$/,""),b=b.replace(/\s+$/,"");var c=diff(a==""?[]:a.split(/\s+/),b==""?[]:b.split(/\s+/)),d=a.match(/\s+/g);d==null?d=["\n"]:d.push("\n");var e=b.match(/\s+/g);e==null?e=["\n"]:e.push("\n");var f="",g=new Array;for(var h=0;h<c.o.length;h++)g[h]=randomColor(),c.o[h].text!=null?f+='<span style="background-color: '+g[h]+'">'+escape(c.o[h].text)+d[h]+"</span>":f+="<del>"+escape(c.o[h])+d[h]+"</del>";var i="";for(var h=0;h<c.n.length;h++)c.n[h].text!=null?i+='<span style="background-color: '+g[c.n[h].row]+'">'+escape(c.n[h].text)+e[h]+"</span>":i+="<ins>"+escape(c.n[h])+e[h]+"</ins>";return{o:f,n:i}}function diff(a,b){var c=new Object,d=new Object;for(var e=0;e<b.length;e++)c[b[e]]==null&&(c[b[e]]={rows:new Array,o:null}),c[b[e]].rows.push(e);for(var e=0;e<a.length;e++)d[a[e]]==null&&(d[a[e]]={rows:new Array,n:null}),d[a[e]].rows.push(e);for(var e in c)c[e].rows.length==1&&typeof d[e]!="undefined"&&d[e].rows.length==1&&(b[c[e].rows[0]]={text:b[c[e].rows[0]],row:d[e].rows[0]},a[d[e].rows[0]]={text:a[d[e].rows[0]],row:c[e].rows[0]});for(var e=0;e<b.length-1;e++)b[e].text!=null&&b[e+1].text==null&&b[e].row+1<a.length&&a[b[e].row+1].text==null&&b[e+1]==a[b[e].row+1]&&(b[e+1]={text:b[e+1],row:b[e].row+1},a[b[e].row+1]={text:a[b[e].row+1],row:e+1});for(var e=b.length-1;e>0;e--)b[e].text!=null&&b[e-1].text==null&&b[e].row>0&&a[b[e].row-1].text==null&&b[e-1]==a[b[e].row-1]&&(b[e-1]={text:b[e-1],row:b[e].row-1},a[b[e].row-1]={text:a[b[e].row-1],row:e-1});return{o:a,n:b}}; >\ No newline at end of file >+ */function escape(a){var b=a;return b=b.replace(/&/g,"&"),b=b.replace(/</g,"<"),b=b.replace(/>/g,">"),b=b.replace(/"/g,"""),b}function diffString(a,b){a=a.replace(/\s+$/,""),b=b.replace(/\s+$/,"");var c=diff(a==""?[]:a.split(/\s+/),b==""?[]:b.split(/\s+/)),d="",e=a.match(/\s+/g);e==null?e=["\n"]:e.push("\n");var f=b.match(/\s+/g);f==null?f=["\n"]:f.push("\n");if(c.n.length==0)for(var g=0;g<c.o.length;g++)d+="<del>"+escape(c.o[g])+e[g]+"</del>";else{if(c.n[0].text==null)for(b=0;b<c.o.length&&c.o[b].text==null;b++)d+="<del>"+escape(c.o[b])+e[b]+"</del>";for(var g=0;g<c.n.length;g++)if(c.n[g].text==null)d+="<ins>"+escape(c.n[g])+f[g]+"</ins>";else{var h="";for(b=c.n[g].row+1;b<c.o.length&&c.o[b].text==null;b++)h+="<del>"+escape(c.o[b])+e[b]+"</del>";d+=" "+escape(c.n[g].text)+f[g]+h}}return d}function randomColor(){return"rgb("+Math.random()*100+"%, "+Math.random()*100+"%, "+Math.random()*100+"%)"}function diffString2(a,b){a=a.replace(/\s+$/,""),b=b.replace(/\s+$/,"");var c=diff(a==""?[]:a.split(/\s+/),b==""?[]:b.split(/\s+/)),d=a.match(/\s+/g);d==null?d=["\n"]:d.push("\n");var e=b.match(/\s+/g);e==null?e=["\n"]:e.push("\n");var f="",g=new Array;for(var h=0;h<c.o.length;h++)g[h]=randomColor(),c.o[h].text!=null?f+='<span style="background-color: '+g[h]+'">'+escape(c.o[h].text)+d[h]+"</span>":f+="<del>"+escape(c.o[h])+d[h]+"</del>";var i="";for(var h=0;h<c.n.length;h++)c.n[h].text!=null?i+='<span style="background-color: '+g[c.n[h].row]+'">'+escape(c.n[h].text)+e[h]+"</span>":i+="<ins>"+escape(c.n[h])+e[h]+"</ins>";return{o:f,n:i}}function diff(a,b){var c=new Object,d=new Object;for(var e=0;e<b.length;e++)c[b[e]]==null&&(c[b[e]]={rows:new Array,o:null}),c[b[e]].rows.push(e);for(var e=0;e<a.length;e++)d[a[e]]==null&&(d[a[e]]={rows:new Array,n:null}),d[a[e]].rows.push(e);for(var e in c)c[e].rows.length==1&&typeof d[e]!="undefined"&&d[e].rows.length==1&&(b[c[e].rows[0]]={text:b[c[e].rows[0]],row:d[e].rows[0]},a[d[e].rows[0]]={text:a[d[e].rows[0]],row:c[e].rows[0]});for(var e=0;e<b.length-1;e++)b[e].text!=null&&b[e+1].text==null&&b[e].row+1<a.length&&a[b[e].row+1].text==null&&b[e+1]==a[b[e].row+1]&&(b[e+1]={text:b[e+1],row:b[e].row+1},a[b[e].row+1]={text:a[b[e].row+1],row:e+1});for(var e=b.length-1;e>0;e--)b[e].text!=null&&b[e-1].text==null&&b[e].row>0&&a[b[e].row-1].text==null&&b[e-1]==a[b[e].row-1]&&(b[e-1]={text:b[e-1],row:b[e].row-1},a[b[e].row-1]={text:a[b[e].row-1],row:e-1});return{o:a,n:b}}; >diff --git a/koha-tmpl/intranet-tmpl/prog/css/src/staff-global.scss b/koha-tmpl/intranet-tmpl/prog/css/src/staff-global.scss >index 5798abfd15..ae6c2b9739 100644 >--- a/koha-tmpl/intranet-tmpl/prog/css/src/staff-global.scss >+++ b/koha-tmpl/intranet-tmpl/prog/css/src/staff-global.scss >@@ -2192,6 +2192,16 @@ li { > td { > &.actions { > white-space: nowrap; >+ >+ a, >+ button { >+ display: inline-block; >+ margin-left: .3rem; >+ >+ &:first-child { >+ margin-left: 0; >+ } >+ } > } > > &.bookcoverimg { >diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/about-team.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/about-team.inc >index 8fc9561093..2ef59873ce 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/includes/about-team.inc >+++ b/koha-tmpl/intranet-tmpl/prog/en/includes/about-team.inc >@@ -8,6 +8,8 @@ > <span>Release manager</span> > [%- CASE 'manager_assistant' -%] > <span>Release manager assistant</span> >+ [%- CASE 'manager_assistants' -%] >+ <span>Release manager assistants</span> > [%- CASE 'manager_mentor' -%] > <span>Release manager mentor</span> > [%- CASE 'qa_manager' -%] >@@ -32,8 +34,12 @@ > <span>Release maintainer</span> > [%- CASE 'maintainer_assistant' -%] > <span>Release maintainer assistant</span> >+ [%- CASE 'maintainer_assistants' -%] >+ <span>Release maintainer assistants</span> > [%- CASE 'maintainer_mentor' -%] > <span>Release maintainer mentor</span> >+ [%- CASE 'maintainer_mentors' -%] >+ <span>Release maintainer mentors</span> > [%- CASE 'wiki' -%] > <span>Wiki curator</span> > [%- CASE 'ci' -%] >@@ -56,12 +62,16 @@ > <span>Live CD maintainer</span> > [%- CASE 'accessibility_advocate' -%] > <span>Accessibility advocate</span> >+ [%- CASE 'accessibility_advocates' -%] >+ <span>Accessibility advocates</span> > [%- CASE 'meeting_facilitator' -%] > <span>Meeting facilitator</span> > [%- CASE 'ktd' -%] > <span>Developer tooling</span> > [%- CASE 'website' -%] > <span>Website administrator</span> >+ [%- CASE 'social_media' -%] >+ <span>Social media manager</span> > [%- CASE 'security_manager' -%] > <span>Security release manager</span> > [%- CASE -%] >@@ -77,7 +87,17 @@ > [%- BLOCK contributions -%] > [%- IF p.roles || p.notes -%] > <ul> >- [% FOREACH r IN p.roles %]<li>[% INCLUDE role role=r %] ([% r.value.join(', ') | html %])</li>[% END %] >+ [% FOREACH r IN p.roles %] >+ [% SET sorted_versions = r.value.sort.reverse %] >+ <li> >+ [% INCLUDE role role=r %] >+ ([% IF sorted_versions.size <= 2 %] >+ [% sorted_versions.join(', ') | html %] >+ [% ELSE %] >+ [% sorted_versions.slice(0,1).join(', ') | html %]<span class="version-ellipsis" data-bs-toggle="tooltip" title="[% sorted_versions.slice(2, -1).join(', ') | html %]">, ...</span> >+ [% END %]) >+ </li> >+ [% END %] > [% IF p.notes %]<li>[% p.notes | html %]</li>[% END %] > </ul> > [%- END -%] >@@ -329,6 +349,13 @@ > </li> > [% END %] > >+ [% IF t.social_media %] >+ <li >+ ><strong>Social media manager:</strong> >+ [% INCLUDE person p=t.social_media %] >+ </li> >+ [% END %] >+ > [% IF t.wiki %] > [% IF t.wiki.size == 1 %] > <li >@@ -347,6 +374,48 @@ > [% END %] > [% END %] > >+ [% IF t.meeting_facilitator %] >+ <li >+ ><strong>Meeting facilitator:</strong> >+ [% INCLUDE person p=t.meeting_facilitator %] >+ </li> >+ [% END %] >+ >+ [% IF t.chairperson %] >+ <li >+ ><strong>Meetings chairperson:</strong> >+ [% INCLUDE person p=t.chairperson %] >+ </li> >+ [% END %] >+ >+ [% IF t.database %] >+ <li >+ ><strong>Documentation specialist:</strong> >+ [% INCLUDE person p=t.database %] >+ </li> >+ [% END %] >+ >+ [% IF t.live_cd %] >+ <li >+ ><strong>Live CD maintainer:</strong> >+ [% INCLUDE person p=t.live_cd %] >+ </li> >+ [% END %] >+ >+ [% IF t.vm %] >+ <li >+ ><strong>Virtual machine maintainer:</strong> >+ [% INCLUDE person p=t.vm %] >+ </li> >+ [% END %] >+ >+ [% IF t.translation_assistant %] >+ <li >+ ><strong>Translation manager assistant:</strong> >+ [% INCLUDE person p=t.translation_assistant %] >+ </li> >+ [% END %] >+ > [% IF v != 'release' %] > <li > ><strong>Release manager:</strong> >diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/background_jobs/batch_item_record_modification.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/background_jobs/batch_item_record_modification.inc >index ee3d09f25e..1ee3fd72cb 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/includes/background_jobs/batch_item_record_modification.inc >+++ b/koha-tmpl/intranet-tmpl/prog/en/includes/background_jobs/batch_item_record_modification.inc >@@ -13,6 +13,13 @@ > [% IF job.status == 'cancelled' %]<span>The job has been cancelled before it finished.</span>[% END %] > <a href="/cgi-bin/koha/tools/batchMod.pl" title="New batch item modification">New batch item modification</a> > </div> >+ <div> >+ [% IF report.errors.size %] >+ <div class="alert alert-warning"> >+ <span>[% report.errors.size | html %] item(s) could not be modified.</span> >+ </div> >+ [% END %] >+ </div> > [% END %] > [% END %] > >diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/catalogue/itemsearch_item.csv.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/catalogue/itemsearch_item.csv.inc >index f87affb256..5d859d8c5b 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/includes/catalogue/itemsearch_item.csv.inc >+++ b/koha-tmpl/intranet-tmpl/prog/en/includes/catalogue/itemsearch_item.csv.inc >@@ -17,5 +17,5 @@ > "[% AuthorisedValues.GetDescriptionByKohaField(frameworkcode => biblio.frameworkcode, kohafield => 'items.notforloan', authorised_value => item.notforloan) | replace('"', '""') | $raw %]" [%- delimiter | $raw -%] > "[% AuthorisedValues.GetDescriptionByKohaField( kohafield => 'items.itemlost', authorised_value => item.itemlost ) || "" | replace('"', '""') | $raw %]" [%- delimiter | $raw -%] > "[% AuthorisedValues.GetDescriptionByKohaField( kohafield => 'items.withdrawn', authorised_value => item.withdrawn ) || "" | replace('"', '""') | $raw %]" [%- delimiter | $raw -%] >-"[% AuthorisedValues.GetDescriptionByKohaField( kohafield => 'items.damaged', authorised_value => item.damaged ) || "" | replace('"', '""') | $raw %]" [%- delimiter | $raw -%] "[% (item.issues || 0) | $raw %]" [%- delimiter | $raw -%] >-"[% item.datelastborrowed | $KohaDates | $raw %]" [%- delimiter | $raw -%] "[% IF item.checkout %][% item.checkout.date_due | $KohaDates | $raw %][% END %]" >+"[% AuthorisedValues.GetDescriptionByKohaField( kohafield => 'items.damaged', authorised_value => item.damaged ) || "" | replace('"', '""') | $raw %]" [%- delimiter | $raw -%] "[% item.dateaccessioned | $KohaDates | $raw %]" >+[%- delimiter | $raw -%] "[% (item.issues || 0) | $raw %]" [%- delimiter | $raw -%] "[% item.datelastborrowed | $KohaDates | $raw %]" [%- delimiter | $raw -%] "[% IF item.checkout %][% item.checkout.date_due | $KohaDates | $raw %][% END %]" >diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/catalogue/itemsearch_item.json.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/catalogue/itemsearch_item.json.inc >index bb5feb5492..2367c59f1b 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/includes/catalogue/itemsearch_item.json.inc >+++ b/koha-tmpl/intranet-tmpl/prog/en/includes/catalogue/itemsearch_item.json.inc >@@ -24,8 +24,8 @@ > "[% item.stocknumber | html | $To %]", "[% AuthorisedValues.GetDescriptionByKohaField( frameworkcode => biblio.frameworkcode, kohafield => 'items.notforloan', authorised_value => item.notforloan) | html %]", > "[% AuthorisedValues.GetDescriptionByKohaField( kohafield => 'items.itemlost', authorised_value => item.itemlost ) || "" | html %]", > "[% AuthorisedValues.GetDescriptionByKohaField( kohafield => 'items.withdrawn', authorised_value => item.withdrawn ) || "" | html %]", >-"[% AuthorisedValues.GetDescriptionByKohaField( kohafield => 'items.damaged', authorised_value => item.damaged ) || "" | html %]", "[% (item.issues || 0) | html %]", "[% item.datelastborrowed | $KohaDates %]", >-"[% IF item.checkout %][% item.checkout.date_due | $KohaDates %][% END %]", >+"[% AuthorisedValues.GetDescriptionByKohaField( kohafield => 'items.damaged', authorised_value => item.damaged ) || "" | html %]", "[% item.dateaccessioned | $KohaDates %]", "[% (item.issues || 0) | html %]", >+"[% item.datelastborrowed | $KohaDates %]", "[% IF item.checkout %][% item.checkout.date_due | $KohaDates %][% END %]", > "[% FILTER escape_quotes ~%] > <div class="btn-group dropup" > ><button type="button" class="btn btn-xs btn-default dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> <i class="fa-solid fa-pencil" aria-hidden="true"></i> Edit </button> >diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/csv_headers/catalogue/itemsearch.tt b/koha-tmpl/intranet-tmpl/prog/en/includes/csv_headers/catalogue/itemsearch.tt >index 502470d854..c55ebca0c5 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/includes/csv_headers/catalogue/itemsearch.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/includes/csv_headers/catalogue/itemsearch.tt >@@ -3,5 +3,5 @@ > [%- PROCESS 'i18n.inc' -%] > [%- SET delimiter = Koha.CSVDelimiter() -%] > [%- BLOCK -%] >- "[% t("Title") | html %]"[%- delimiter | $raw -%]"[% t("Publication date") | html %]"[%- delimiter | $raw -%]"[% t("Publisher") | html %]"[%- delimiter | $raw -%]"[% t("Collection") | html %]"[%- delimiter | $raw -%]"[% t("Barcode") | html %]"[%- delimiter | $raw -%]"[% t("Item number") | html %]"[%- delimiter | $raw -%]"[% t("Serial enumeration") | html %]"[%- delimiter | $raw -%]"[% t("Call number") | html %]"[%- delimiter | $raw -%]"[% t("Home library") | html %]"[%- delimiter | $raw -%]"[% t("Current library") | html %]"[%- delimiter | $raw -%]"[% t("Shelving location") | html %]"[%- delimiter | $raw -%]"[% t("Item type") | html %]"[%- delimiter | $raw -%]"[% t("Inventory number") | html %]"[%- delimiter | $raw -%]"[% t("Not for loan status") | html %]"[%- delimiter | $raw -%]"[% t("Lost status") | html %]"[%- delimiter | $raw -%]"[% t("Withdrawn status") | html %]"[%- delimiter | $raw -%]"[% t("Damaged status") | html %]"[%- delimiter | $raw -%]"[% t("Checkouts") | html %]"[%- delimiter | $raw -%]"[% t("Last checkout date") | html %]"[%- delimiter | $raw -%]"[% t("Due date") | html %]" >+ "[% t("Title") | html %]"[%- delimiter | $raw -%]"[% t("Publication date") | html %]"[%- delimiter | $raw -%]"[% t("Publisher") | html %]"[%- delimiter | $raw -%]"[% t("Collection") | html %]"[%- delimiter | $raw -%]"[% t("Barcode") | html %]"[%- delimiter | $raw -%]"[% t("Item number") | html %]"[%- delimiter | $raw -%]"[% t("Serial enumeration") | html %]"[%- delimiter | $raw -%]"[% t("Call number") | html %]"[%- delimiter | $raw -%]"[% t("Home library") | html %]"[%- delimiter | $raw -%]"[% t("Current library") | html %]"[%- delimiter | $raw -%]"[% t("Shelving location") | html %]"[%- delimiter | $raw -%]"[% t("Item type") | html %]"[%- delimiter | $raw -%]"[% t("Inventory number") | html %]"[%- delimiter | $raw -%]"[% t("Not for loan status") | html %]"[%- delimiter | $raw -%]"[% t("Lost status") | html %]"[%- delimiter | $raw -%]"[% t("Withdrawn status") | html %]"[%- delimiter | $raw -%]"[% t("Damaged status") | html %]"[% t("Date accessioned") | html %]"[%- delimiter | $raw -%]"[%- delimiter | $raw -%]"[% t("Checkouts") | html %]"[%- delimiter | $raw -%]"[% t("Last checkout date") | html %]"[%- delimiter | $raw -%]"[% t("Due date") | html %]" > [%- END -%] >diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/guided-reports-view.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/guided-reports-view.inc >index 4461581ba1..59e416f7b8 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/includes/guided-reports-view.inc >+++ b/koha-tmpl/intranet-tmpl/prog/en/includes/guided-reports-view.inc >@@ -10,12 +10,12 @@ > </ul> > <h5>Useful resources</h5> > <ul> >- <li><a href="http://wiki.koha-community.org/wiki/SQL_Reports_Library" target="blank">Koha report library</a></li> >+ <li><a href="https://wiki.koha-community.org/wiki/SQL_Reports_Library" target="blank">Koha report library</a></li> > [% SET koha_version = Koha.Version %] > [% IF koha_version.development %] >- <li><a href="http://schema.koha-community.org/main" target="blank">Koha database schema</a></li> >+ <li><a href="https://schema.koha-community.org/main" target="blank">Koha database schema</a></li> > [% ELSE %] >- <li><a href="http://schema.koha-community.org/[% koha_version.major | uri %]_[% koha_version.minor | uri %]" target="blank">Koha database schema</a></li> >+ <li><a href="https://schema.koha-community.org/[% koha_version.major | uri %]_[% koha_version.minor | uri %]" target="blank">Koha database schema</a></li> > [% END %] > </ul> > </div> >diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/header.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/header.inc >index 15e1f17ce5..16ed39f758 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/includes/header.inc >+++ b/koha-tmpl/intranet-tmpl/prog/en/includes/header.inc >@@ -104,14 +104,16 @@ > </span> > <span id="logged-in-info-full"> > [% SET is_superlibrarian = CAN_user_superlibrarian ? 'is_superlibrarian' : '' %] >- <span class="loggedinusername [% is_superlibrarian | html %]">[% logged_in_user.userid | html %]</span> >- <span class="loggedincategorycode content_hidden">[% logged_in_user.categorycode | html %]</span> >+ <span class="loggedinusername [% is_superlibrarian | html %]" data-loggedinusername="[% logged_in_user.userid | html %]" data-is-superlibrarian="[% is_superlibrarian | html %]" >+ >[% logged_in_user.userid | html %]</span >+ > >+ <span class="loggedincategorycode content_hidden" data-loggedincategorycode="[% logged_in_user.categorycode | html %]">[% logged_in_user.categorycode | html %]</span> > [% IF ( StaffLoginRestrictLibraryByIP ) %] > <brand> [% Branches.GetLoggedInBranchname | html %] </brand> > [% ELSE %] > <strong> >- <span class="logged-in-branch-name">[% Branches.GetLoggedInBranchname | html %]</span> >- <span class="logged-in-branch-code content_hidden">[% Branches.GetLoggedInBranchcode | html %]</span> >+ <span class="logged-in-branch-name" data-logged-in-branch-name="[% Branches.GetLoggedInBranchname | html %]">[% Branches.GetLoggedInBranchname | html %]</span> >+ <span class="logged-in-branch-code content_hidden" data-logged-in-branch-code="[% Branches.GetLoggedInBranchcode | html %]">[% Branches.GetLoggedInBranchcode | html %]</span> > </strong> > [% END %] > [% IF Koha.Preference('UseCirculationDesks') && Desks.ListForLibrary.count %] >@@ -120,16 +122,16 @@ > [% IF ( Desks.GetLoggedInDeskName == '' ) %] > <span class="logged-in-desk-name">NO DESK SET</span> > [% ELSE %] >- <span class="logged-in-desk-name">[% Desks.GetLoggedInDeskName | html %]</span> >- <span class="logged-in-desk-id content_hidden">[% Desks.GetLoggedInDeskId | html %]</span> >+ <span class="logged-in-desk-name" data-logged-in-desk-name="[% Desks.GetLoggedInDeskName | html %]">[% Desks.GetLoggedInDeskName | html %]</span> >+ <span class="logged-in-desk-id content_hidden" data-logged-in-desk-id="[% Desks.GetLoggedInDeskId | html %]">[% Desks.GetLoggedInDeskId | html %]</span> > [% END %] > </strong> > [% END %] > [% IF Koha.Preference('UseCashRegisters') && !(Registers.session_register_name == '') %] > <span class="separator">|</span> > <strong> >- <span class="logged-in-register-name">[% Registers.session_register_name | html %]</span> >- <span class="logged-in-register-id content_hidden">[% Registers.session_register_id | html %]</span> >+ <span class="logged-in-register-name" data-logged-in-register-name="[% Registers.session_register_name | html %]">[% Registers.session_register_name | html %]</span> >+ <span class="logged-in-register-id content_hidden" data-logged-in-register-id="[% Registers.session_register_id | html %]">[% Registers.session_register_id | html %]</span> > </strong> > [% END %] > </span> >diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/html-customization-help.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/html-customization-help.inc >index bc0bfabe7c..1faec6c9fa 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/includes/html-customization-help.inc >+++ b/koha-tmpl/intranet-tmpl/prog/en/includes/html-customization-help.inc >@@ -16,6 +16,8 @@ > > <div id="IntranetmainUserblock_notes" class="hint customization_note"> Include this content in its own section on the main page of the staff interface </div> > >+<div id="StaffReportsHome_notes" class="hint customization_note"> Include this content in its own section on the reports home page</div> >+ > <div id="opaccredits_notes" class="hint customization_note"> Include this content in the footer of all pages in the OPAC. </div> > > <div id="OpacCustomSearch_notes" class="hint customization_note"> Replace the search box at the top of OPAC pages with this content. </div> >diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/member-display-address-style.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/member-display-address-style.inc >index 7f29640ceb..3e0bb172e0 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/includes/member-display-address-style.inc >+++ b/koha-tmpl/intranet-tmpl/prog/en/includes/member-display-address-style.inc >@@ -29,10 +29,18 @@ > [%~ END ~%] > [%~ IF ( patron.city || patron.zipcode || patron.state || patron.country ) ~%] > <li class="patroncity"> >- <span class="city">[%~ patron.city |html ~%]</span>[%~ IF ( patron.state ) %][%~ IF ( patron.city ) ~%],[% END ~%]<span class="state">[% patron.state |html ~%]</span>[%~ END ~%] >- [%~ IF ( patron.zipcode ) %]<span class="zipcode">[%~ " " _ patron.zipcode |html ~%]</span>[% END %][%~ IF ( patron.country ) %][%~ IF ( patron.zipcode || patron.state || patron.city ) ~%] >- <span class="comma">,</span> >- [% END ~%]<span class="country">[% patron.country |html ~%]</span>[%~ END ~%] >+ <span class="city">[%~ patron.city |html ~%]</span> >+ [%~ IF ( patron.state ) %] >+ [%~ IF ( patron.city ) ~%] >+ , >+ [% END ~%] >+ <span class="state">[% patron.state |html ~%]</span> >+ [%~ END ~%] >+ [%~ IF ( patron.zipcode ) %]<span class="zipcode">[%~ " " _ patron.zipcode |html ~%]</span>[% END %] >+ [%~ IF ( patron.country ) %] >+ [% line_break | $raw %] >+ <span class="country">[% patron.country |html ~%]</span> >+ [%~ END ~%] > </li> > [%~ END ~%] > [%~ END ~%] >@@ -44,11 +52,15 @@ > [%~ IF patron.streettype ~%] > [%~ SET roadtype_desc = AuthorisedValues.GetByCode('ROADTYPE', patron.streettype) ~%] > [%~ END ~%] >- <li class="patronaddress1" >- ><span class="address1">[%~ patron.address | html ~%]</span>[%~ IF roadtype_desc %]<span class="roadtype">[% roadtype_desc | html ~%]</span>[%~ END ~%][%~ IF patron.streetnumber %] >+ <li class="patronaddress1"> >+ <span class="address1">[%~ patron.address | html ~%]</span> >+ [%~ IF roadtype_desc %] >+ <span class="roadtype">[% roadtype_desc | html ~%]</span> >+ [%~ END ~%] >+ [%~ IF patron.streetnumber %] > <span class="streetnumber">[% patron.streetnumber | html ~%]</span> >- [%~ END ~%]</li >- > >+ [%~ END ~%] >+ </li> > [%~ END ~%] > [%~ IF ( patron.address2 ) ~%] > <li class="patronaddress2">[%~ patron.address2 | html ~%]</li> >@@ -56,7 +68,11 @@ > [%~ END ~%] > [%~ IF ( patron.city || patron.zipcode || patron.state || patron.country ) ~%] > <li class="patroncity"> >- [%~ IF ( patron.zipcode ) ~%]<span class="zipcode">[%~ patron.zipcode | html %]</span>[% END ~%]<span class="city">[%~ patron.city | html ~%]</span>[%~ IF ( patron.state ) ~%] >+ [%~ IF ( patron.zipcode ) ~%] >+ <span class="zipcode">[%~ patron.zipcode | html %]</span> >+ [% END ~%] >+ <span class="city">[%~ patron.city | html ~%]</span> >+ [%~ IF ( patron.state ) ~%] > [% line_break | $raw %]<span class="state">[%~ patron.state | html ~%]</span> > [%~ END ~%] > [%~ IF ( patron.country ) ~%][% line_break | $raw %]<span class="country">[%~ patron.country | html ~%]</span>[%~ END ~%] >@@ -71,9 +87,13 @@ > [%~ IF patron.streettype ~%] > [%~ SET roadtype_desc = AuthorisedValues.GetByCode('ROADTYPE', patron.streettype) ~%] > [%~ END ~%] >- <li class="patronaddress1" >- >[%~ IF patron.streetnumber ~%]<span class="streetnumber">[%~ patron.streetnumber | html %]</span>[% END ~%] >- [%~ IF roadtype_desc ~%]<span class="roadtype">[%~ roadtype_desc | html %]</span>[% END ~%] >+ <li class="patronaddress1"> >+ [%~ IF patron.streetnumber ~%] >+ <span class="streetnumber">[%~ patron.streetnumber | html %]</span> >+ [% END ~%] >+ [%~ IF roadtype_desc ~%] >+ <span class="roadtype">[%~ roadtype_desc | html %]</span> >+ [% END ~%] > <span class="address1">[%~ patron.address | html ~%]</span> > </li> > [%~ END ~%] >@@ -83,7 +103,11 @@ > [%~ END ~%] > [%~ IF ( patron.city || patron.zipcode || patron.state || patron.country ) ~%] > <li class="patroncity"> >- [%~ IF ( patron.zipcode ) ~%]<span class="zipcode">[%~ patron.zipcode | html %]</span>[% END ~%]<span class="city">[%~ patron.city | html ~%]</span>[%~ IF ( patron.state ) ~%] >+ [%~ IF ( patron.zipcode ) ~%] >+ <span class="zipcode">[%~ patron.zipcode | html %]</span> >+ [% END ~%] >+ <span class="city">[%~ patron.city | html ~%]</span> >+ [%~ IF ( patron.state ) ~%] > [% line_break | $raw %]<span class="state">[%~ patron.state | html ~%]</span> > [%~ END ~%] > [%~ IF ( patron.country ) ~%][% line_break | $raw %]<span class="country">[%~ patron.country | html ~%]</span>[%~ END ~%] >diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/patron_messages.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/patron_messages.inc >index 5d71adf30d..942993dde7 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/includes/patron_messages.inc >+++ b/koha-tmpl/intranet-tmpl/prog/en/includes/patron_messages.inc >@@ -92,32 +92,28 @@ > > [% IF ( userdebarred ) %] > <li class="userdebarred blocker"> >- <span class="circ-hlt"> Restricted since [% debarredsince | $KohaDates %]:</span> Patron's account is restricted >- [% IF ( userdebarreddate ) %] >- until [% userdebarreddate | $KohaDates %] >- [% END %] >- >- [% IF ( debarredcomment ) %] >- with the explanation: <br /> >- <em> >- [% IF debarredcomment.search('OVERDUES_PROCESS') %] >- Restriction added by overdues process [% debarredcomment.remove('OVERDUES_PROCESS ') | $raw | html_line_break %] >- [% ELSE %] >- [% FOREACH restriction IN patron.restrictions %] >- <div class="[% restriction.type.code | lower | html %]_restriction"> >- <span class="restriction_expiration"> >- [% IF restriction.expiration %] >- [% restriction.expiration | $KohaDates %] >- [% ELSE %] >- <strong>Indefinite</strong> >- [% END %] >- </span> >- <span class="restriction_detail"> [%- restriction.type.display_text | html -%][%- IF restriction.comment -%]: [%- restriction.comment | html_line_break -%][%- END -%] </span> >- </div> >+ <span class="circ-hlt">Account restricted since [% debarredsince | $KohaDates %]</span> >+ <ul> >+ [% FOREACH restriction IN patron.restrictions %] >+ <li class="[% restriction.type.code | lower | html %]_restriction"> >+ <span class="restriction_expiration"> >+ [% IF restriction.expiration %] >+ <strong>Restriction expiring [% restriction.expiration | $KohaDates %]</strong> >+ [% ELSE %] >+ <strong>Indefinite restriction</strong> >+ [% END %] >+ </span> >+ [% IF restriction.comment.search('OVERDUES_PROCESS') %] >+ Restriction added by overdues process [% restriction.comment.remove('OVERDUES_PROCESS ') | $raw | html_line_break %] >+ [% ELSE %] >+ [% IF restriction.comment %] >+ <span class="restriction_detail">[%- restriction.comment | html_line_break -%]</span> >+ [% END %] > [% END %] >- [% END %] </em >- ><br /> >- [% END %] >+ <span class="restriction_type">([%- restriction.type.display_text | html -%])</span> >+ </li> >+ [% END %] >+ </ul> > <a class="btn btn-xs btn-default" href="#reldebarments-tab" onclick="(new bootstrap.Tab($('#reldebarments-tab'))).show()"><i class="fa fa-ban"></i> View restrictions</a> > </li> > <!-- /.blocker --> >diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/prefs-menu.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/prefs-menu.inc >index 937e174112..2b17d1f079 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/includes/prefs-menu.inc >+++ b/koha-tmpl/intranet-tmpl/prog/en/includes/prefs-menu.inc >@@ -165,6 +165,17 @@ > </li> > [% END %] > >+ [% IF ( reports ) %] >+ <li class="active"> >+ <a title="Reports" href="/cgi-bin/koha/admin/preferences.pl?tab=reports">Reports</a> >+ [% PROCESS subtabs %] >+ </li> >+ [% ELSE %] >+ <li> >+ <a title="Reports" href="/cgi-bin/koha/admin/preferences.pl?tab=reports">Reports</a> >+ </li> >+ [% END %] >+ > [% IF ( searching ) %] > <li class="active"> > <a title="Searching" href="/cgi-bin/koha/admin/preferences.pl?tab=searching">Searching</a> >diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/tools-menu.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/tools-menu.inc >index 28cd71f6b7..29a626aa1d 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/includes/tools-menu.inc >+++ b/koha-tmpl/intranet-tmpl/prog/en/includes/tools-menu.inc >@@ -6,7 +6,7 @@ > <ul> > <li><a href="/cgi-bin/koha/tools/tools-home.pl">Tools home</a></li> > </ul> >- [% IF ( CAN_user_tools_manage_patron_lists || CAN_user_clubs || CAN_user_tools_moderate_comments || CAN_user_tools_import_patrons || CAN_user_tools_edit_notices || CAN_user_tools_edit_notice_status_triggers || CAN_user_tools_label_creator || CAN_user_tools_delete_anonymize_patrons || CAN_user_tools_edit_patrons || CAN_user_tools_moderate_tags || ( CAN_user_tools_batch_upload_patron_images && Koha.Preference('patronimages') ) || CAN_user_tools_rotating_collections ) %] >+ [% IF ( CAN_user_tools_manage_patron_lists || CAN_user_clubs || CAN_user_tools_moderate_comments || CAN_user_tools_import_patrons || CAN_user_tools_edit_notices || CAN_user_tools_edit_notice_status_triggers || CAN_user_tools_label_creator || CAN_user_tools_delete_anonymize_patrons || CAN_user_tools_edit_patrons || CAN_user_tools_batch_extend_due_dates || CAN_user_tools_moderate_tags || ( CAN_user_tools_batch_upload_patron_images && Koha.Preference('patronimages') ) || CAN_user_tools_rotating_collections ) %] > <h5>Patrons and circulation</h5> > <ul> > [% IF ( CAN_user_tools_manage_patron_lists ) %] >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/about.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/about.tt >index 2ff35d710a..853ef8d27d 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/about.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/about.tt >@@ -13,6 +13,11 @@ > [% END %]</title > > > [% INCLUDE 'doc-head-close.inc' %] >+<style> >+ .version-ellipsis { >+ text-decoration: underline; >+ } >+</style> > </head> > <body id="about_about" class="about"> > [% INCLUDE 'header.inc' %] >@@ -801,11 +806,7 @@ > </div> > [% IF db_audit.diff_found %] > <div> >- <p>Run the following SQL to fix the database:</p> >- <pre> >- <code>[% db_audit.diff | $raw %]</code> >- </pre >- > >+ <pre><code>[% db_audit.diff | html %]</code></pre> > </div> > [% END %] > [% END # tab=database %] >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/acqui/basket.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/acqui/basket.tt >index 49c9d98fd3..8561c5fe75 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/acqui/basket.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/acqui/basket.tt >@@ -150,6 +150,8 @@ > class="btn btn-default btn-xs submit-form-link" > href="#" > data-ean="[% eanacct.ean | html %]" >+ data-ean_description="[% eanacct.description | html %]" >+ data-ean_branch="[% eanacct.branch.branchname | html %]" > data-basketno="[% basketno | html %]" > data-action="basket.pl" > data-method="post" >@@ -812,6 +814,19 @@ > <a href="neworderempty.pl?ordernumber=[% books_loo.ordernumber | uri %]&booksellerid=[% booksellerid | uri %]&basketno=[% basketno | uri %]">Modify</a> > <br /> > <a href="#" class="transfer_order" data-ordernumber="[% books_loo.ordernumber | html %]">Transfer</a> >+ <br /> >+ [% END %] >+ [% UNLESS ( books_loo.suggestionid ) %] >+ <a >+ data-action="/cgi-bin/koha/acqui/newordersuggestion.pl" >+ data-booksellerid="[% booksellerid | html %]" >+ data-basketno="[% basketno | html %]" >+ data-link_order="[% books_loo.ordernumber | html %]" >+ data-method="get" >+ class="submit-form-link" >+ href="#" >+ >[% tp('verb', 'Link suggestion') | html %]</a >+ > > [% END %] > </td> > [% END %] >@@ -1015,7 +1030,11 @@ > <div id="closebasket_needsconfirmation" class="alert alert-warning"> > <form action="/cgi-bin/koha/acqui/basket.pl" method="post"> > [% INCLUDE 'csrf-token.inc' %] >- <h1>Are you sure you want to generate an EDIFACT order and close basket [% basketname | html %]?</h1> >+ <h1>Are you sure you want to generate this EDIFACT order?</h1> >+ <ul> >+ <li> An EDIFACT order will be generated for <b> [% ean_branch | html %] ([% ean | html %]) [% IF ean_description %][% ean_description | html %][% END %]</b></li> >+ <li> The basket <b>[% basketname | html %]</b> will be closed </li> >+ </ul> > [% IF CAN_user_acquisition_group_manage %] > <p> > <label for="createbasketgroup">Attach this basket to a new basket group with the same name</label> >@@ -1028,11 +1047,11 @@ > <input type="hidden" name="booksellerid" value="[% booksellerid | html %]" /> > <input type="hidden" name="confirm" value="1" /> > <input type="hidden" name="basketgroupname" value="[% basketgroupname | html %]" /> >- <button type="submit" class="btn btn-default approve" accesskey="Y"><i class="fa fa-fw fa-check"></i> Yes, close (Y)</button> >+ <button type="submit" class="btn btn-default approve" accesskey="Y"><i class="fa fa-fw fa-check"></i> Yes, generate order and close basket (Y)</button> > </form> > <form action="/cgi-bin/koha/acqui/basket.pl" method="get"> > <input type="hidden" name="basketno" value="[% basketno | html %]" /> >- <button type="submit" class="btn btn-default deny" accesskey="N"><i class="fa fa-fw fa-times"></i> No, don't close (N)</button> >+ <button type="submit" class="btn btn-default deny" accesskey="N"><i class="fa fa-fw fa-times"></i> Cancel (C)</button> > </form> > </div> > <!-- /#closebasket_needsconfirmation --> >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/acqui/newordersuggestion.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/acqui/newordersuggestion.tt >index 613b56588b..a598e9d822 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/acqui/newordersuggestion.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/acqui/newordersuggestion.tt >@@ -89,7 +89,20 @@ > </td> > <td> [% suggestion.total | $Price %] </td> > <td class="actions"> >- [% IF ( suggestion.biblionumber ) %] >+ [% IF link_order %] >+ <a >+ data-action="/cgi-bin/koha/acqui/newordersuggestion.pl" >+ data-op="cud-link_order" >+ data-link_order="[% link_order | html %]" >+ data-booksellerid="[% booksellerid | html %]" >+ data-basketno="[% basketno | html %]" >+ data-suggestionid="[% suggestion.suggestionid | html %]" >+ data-method="post" >+ class="btn btn-default btn-xs submit-form-link" >+ href="#" >+ ><i class="fa fa-plus"></i> [% tp('verb', 'Link suggestion') | html %]</a >+ > >+ [% ELSIF ( suggestion.biblionumber ) %] > <a > href="neworderempty.pl?booksellerid=[% booksellerid | uri %]&basketno=[% basketno | uri %]&suggestionid=[% suggestion.suggestionid | uri %]&biblio=[% suggestion.biblionumber | uri %]" > class="btn btn-default btn-xs" >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/categories.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/categories.tt >index 8a5cf0478c..e91ba21feb 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/categories.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/categories.tt >@@ -486,13 +486,13 @@ > <label for="noissuescharge">Checkout charge limit: </label> > <input type="text" pattern="^\d+(\.\d{2})?$" name="noissuescharge" id="noissuescharge" value="[% category.noissuescharge | $Price on_editing => 1 %]" size="5" maxlength="8" /> > [%- SET pref_noissuescharge_link = '<a href="/cgi-bin/koha/admin/preferences.pl?op=search&searchfield=noissuescharge&ok=Search">noissuescharge</a>' -%] >- <div class="hint">If set, this will override the global value set in the [%- pref_noissuescharge_link | $raw | $KohaSpan -%] preference.</div> >+ <div class="hint">If set, this will override the global value set in the [%- pref_noissuescharge_link | $raw | $KohaSpan -%] system preference.</div> > </li> > <li> > <label for="noissueschargeguarantees">Guarantees checkout charge limit: </label> > <input type="text" pattern="^\d+(\.\d{2})?$" name="noissueschargeguarantees" id="noissueschargeguarantees" value="[% category.noissueschargeguarantees | $Price on_editing => 1 %]" size="5" maxlength="8" /> > [%- SET pref_NoIssuesChargeGuarantees_link = '<a href="/cgi-bin/koha/admin/preferences.pl?op=search&searchfield=NoIssuesChargeGuarantees&ok=Search">NoIssuesChargeGuarantees</a>' -%] >- <div class="hint">If set, this will override the global value set in the [%- pref_NoIssuesChargeGuarantees_link | $raw | $KohaSpan -%] preference.</div> >+ <div class="hint">If set, this will override the global value set in the [%- pref_NoIssuesChargeGuarantees_link | $raw | $KohaSpan -%] system preference.</div> > </li> > <li> > <label for="noissueschargeguarantorswithguarantees">Guarantors with guarantees checkout charge limit: </label> >@@ -505,8 +505,8 @@ > size="5" > maxlength="8" > /> >- [%- SET pref_NoIssuesChargeGuarantorsWithGuarantees_link = '<a href="/cgi-bin/koha/admin/preferences.pl?op=search&searchfield=NoIssuesChargeGuarantees&ok=Search">NoIssuesChargeGuarantees</a>' -%] >- <div class="hint">If set, this will override the global value set in the [%- pref_NoIssuesChargeGuarantorsWithGuarantees_link | $raw | $KohaSpan -%] preference.</div> >+ [%- SET pref_NoIssuesChargeGuarantorsWithGuarantees_link = '<a href="/cgi-bin/koha/admin/preferences.pl?op=search&searchfield=NoIssuesChargeGuarantorsWithGuarantees&ok=Search">NoIssuesChargeGuarantorsWithGuarantees</a>' -%] >+ <div class="hint">If set, this will override the global value set in the [%- pref_NoIssuesChargeGuarantorsWithGuarantees_link | $raw | $KohaSpan -%] system preference.</div> > </li> > <li> > <label for="enforce_expiry_notice">Enforce patron account expiry notice:</label> >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/localization.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/localization.tt >index 199502cdaf..e49f2ac527 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/localization.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/localization.tt >@@ -198,11 +198,12 @@ > $(document).ready(function() { > $(".dialog").hide(); > >- var table = $("#localization").kohaTable({ >+ let table = $("#localization").kohaTable({ > dom: "t", > paging: false, > autoWidth: false, > }); >+ let table_dt = table.DataTable(); > > var languages_select = $('<select name="lang" id="lang"></select>'); > [% FOR language IN languages %] >@@ -275,7 +276,7 @@ > if ( success.error ) { > show_message({ type: 'error_on_insert', data: success }); > } else { >- var new_row = table.row.add( [ success.id, success.entity, success.code, success.lang, success.translation, "<a href=\"#\" class=\"delete\"><i class=\"fa fa-trash-can\"></i> Delete</a>" ] ).draw().node(); >+ var new_row = table_dt.row.add( [ success.id, success.entity, success.code, success.lang, success.translation, "<a href=\"#\" class=\"delete\"><i class=\"fa fa-trash-can\"></i> Delete</a>" ] ).draw().node(); > $( new_row ).attr("id", "row_id_" + success.id ).data("id", success.id ); > show_message({ type: 'success_on_insert', data: success }); > } >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/marctagstructure.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/marctagstructure.tt >index f307644d51..5bdc3777c9 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/marctagstructure.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/marctagstructure.tt >@@ -94,7 +94,9 @@ > [% IF ( else ) %] > <div id="toolbar" class="btn-toolbar"> > <a class="btn btn-default" id="addtag" href="/cgi-bin/koha/admin/marctagstructure.pl?op=add_form&frameworkcode=[% framework.frameworkcode | uri %]"><i class="fa fa-plus"></i> New tag</a> >- <a class="btn btn-default" href="/cgi-bin/koha/admin/biblio_framework.pl?op=add_form&frameworkcode=[% framework.frameworkcode | uri %]"><i class="fa-solid fa-pencil" aria-hidden="true"></i> Edit framework</a> >+ [% IF framework %] >+ <a class="btn btn-default" href="/cgi-bin/koha/admin/biblio_framework.pl?op=add_form&frameworkcode=[% framework.frameworkcode | uri %]"><i class="fa-solid fa-pencil" aria-hidden="true"></i> Edit framework</a> >+ [% END %] > </div> > [% END %] > >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/acquisitions.pref b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/acquisitions.pref >index 523c30b896..7f1a886095 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/acquisitions.pref >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/acquisitions.pref >@@ -9,7 +9,7 @@ Acquisitions: > cataloguing: cataloging the record. > - This is only the default behavior, and can be changed per-basket. > - >- - "The following <a href='http://schema.koha-community.org/__VERSION__/tables/items.html' target='blank'>database columns</a> should be unique in an item:" >+ - "The following <a href='https://schema.koha-community.org/__VERSION__/tables/items.html' target='blank'>database columns</a> should be unique in an item:" > - pref: UniqueItemFields > type: modalselect > source: items >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/circulation.pref b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/circulation.pref >index 9719f3c87e..9cd73cfe85 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/circulation.pref >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/circulation.pref >@@ -677,6 +677,13 @@ Circulation: > 1: Block > 0: "Don't block" > - returning of items that have been withdrawn. >+ - >+ - "Prevent the ability to withdraw items with the following statuses:" >+ - pref: PreventWithdrawingItemsStatus >+ multiple: >+ intransit: In-transit >+ checkedout: Checked out >+ - > - > - pref: BlockReturnOfLostItems > choices: >@@ -1527,4 +1534,4 @@ Circulation: > choices: > 1: Enable > 0: Disable >- - "the curbside pickup module." >+ - "the curbside pickup module." >\ No newline at end of file >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/i18n_l10n.pref b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/i18n_l10n.pref >index 34a9d54211..e95b345434 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/i18n_l10n.pref >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/i18n_l10n.pref >@@ -63,6 +63,11 @@ I18N/L10N: > de: German style ([Address] [Street number] - [ZIP/Postal Code] [City] - [Country]) > fr: French style ([Street number] [Address] - [ZIP/Postal Code] [City] - [Country]) > - . >+ - >+ - Sort facet names using >+ - pref: FacetSortingLocale >+ type: locale-list >+ - locale when FacetOrder is set to Alphabetical. This enables proper Unicode-aware sorting of accented characters and locale-specific alphabetical ordering. > - > - pref: TranslateNotices > choices: >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/opac.pref b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/opac.pref >index 5441fd7e11..f15aad0b93 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/opac.pref >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/opac.pref >@@ -668,7 +668,7 @@ OPAC: > type: textarea > syntax: text/x-yaml > class: code >- - Define custom rules to hide specific items from search and view on the OPAC. How to write these rules is documented on the <a href="http://wiki.koha-community.org/wiki/OpacHiddenItems" target="_blank">Koha wiki</a>. >+ - Define custom rules to hide specific items from search and view on the OPAC. How to write these rules is documented on the <a href="https://wiki.koha-community.org/wiki/OpacHiddenItems" target="_blank">Koha wiki</a>. > - > - 'List of patron categories, that can see items otherwise hidden by <a href="/cgi-bin/koha/admin/preferences.pl?op=search&searchfield=OpacHiddenItems">OpacHiddenItems</a>:' > - pref: OpacHiddenItemsExceptions >@@ -851,19 +851,19 @@ OPAC: > - "days after account creation." > - "<br><strong>NOTE:</strong> This system preference requires the <code>misc/cronjobs/cleanup_database.pl</code> cronjob. Ask your system administrator to schedule it.<br>No patrons will be deleted if you set the pref to zero or make it empty." > - >- - "The following <a href='http://schema.koha-community.org/__VERSION__/tables/borrowers.html' target='blank'>database columns</a> must be filled in on the patron modification screen:" >+ - "The following <a href='https://schema.koha-community.org/__VERSION__/tables/borrowers.html' target='blank'>database columns</a> must be filled in on the patron modification screen:" > - pref: PatronSelfModificationMandatoryField > type: modalselect > source: borrowers > exclusions: password|cardnumber|dateexpiry|categorycode|sort1|sort2|opacnote|borrowernotes|gonenoaddress|lost|flags > - >- - "The following <a href='http://schema.koha-community.org/__VERSION__/tables/borrowers.html' target='blank'>database columns</a> must be filled in on the patron entry screen:" >+ - "The following <a href='https://schema.koha-community.org/__VERSION__/tables/borrowers.html' target='blank'>database columns</a> must be filled in on the patron entry screen:" > - pref: PatronSelfRegistrationBorrowerMandatoryField > type: modalselect > source: borrowers > exclusions: sort1|sort2|opacnote|borrowernotes|gonenoaddress|lost|flags > - >- - "The following <a href='http://schema.koha-community.org/__VERSION__/tables/borrowers.html' target='blank'>database columns</a> will not appear on the patron self-registration screen:" >+ - "The following <a href='https://schema.koha-community.org/__VERSION__/tables/borrowers.html' target='blank'>database columns</a> will not appear on the patron self-registration screen:" > - pref: PatronSelfRegistrationBorrowerUnwantedField > type: modalselect > source: borrowers >@@ -871,7 +871,7 @@ OPAC: > inclusions: categorycode|dateexpiry > - '<br /> NOTE: preferred_name will be set to firstname if not included in the form.' > - >- - "The following <a href='http://schema.koha-community.org/__VERSION__/tables/borrowers.html' target='blank'>database columns</a> will not appear on the patron self-modification screen:" >+ - "The following <a href='https://schema.koha-community.org/__VERSION__/tables/borrowers.html' target='blank'>database columns</a> will not appear on the patron self-modification screen:" > - pref: PatronSelfModificationBorrowerUnwantedField > type: modalselect > source: borrowers >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/patrons.pref b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/patrons.pref >index 424b3eee9b..2ca8899ec3 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/patrons.pref >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/patrons.pref >@@ -211,7 +211,7 @@ Patrons: > - pref: EmailFieldPrecedence > class: multi > - '<br><strong>NOTE:</strong> All patron fields can be used, but to work correctly they must contain a valid email address or an empty string.' >- - "Valid options are the <a href='http://schema.koha-community.org/__VERSION__/tables/borrowers.html' target='blank'>database columns</a> of the borrowers table, separated by | (pipe)." >+ - "Valid options are the <a href='https://schema.koha-community.org/__VERSION__/tables/borrowers.html' target='blank'>database columns</a> of the borrowers table, separated by | (pipe)." > - "Example: email|emailpro|B_email" > - > - "When <a href='/cgi-bin/koha/admin/preferences.pl?op=search&searchfield=EmailFieldPrimary'>EmailFieldPrimary</a> is set to '<strong>selected addresses</strong>', send email to all valid email addresses in the selected fields:" >@@ -274,14 +274,14 @@ Patrons: > cron: (Deprecated) according to --send-notices cron switch > Patron forms: > - >- - "The following <a href='http://schema.koha-community.org/__VERSION__/tables/borrowers.html' target='blank'>database columns</a> must be filled in on the patron entry screen:" >+ - "The following <a href='https://schema.koha-community.org/__VERSION__/tables/borrowers.html' target='blank'>database columns</a> must be filled in on the patron entry screen:" > - pref: BorrowerMandatoryField > type: modalselect > source: borrowers > exclusions: flags > - '<strong>NOTE:</strong> If <a href="/cgi-bin/koha/admin/preferences.pl?op=search&searchfield=autoMemberNum">autoMemberNum</a> is enabled, the system preference <a href="/cgi-bin/koha/admin/preferences.pl?op=search&searchfield=BorrowerMandatoryField">BorrowerMandatoryField</a> must not contain the field <code>cardnumber</code>.' > - >- - "The following <a href='http://schema.koha-community.org/__VERSION__/tables/borrowers.html' target='blank'>database columns</a> will not appear on the patron entry screen:" >+ - "The following <a href='https://schema.koha-community.org/__VERSION__/tables/borrowers.html' target='blank'>database columns</a> will not appear on the patron entry screen:" > - pref: BorrowerUnwantedField > type: modalselect > source: borrowers >@@ -311,7 +311,7 @@ Patrons: > 0: "Don't" > - store and display surnames in upper case. > - >- - "The following <a href='http://schema.koha-community.org/__VERSION__/tables/borrowers.html' target='blank'>database columns</a>:" >+ - "The following <a href='https://schema.koha-community.org/__VERSION__/tables/borrowers.html' target='blank'>database columns</a>:" > - pref: PatronQuickAddFields > type: modalselect > source: borrowers >@@ -337,7 +337,7 @@ Patrons: > messaging: "Patron messaging preferences" > - . > - >- - "The following <a href='http://schema.koha-community.org/__VERSION__/tables/borrowers.html' target='blank'>database columns</a>:" >+ - "The following <a href='https://schema.koha-community.org/__VERSION__/tables/borrowers.html' target='blank'>database columns</a>:" > - pref: PatronDuplicateMatchingAddFields > type: modalselect > source: borrowers >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/reports.pref b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/reports.pref >new file mode 100644 >index 0000000000..1579aaf090 >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/reports.pref >@@ -0,0 +1,9 @@ >+Reports: >+ Reports Access: >+ - >+ - pref: EnableFilteringReports >+ choices: >+ 1: Enable >+ 0: Disable >+ - "filtering report access based on staff home library." >+ - "<br><strong>NOTE:</strong> A NOTE ABOUT THE EFFECTS OF THIS PREFERENCE / SETTING" >\ No newline at end of file >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/staff_interface.pref b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/staff_interface.pref >index 8da36fceb1..00b1b06912 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/staff_interface.pref >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/staff_interface.pref >@@ -113,12 +113,6 @@ Staff interface: > type: textarea > syntax: text/html > class: code >- - >- - "Show the following HTML in its own div on the bottom of the home page of the reports module:" >- - pref: IntranetReportsHomeHTML >- type: textarea >- syntax: text/html >- class: code > - > - pref: StaffHighlightedWords > type: boolean >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/search_filters.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/search_filters.tt >index f822751a8f..1aea30fc4e 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/search_filters.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/search_filters.tt >@@ -30,11 +30,9 @@ > [% END #/ WRAPPER sub-header.inc %] > > [% WRAPPER 'main-container.inc' aside='admin-menu' %] >- >+ <h1>Search filters</h1> > [% IF filters_count %] >- <div id="search_filters_list"> >- <h2>Search filters</h2> >- >+ <div id="search_filters_list" class="page-section"> > <table id="search_filters_table"> > <thead> > <tr> >@@ -44,7 +42,7 @@ > <th>Limits</th> > <th>OPAC</th> > <th>Staff interface</th> >- <th> </th> >+ <th data-class-name="actions noExport">Actions</th> > </tr> > </thead> > </table> >@@ -62,14 +60,16 @@ > <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> > </div> > <div class="modal-body"> >- <div class="form-group"> >+ <div class="mb-3"> > <input type="hidden" id="filter_edit_id" name="filter_edit_id" /> > <label for="filter_edit_name">Name:</label> > <input id="filter_edit_name" name="filter_edit_name" type="text" /> >- <label for="filter_edit_opac">Show in OPAC?</label> >- <input type="checkbox" id="filter_edit_opac" name="filter_edit_opac" /> >- <label for="filter_edit_staff_client">Show in staff interface?</label> >- <input type="checkbox" id="filter_edit_staff_client" name="filter_edit_staff_client" /> >+ </div> >+ <div class="mb-3"> >+ <label for="filter_edit_opac"><input type="checkbox" id="filter_edit_opac" name="filter_edit_opac" /> Show in OPAC?</label> >+ </div> >+ <div class="mb-3"> >+ <label for="filter_edit_staff_client"><input type="checkbox" id="filter_edit_staff_client" name="filter_edit_staff_client" /> Show in staff interface?</label> > </div> > </div> > <div class="modal-footer"> >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/auth.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/auth.tt >index e46d7ad41a..4a0a210904 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/auth.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/auth.tt >@@ -59,7 +59,7 @@ > [% INCLUDE 'messages.inc' %] > > <div id="login"> >- <h1><a href="http://koha-community.org">Koha</a></h1> >+ <h1><a href="https://koha-community.org">Koha</a></h1> > > [% SET StaffLoginInstructions = AdditionalContents.get( location => "StaffLoginInstructions", lang => lang ) %] > >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/authorities/authorities.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/authorities/authorities.tt >index 09f997a2af..d9e91e55c0 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/authorities/authorities.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/authorities/authorities.tt >@@ -242,7 +242,7 @@ > if ( elt.nodeName == 'SELECT' ) { > $(elt).siblings('.select2').find("span[role='combobox']").addClass(notFilledClass); > } else { >- elt.setAttribute('class','input_marceditor noEnterSubmit ' + notFilledClass); >+ elt.setAttribute('class','input_marceditor noEnterSubmit framework_plugin ' + notFilledClass); > } > $('#' + subfields[i]).focus(); > tabflag[tag+subfield+tagnumber][1]=label[i]; >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/basket/basket.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/basket/basket.tt >index d763dea352..df4b5fb611 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/basket/basket.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/basket/basket.tt >@@ -39,7 +39,7 @@ > <body id="cart_basket" class="cart"> > > [% WRAPPER 'main-container.inc' wide_centered => 1 %] >- <div class="container"> >+ <div class="container-fluid"> > <h1>Your cart</h1> > > <div id="toolbar" class="btn-toolbar"> >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/batch/print-notices.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/batch/print-notices.tt >index d7ccd43ea5..44a6637d26 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/batch/print-notices.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/batch/print-notices.tt >@@ -14,9 +14,6 @@ > .message { page-break-after: always } > pre { font-family: monospace } > pre {white-space: pre-wrap;} >- pre {white-space: -moz-pre-wrap;} >- pre {white-space: -o-pre-wrap;} >- pre {word-wrap: break-work;} > --> > </style> > <!-- prettier-ignore-end --> >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/detail.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/detail.tt >index 5f026f36d3..0fc818512a 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/detail.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/detail.tt >@@ -1262,6 +1262,11 @@ > [% Asset.js("js/recalls.js") | $raw %] > [% Asset.js("js/coce.js") | $raw %] > [% Asset.js("lib/Chocolat/js/chocolat.js") | $raw %] >+ >+ [%# The following PROCESS needs: %] >+ [%# can_edit_items_from item_type_image_locations %] >+ [% PROCESS build_items_table_js biblio => biblio %] >+ > [% IF ( Koha.Preference('CatalogConcerns') ) %] > <script> > /* Set a variable needed by add_catalog_concern.js */ >@@ -1725,6 +1730,44 @@ > var bundle_lost_value = [% Koha.Preference('BundleLostValue') | html %]; > [% END %] > >+ let items_tab_ids = [ 'holdings', 'otherholdings' ]; >+ items_tab_ids.forEach( function( tab_id, index ) { >+ >+ // Early return if the tab is not shown (ie. no table) >+ if (!$("#%s_table".format(tab_id)).length) return; >+ [% IF Koha.Preference('AlwaysShowHoldingsTableFilters') %] >+ build_items_table(tab_id, true, {}, build_items_table_drawncallback); >+ [% ELSE %] >+ build_items_table(tab_id, false, {}, build_items_table_drawncallback); >+ [% END %] >+ >+ [% IF bundlesEnabled %] >+ // Add event listener for opening and closing bundle details >+ $('#' + tab_id + '_table tbody').on('click', 'button.details-control', function () { >+ var button = $(this); >+ var tr = button.closest('tr'); >+ var dTable = button.closest('table').DataTable({ 'retrieve': true }); >+ >+ let row = dTable.row( tr ); >+ let data = row.data(); >+ let itemnumber = data.item_id; >+ let duedate = (data.checkout&&data.checkout.due_date) || null; >+ >+ if ( row.child.isShown() ) { >+ // This row is already open - close it >+ row.child.hide(); >+ tr.removeClass('shown'); >+ button.removeClass('active'); >+ } else { >+ // Open this row >+ createChild(row, itemnumber, duedate); >+ tr.addClass('shown'); >+ button.addClass('active'); >+ } >+ }); >+ [% END # /IF bundlesEnabled %] >+ }); >+ > $(document).ready(function() { > [% IF bundlesEnabled %] // Bundle handling > function createChild ( row, itemnumber, duedate ) { >@@ -2036,44 +2079,6 @@ > // End bundle handling > [% END # /IF bundlesEnabled %] > >- let items_tab_ids = [ 'holdings', 'otherholdings' ]; >- items_tab_ids.forEach( function( tab_id, index ) { >- >- // Early return if the tab is not shown (ie. no table) >- if (!$("#%s-tab".format(tab_id)).length) return; >- [% IF Koha.Preference('AlwaysShowHoldingsTableFilters') %] >- build_items_table(tab_id, true, {}, build_items_table_drawncallback); >- [% ELSE %] >- build_items_table(tab_id, false, {}, build_items_table_drawncallback); >- [% END %] >- >- [% IF bundlesEnabled %] >- // Add event listener for opening and closing bundle details >- $('#' + tab_id + '_table tbody').on('click', 'button.details-control', function () { >- var button = $(this); >- var tr = button.closest('tr'); >- var dTable = button.closest('table').DataTable({ 'retrieve': true }); >- >- let row = dTable.row( tr ); >- let data = row.data(); >- let itemnumber = data.item_id; >- let duedate = (data.checkout&&data.checkout.due_date) || null; >- >- if ( row.child.isShown() ) { >- // This row is already open - close it >- row.child.hide(); >- tr.removeClass('shown'); >- button.removeClass('active'); >- } else { >- // Open this row >- createChild(row, itemnumber, duedate); >- tr.addClass('shown'); >- button.addClass('active'); >- } >- }); >- [% END # /IF bundlesEnabled %] >- }); >- > [% IF Koha.Preference('AcquisitionDetails') %] > var table_settings = [% TablesSettings.GetTableSettings('catalogue', 'detail', 'acquisitiondetails-table', 'json') | $raw %]; > var acquisitiondetails_table = $("#orders").kohaTable( >@@ -2360,10 +2365,6 @@ > return confirm( _("Are you sure you want to delete this comment?") ); > }); > </script> >- [%# The following PROCESS needs: %] >- [%# can_edit_items_from item_type_image_locations %] >- [% PROCESS build_items_table_js biblio => biblio %] >- > [% CoverImagePlugins | $raw %] > [% END # /jsinclude %] > [% INCLUDE 'intranet-bottom.inc' %] >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/itemsearch.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/itemsearch.tt >index cbfdfa733c..a050fdf52e 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/itemsearch.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/itemsearch.tt >@@ -490,6 +490,7 @@ > + ' <th id="items_itemlost" data-colname="lost_status">' + _("Lost status") + '</th>' > + ' <th id="items_widthdrawn" data-colname="withdrawn_status">' + _("Withdrawn status") + '</th>' > + ' <th id="items_damaged" data-colname="damaged_status">' + _("Damaged status") + '</th>' >+ + ' <th id="items_dateaccessioned" data-colname="dateaccessioned">' + _("Date accessioned") + '</th>' > + ' <th id="items_checkouts" data-colname="checkouts">' + _("Checkouts") + '</th>' > + ' <th id="items_datelastborrowed" data-colname="last_checkout_date">' + _("Last checkout date") + '</th>' > + ' <th id="items_date_due" data-colname="due_date">' + _("Due date") + '</th>' >@@ -685,6 +686,7 @@ > { 'name': 'itemlost' }, > { 'name': 'withdrawn' }, > { 'name': 'damaged' }, >+ { 'name': 'dateaccessioned' }, > { 'name': 'issues' }, > { 'name': 'datelastborrowed' }, > { 'name': 'date_due' }, >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/moredetail.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/moredetail.tt >index e6ca9c6ae9..84526221a9 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/moredetail.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/moredetail.tt >@@ -152,6 +152,16 @@ > [% SET not_for_loan = 1 %] > [% SET not_for_loan_description = AuthorisedValues.GetDescriptionByKohaField( kohafield => 'items.notforloan', authorised_value => item.notforloan ) %] > [% END %] >+ [% IF withdraw_error %] >+ <div class="alert alert-warning"> >+ [% IF withdraw_error == 'intransit_cannot_withdraw' %] >+ Cannot withdraw item in transit. >+ [% END %] >+ [% IF withdraw_error == 'onloan_cannot_withdraw' %] >+ Cannot withdraw checked out item. >+ [% END %] >+ </div> >+ [% END %] > <div class="listgroup"> > <h4> > Item information >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/addbiblio.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/addbiblio.tt >index 003358530c..7d75f7970c 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/addbiblio.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/addbiblio.tt >@@ -576,7 +576,7 @@ > if ( elt.nodeName == 'SELECT' ) { > $(elt).siblings('.select2').find("span[role='combobox']").addClass(notFilledClass); > } else { >- elt.setAttribute('class','input_marceditor noEnterSubmit ' + notFilledClass); >+ elt.setAttribute('class','input_marceditor noEnterSubmit framework_plugin ' + notFilledClass); > } > $('#' + subfields[i]).focus(); > tabflag[tag+subfield+tagnumber][1]=label[i]; >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/addbooks.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/addbooks.tt >index 0e61ec2d8b..030f36306b 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/addbooks.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/addbooks.tt >@@ -237,14 +237,14 @@ > <a class="btn btn-default btn-xs dropdown-toggle" id="reservoirsearchactions[% breeding_loo.id | html %]" role="button" data-bs-toggle="dropdown" href="#"> Actions </a> > <ul class="dropdown-menu dropdown-menu-end" role="menu" aria-labelledby="reservoirsearchactions[% breeding_loo.id | html %]"> > <li >- ><a href="/cgi-bin/koha/catalogue/showmarc.pl?importid=[% breeding_loo.id | uri %]" class="previewData"><i class="fa-solid fa-eye"></i> MARC preview</a></li >+ ><a href="/cgi-bin/koha/catalogue/showmarc.pl?importid=[% breeding_loo.id | uri %]" class="dropdown-item previewData"><i class="fa-solid fa-eye"></i> MARC preview</a></li > > > <li >- ><a href="/cgi-bin/koha/catalogue/showmarc.pl?viewas=card&importid=[% breeding_loo.id | uri %]" class="previewData"><i class="fa-solid fa-eye"></i> Card preview</a></li >+ ><a href="/cgi-bin/koha/catalogue/showmarc.pl?viewas=card&importid=[% breeding_loo.id | uri %]" class="dropdown-item previewData"><i class="fa-solid fa-eye"></i> Card preview</a></li > > > [% IF ( CAN_user_editcatalogue_edit_catalogue ) %] > <li >- ><a href="/cgi-bin/koha/cataloguing/addbiblio.pl?breedingid=[% breeding_loo.id | uri %]"><i class="fa fa-plus"></i> Add biblio</a></li >+ ><a class="dropdown-item" href="/cgi-bin/koha/cataloguing/addbiblio.pl?breedingid=[% breeding_loo.id | uri %]"><i class="fa fa-plus"></i> Add biblio</a></li > > > [% END %] > </ul> >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/additem.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/additem.tt >index 81edf34130..7b55c3c666 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/additem.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/additem.tt >@@ -88,6 +88,8 @@ > <h1>Items for [% biblio.title | html %] [% IF ( biblio.author ) %]by [% biblio.author | html %][% END %] (Record #[% biblio.biblionumber | html %])</h1> > <a id="newitem_jump" href="#f"><i class="fa fa-arrow-down"></i> Jump to form</a> > >+ [% IF ( onloan_cannot_withdraw ) %]<div class="alert alert-warning"><strong>Error saving item</strong>: Onloan item cannot be withdrawn.</div>[% END %] >+ [% IF ( intransit_cannot_withdraw ) %]<div class="alert alert-warning"><strong>Error saving item</strong>: In transit item cannot be withdrawn.</div>[% END %] > [% IF ( barcode_not_unique ) %]<div class="alert alert-warning"><strong>Error saving item</strong>: Barcode must be unique.</div>[% END %] > [% IF ( no_next_barcode ) %]<div class="alert alert-warning"><strong>Error saving items</strong>: Unable to automatically determine values for barcodes. No item has been inserted.</div>[% END %] > [% IF ( book_on_loan ) %]<div class="alert alert-warning"><strong>Cannot delete</strong>: item is checked out.</div>[% END %] >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/value_builder/unimarc_field_181a.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/value_builder/unimarc_field_181a.tt >index 79120a323a..dee4a5b8ed 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/value_builder/unimarc_field_181a.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/value_builder/unimarc_field_181a.tt >@@ -18,7 +18,7 @@ > <div class="page-section"> > <table> > <tr> >- <td><label for="f1">Content Form </label></td> >+ <td><label for="f1">Content form </label></td> > <td> > <select name="f1" id="f1"> > [% IF ( f1 == "a" ) %] >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/value_builder/unimarc_field_181b.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/value_builder/unimarc_field_181b.tt >index 7bbaaa0bcb..bc467e41c6 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/value_builder/unimarc_field_181b.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/value_builder/unimarc_field_181b.tt >@@ -18,7 +18,7 @@ > <div class="page-section"> > <table> > <tr> >- <td><label for="f1">Specification of Type </label></td> >+ <td><label for="f1">Specification of type </label></td> > <td> > <select name="f1" id="f1"> > [% IF ( f1 == "a" ) %] >@@ -54,7 +54,7 @@ > </td> > </tr> > <tr> >- <td><label for="f2">Specification of Motion </label></td> >+ <td><label for="f2">Specification of motion </label></td> > <td> > <select name="f2" id="f2"> > [% IF ( f2 == "a" ) %] >@@ -84,7 +84,7 @@ > </td> > </tr> > <tr> >- <td><label for="f3">Specification of Dimensionality </label></td> >+ <td><label for="f3">Specification of dimensionality </label></td> > <td> > <select name="f3" id="f3"> > [% IF ( f3 == "a" ) %] >@@ -114,7 +114,7 @@ > </td> > </tr> > <tr> >- <td><label for="f4">Sensory Specification 1 </label></td> >+ <td><label for="f4">Sensory specification 1 </label></td> > <td> > <select name="f4" id="f4"> > [% IF ( f4 == "a" ) %] >@@ -156,7 +156,7 @@ > </td> > </tr> > <tr> >- <td><label for="f5">Sensory Specification 2 </label></td> >+ <td><label for="f5">Sensory specification 2 </label></td> > <td> > <select name="f5" id="f5"> > [% IF ( f5 == "a" ) %] >@@ -198,7 +198,7 @@ > </td> > </tr> > <tr> >- <td><label for="f6">Sensory Specification 3 </label></td> >+ <td><label for="f6">Sensory specification 3 </label></td> > <td> > <select name="f6" id="f6"> > [% IF ( f6 == "a" ) %] >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/value_builder/unimarc_field_181c.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/value_builder/unimarc_field_181c.tt >index 3e8d5c2619..562e54d311 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/value_builder/unimarc_field_181c.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/value_builder/unimarc_field_181c.tt >@@ -18,7 +18,7 @@ > <div class="page-section"> > <table> > <tr> >- <td><label for="f1">Content Type </label></td> >+ <td><label for="f1">Content type </label></td> > <td> > <select name="f1" id="f1"> > [% IF ( f1 == "cri" ) %] >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/value_builder/unimarc_field_182a.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/value_builder/unimarc_field_182a.tt >index b26e3763ab..2179e6ef71 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/value_builder/unimarc_field_182a.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/value_builder/unimarc_field_182a.tt >@@ -18,7 +18,7 @@ > <div class="page-section"> > <table> > <tr> >- <td><label for="f1">Media Type Code </label></td> >+ <td><label for="f1">Media type code </label></td> > <td> > <select name="f1" id="f1"> > [% IF ( f1 == "a" ) %] >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/value_builder/unimarc_field_182c.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/value_builder/unimarc_field_182c.tt >index 97b7c64b17..54482ec020 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/value_builder/unimarc_field_182c.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/value_builder/unimarc_field_182c.tt >@@ -18,7 +18,7 @@ > <div class="page-section"> > <table> > <tr> >- <td><label for="f1">Media Type </label></td> >+ <td><label for="f1">Media type </label></td> > <td> > <select name="f1" id="f1"> > [% IF ( f1 == "s" ) %] >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/value_builder/unimarc_field_183a.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/value_builder/unimarc_field_183a.tt >index a3dffcfbb6..0ac57580bc 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/value_builder/unimarc_field_183a.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/value_builder/unimarc_field_183a.tt >@@ -19,7 +19,7 @@ > <tr> > <td> > <select name="f1" id="f1"> >- <optgroup label="Audio Carriers"> >+ <optgroup label="Audio carriers"> > [% IF ( f1 == "sg" ) %] > <option value="sg" selected="selected">sg - audio cartridge</option> > [% ELSE %] >@@ -81,7 +81,7 @@ > [% END %] > </optgroup> > >- <optgroup label="Computer Carriers"> >+ <optgroup label="Computer carriers"> > [% IF ( f1 == "ck" ) %] > <option value="ck" selected="selected">ck - computer card</option> > [% ELSE %] >@@ -137,7 +137,7 @@ > [% END %] > </optgroup> > >- <optgroup label="Microform Carriers"> >+ <optgroup label="Microform carriers"> > [% IF ( f1 == "ha" ) %] > <option value="ha" selected="selected">ha - aperture card</option> > [% ELSE %] >@@ -199,7 +199,7 @@ > [% END %] > </optgroup> > >- <optgroup label="Microscopic Carriers"> >+ <optgroup label="Microscopic carriers"> > [% IF ( f1 == "pp" ) %] > <option value="pp" selected="selected">pp - microscope slide</option> > [% ELSE %] >@@ -275,7 +275,7 @@ > [% END %] > </optgroup> > >- <optgroup label="Stereographic Carriers"> >+ <optgroup label="Stereographic carriers"> > [% IF ( f1 == "eh" ) %] > <option value="eh" selected="selected">eh - stereograph card</option> > [% ELSE %] >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/circ/article-requests.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/circ/article-requests.tt >index 22a1f994a0..a92f177f8d 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/circ/article-requests.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/circ/article-requests.tt >@@ -524,7 +524,7 @@ > [% INCLUDE 'datatables.inc' %] > <script> > var active_tab = "#article-requests-requested_panel"; >- var last_cancel_reason, requested_datatable, pending_datatable, processing_datatable, active_datatable; >+ var last_cancel_reason, requested_datatable, pending_datatable, processing_datatable, active_datatable, requested_dt, pending_dt, processing_dt; > $(document).ready(function() { > > $("#article-request-tabs a[data-bs-toggle='tab']").on("shown.bs.tab", function (e) { >@@ -575,8 +575,14 @@ > }); > > requested_datatable = $("#article-requests-requested-table").kohaTable(); >+ requested_dt = requested_datatable.DataTable(); >+ > pending_datatable = $("#article-requests-pending-table").kohaTable(); >+ pending_dt = pending_datatable.DataTable(); >+ > processing_datatable = $("#article-requests-processing-table").kohaTable(); >+ processing_dt = processing_datatable.DataTable(); >+ > active_datatable = requested_datatable; > activateBatchActions( active_tab ); > }); >@@ -634,12 +640,11 @@ > > > a.closest('td').prepend('<img src="[% interface | html %]/[% theme | html %]/img/spinner-small.gif"/>').find('div.dropdown').hide(); >- > $.ajax({ > type: "DELETE", > url: '/api/v1/article_requests/'+id+query, > success: function( data ) { >- active_datatable.row( a.closest('tr') ).remove().draw(); >+ active_datatable.DataTable().row( table_row ).remove().draw(); > UpdateTabCounts(); > activateBatchActions( active_tab ); > } >@@ -674,7 +679,7 @@ > async function Process( id, a ) { > var table_row = a.closest('tr'); > var table = a.closest('table'); >- var orig_datatable = table.attr('id')==='article-requests-pending-table'?pending_datatable:requested_datatable; >+ var orig_dt = table.attr('id')==='article-requests-pending-table'?pending_dt:requested_dt; > > a.closest('td').prepend('<img src="[% interface | html %]/[% theme | html %]/img/spinner-small.gif" class="spinner"/>').find('div.dropdown').hide(); > >@@ -684,8 +689,8 @@ > $("img.spinner").remove(); > table_row.find('.ar-process-request').remove(); > table_row.find('input[type="checkbox"]').prop('checked', false); >- orig_datatable.row( table_row ).remove().draw(); >- processing_datatable.row.add( table_row ).draw(); >+ orig_dt.row( table_row ).remove().draw(); >+ processing_dt.row.add( table_row ).draw(); > UpdateTabCounts(); > activateBatchActions( active_tab ); > }, >@@ -711,7 +716,7 @@ > await client.articleRequests.complete(id).then( > success => { > $("img.spinner").remove(); >- active_datatable.row( a.closest('tr') ).remove().draw(); >+ active_datatable.DataTable().row( a.closest('tr') ).remove().draw(); > UpdateTabCounts(); > activateBatchActions( active_tab ); > }, >@@ -734,8 +739,8 @@ > await client.articleRequests.pending(id).then( > success => { > $("img.spinner").remove(); >- requested_datatable.row( table_row ).remove().draw(); >- pending_datatable.row.add( table_row ).draw(); >+ requested_dt.row( table_row ).remove().draw(); >+ pending_dt.row.add( table_row ).draw(); > UpdateTabCounts(); > activateBatchActions( active_tab ); > }, >@@ -750,9 +755,9 @@ > }); > > function UpdateTabCounts() { >- $("#ar_requested_count").html( requested_datatable.rows().count() ); >- $("#ar_pending_count").html( pending_datatable.rows().count() ); >- $("#ar_processing_count").html( processing_datatable.rows().count() ); >+ $("#ar_requested_count").html( requested_dt.rows().count() ); >+ $("#ar_pending_count").html( pending_dt.rows().count() ); >+ $("#ar_processing_count").html( processing_dt.rows().count() ); > } > > function EditURLs(id) { >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/circ/branchoverdues.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/circ/branchoverdues.tt >index e121a4600c..cb30349577 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/circ/branchoverdues.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/circ/branchoverdues.tt >@@ -23,7 +23,7 @@ > <a href="/cgi-bin/koha/circ/circulation-home.pl">Circulation</a> > [% END %] > [% WRAPPER breadcrumb_item bc_active= 1 %] >- [% tx('Overdues at with fines {library}', { library = Branches.GetLoggedInBranchname }) %] >+ [% tx('Overdues with fines at {library}', { library = Branches.GetLoggedInBranchname }) %] > [% END %] > [% END #/ WRAPPER breadcrumbs %] > [% END #/ WRAPPER sub-header.inc %] >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/circ/pendingreserves.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/circ/pendingreserves.tt >index a812fe2d5e..af828b6ec6 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/circ/pendingreserves.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/circ/pendingreserves.tt >@@ -298,7 +298,7 @@ > let options = [... new Set(column > .data() > .toArray() >- .map(d => d.replace(regex, '').trim().split(/\n/gi).flat()) >+ .map(d => d.replace(regex, '').trim().split(/\n/gi).map(s => s.trim()).flat()) > .flat() > .sort())]; > >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/circ/transferstoreceive.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/circ/transferstoreceive.tt >index ef9960ffcc..2bec7d4239 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/circ/transferstoreceive.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/circ/transferstoreceive.tt >@@ -78,6 +78,9 @@ > [% BLOCK subject %]Hold:[% END %] > <a href="mailto:[% reser.patron.notice_email_address | uri %]?subject=[% INCLUDE subject %] [% reser.title | uri %]"> [% reser.patron.notice_email_address | html %] </a> > [% END %] >+ [% IF reser.patron.cardnumber %] >+ <div class="cardnumber">Card number: [% reser.patron.cardnumber | html %]</div> >+ [% END %] > [% ELSIF ( reser.recall ) %] > Recall requested by > <a href="/cgi-bin/koha/members/moremember.pl?borrowernumber=[% reser.recall.patron_id | uri %]" >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/circ/waitingreserves.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/circ/waitingreserves.tt >index 1de9bd9eac..8c5b6f337b 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/circ/waitingreserves.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/circ/waitingreserves.tt >@@ -15,6 +15,15 @@ > [% END %]</title > > > [% INCLUDE 'doc-head-close.inc' %] >+[% FILTER collapse %] >+ <style> >+ .tab-toolbar { >+ padding: 0.5rem 0; >+ margin-bottom: 0.5rem; >+ border-bottom: 1px solid #eee; >+ } >+ </style> >+[% END %] > </head> > > <body id="circ_waitingreserves" class="circ"> >@@ -97,8 +106,8 @@ > [% WRAPPER tab_panels %] > [% WRAPPER tab_panel tabname="holdswaiting" bt_active= 1 %] > [% IF ( reserveloop ) %] >- <div id="toolbar" class="btn-toolbar"> >- <button class="btn cancel_selected_holds" data-bulk="true"></button> >+ <div class="tab-toolbar"> >+ <button class="btn btn-default cancel_selected_holds" data-bulk="true"></button> > </div> > [% INCLUDE waiting_holds.inc table_name='holdst' reserveloop=reserveloop tab='holdwaiting' %] > [% ELSE %] >@@ -108,8 +117,8 @@ > > [% WRAPPER tab_panel tabname="holdsover" %] > [% IF ( overloop ) %] >- <div id="toolbar" class="btn-toolbar"> >- <button class="btn cancel_selected_holds" data-bulk="true"></button> >+ <div class="tab-toolbar"> >+ <button class="btn btn-default cancel_selected_holds" data-bulk="true"></button> > <div class="btn-group"> > <form name="cancelAllReserve" action="waitingreserves.pl" method="post"> > [% INCLUDE 'csrf-token.inc' %] >@@ -117,15 +126,22 @@ > <input type="hidden" name="allbranches" value="[% allbranches | html %]" /> > <input type="hidden" name="tab" value="holdsover" /> > [% IF TransferWhenCancelAllWaitingHolds %] >- <input type="submit" class="btn btn-primary" value="Cancel and transfer all" /> >+ <button type="submit" class="btn btn-primary">Cancel and transfer all</button> > [% ELSE %] >- <input type="submit" class="btn btn-primary" value="Cancel all" /> >+ <button type="submit" class="btn btn-primary">Cancel all</button> > [% END %] > </form> > </div> > </div> > [% UNLESS TransferWhenCancelAllWaitingHolds %] >- <div class="hint">Only items that need not be transferred will be cancelled (TransferWhenCancelAllWaitingHolds syspref)</div> >+ <div class="hint" >+ >Only items that need not be transferred will be cancelled >+ [% IF ( CAN_user_parameters_manage_sysprefs ) %] >+ (<a href="/cgi-bin/koha/admin/preferences.pl?op=search&searchfield=TransferWhenCancelAllWaitingHolds">TransferWhenCancelAllWaitingHolds</a> system preference) >+ [% ELSE %] >+ (TransferWhenCancelAllWaitingHolds system preference) >+ [% END %] >+ </div> > [% END %] > [% INCLUDE waiting_holds.inc table_name='holdso' reserveloop=overloop tab='holdsover' %] > [% ELSE %] >@@ -135,8 +151,8 @@ > > [% WRAPPER tab_panel tabname="holdscancelled" %] > [% IF cancel_reqs_count %] >- <div id="toolbar" class="btn-toolbar"> >- <button class="btn cancel_selected_holds" data-bulk="true"></button> >+ <div class="tab-toolbar"> >+ <button class="btn btn-default cancel_selected_holds" data-bulk="true"></button> > </div> > [% INCLUDE waiting_holds.inc select_column='1' table_name='holdscr' reserveloop=cancel_reqs tab='holdscr' %] > [% ELSE %] >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/clubs/patron-enroll.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/clubs/patron-enroll.tt >index a25dedbf67..3f37ed910b 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/clubs/patron-enroll.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/clubs/patron-enroll.tt >@@ -2,7 +2,7 @@ > > <h1 id="heading"> Enroll in <em>[% club.name | html %]</em> </h1> > >-<div class="container"> >+<div class="container-fluid"> > <form method="get" id="patron-enrollment-form"> > <input type="hidden" name="id" value="[% club.id | html %]" /> > <input type="hidden" name="borrowernumber" value="[% borrowernumber | html %]" /> >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/members/holdshistory.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/members/holdshistory.tt >index 970bd4d4ad..602338097b 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/members/holdshistory.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/members/holdshistory.tt >@@ -45,7 +45,7 @@ > > [% UNLESS Koha.Preference('IntranetReadingHistoryHolds') %] > <div class="alert alert-warning">Staff members are not allowed to access patron's holds history</div> >- [% ELSIF is_anonymous %] >+ [% ELSIF patron.is_anonymous %] > <div class="alert alert-warning">This is the anonymous patron, so no holds history is displayed.</div> > [% ELSE %] > >@@ -97,6 +97,7 @@ > <th>Requested item type</th> > [% END %] > <th>Status</th> >+ <th>Notes</th> > </tr> > </thead> > </table> >@@ -141,6 +142,7 @@ > <th>Requested item type</th> > [% END %] > <th>Status</th> >+ <th>Notes</th> > </tr> > </thead> > </table> >@@ -332,6 +334,14 @@ > return _("Pending"); > } > }, >+ { >+ data: "notes", >+ searchable: true, >+ orderable: true, >+ render: function (data, type, row, meta) { >+ return row.notes; >+ } >+ }, > ], > }, table_settings, true, additional_filters); > } >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/members/member-flags.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/members/member-flags.tt >index 2299ab5301..29c1e31c10 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/members/member-flags.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/members/member-flags.tt >@@ -54,7 +54,7 @@ > <a id="UncheckAllFlags" class="btn btn-link" href="#"> <i class="fa fa-times"></i> Clear all </a> > > <div class="btn-group"> >- <button id="copyPermissions" class="btn btn-link"><i class="fa fa-copy"></i> Copy settings</button> >+ <button id="copyPermissions" class="btn btn-link"><i class="fa fa-copy"></i> Copy permissions</button> > <button class="btn btn-default dropdown-toggle" data-bs-toggle="dropdown"><span class="caret"></span></button> > <ul class="dropdown-menu"> > <li><a id="clearCopied" href="#">Forget copied permissions</a></li> >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/members/memberentrygen.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/members/memberentrygen.tt >index 361a178107..4f86e49e18 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/members/memberentrygen.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/members/memberentrygen.tt >@@ -489,12 +489,12 @@ > <li class="guarantor-details" data-borrowernumber="[% r.guarantor_id | $raw %]"> > <span class="label">Guarantor:</span> > [% INCLUDE 'patron-title.inc' patron=r.guarantor hide_patron_infos_if_needed=1 %] >- <input type="hidden" class="new_guarantor_id relation-[% r.id | html %]" name="new_guarantor_id" value="[% r.guarantor_id | html %]" /> >+ <input type="hidden" class="guarantor_id relation-[% r.id | html %]" name="guarantor_id" value="[% r.guarantor_id | html %]" /> > </li> > <li> > <span class="label">Relationship:</span> > <span>[% r.relationship | html %]</span> >- <input type="hidden" class="new_guarantor_relationship relation-[% r.id | html %]" name="new_guarantor_relationship" value="[% r.relationship | html %]" /> >+ <input type="hidden" class="guarantor_relationship relation-[% r.id | html %]" name="guarantor_relationship" value="[% r.relationship | html %]" /> > </li> > > <li> >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/members/moremember.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/members/moremember.tt >index f1fdb91537..83f4b8893b 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/members/moremember.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/members/moremember.tt >@@ -50,7 +50,7 @@ > > [% INCLUDE 'members-toolbar.inc' %] > >- [% IF is_anonymous %] >+ [% IF patron.is_anonymous %] > <div class="alert alert-warning">This is the anonymous patron.</div> > [% END %] > >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/reports/guided_reports_start.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/reports/guided_reports_start.tt >index c61553b88b..ade44dd2c7 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/reports/guided_reports_start.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/reports/guided_reports_start.tt >@@ -3,6 +3,7 @@ > [% USE AuthorisedValues %] > [% USE KohaDates %] > [% USE Koha %] >+[% USE Branches %] > [% USE TablesSettings %] > [% USE HtmlScrubber %] > [% USE JSON.Escape %] >@@ -850,6 +851,7 @@ > <input type="hidden" name="reportname" value="[% reportname | html %]" /> > <input type="hidden" name="group" value="[% group | html %]" /> > <input type="hidden" name="subgroup" value="[% subgroup | html %]" /> >+ <input type="hidden" name="branches" value="[% branches | html %] /> > <input type="hidden" name="notes" value="[% notes | scrub_html type => 'note' | $raw %]" /> > <input type="hidden" name="cache_expiry" value="[% cache_expiry | html %]" /> > <input type="hidden" name="cache_expiry_units" value="[% cache_expiry_units | html %]" /> >@@ -1434,7 +1436,18 @@ > <span class="required">Required</span> > </div> > </fieldset> >- >+ [% IF ( Koha.Preference('EnableFilteringReports') ) %] >+ <fieldset class="rows"> >+ <legend>Library limitation:</legend> >+ <div >+ ><label for="library_limitation">Library limitation: </label> >+ <select id="library_limitation" name="branches" multiple size="10"> >+ [% PROCESS options_for_libraries libraries => Branches.all( selected => report.get_library_limits, unfiltered => 1, do_not_select_my_library => 1 ) %] >+ </select> >+ <div class="hint">Limits the use of this report to the selected libraries.</div> >+ </div> >+ </fieldset> >+ [% END %] > <fieldset class="action"> > <input type="hidden" name="op" value="cud-save" /> > <input type="submit" name="submit" class="btn btn-primary" value="Save report" /> >@@ -1548,7 +1561,18 @@ > <br /> > <span class="required" style="margin-left:30px;">Required</span> > </fieldset> >- >+ [% IF ( Koha.Preference('EnableFilteringReports') ) %] >+ <fieldset class="rows"> >+ <legend>Library limitation:</legend> >+ <div >+ ><label for="library_limitation">Library limitation: </label> >+ <select id="library_limitation" name="branches" multiple size="10"> >+ [% PROCESS options_for_libraries libraries => Branches.all( selected => report.get_library_limits, unfiltered => 1, do_not_select_my_library => 1 ) %] >+ </select> >+ <div class="hint">Limits the use of this report to the selected libraries.</div> >+ </div> >+ </fieldset> >+ [% END %] > <fieldset class="action"> > <button class="btn btn-primary" type="submit" name="op" value="cud-update_sql">Update SQL</button> > <button class="btn btn-default" type="submit" name="op" value="cud-update_and_run_sql">Update and run SQL</button> >@@ -1984,6 +2008,9 @@ > $(document).ready(function(){ > > var activeTab = localStorage.getItem("sql_reports_activetab"); >+ if( activeTab == 0 ){ >+ $("#subgroup_filter_block").hide(); >+ } > > $("body").on('click',".fetch_chart_data",function(){ > if( [% unlimited_total || 0 | $raw %] > 1000 ){ >@@ -2492,13 +2519,17 @@ > > if (g_id && g_id.length > 0) { > col4.search(g_id, {exact:true}).visible(false); >- for(var i in group_subgroups[g_id]) { >- $("#subgroup_filter").append( >- '<option value="' + group_subgroups[g_id][i][0] + '">' >- + group_subgroups[g_id][i][1] + '</option>' >- ); >+ if( group_subgroups[g_id] && group_subgroups[g_id].length > 0 ){ >+ for(var i in group_subgroups[g_id]) { >+ $("#subgroup_filter").append( >+ '<option value="' + group_subgroups[g_id][i][0] + '">' >+ + group_subgroups[g_id][i][1] + '</option>' >+ ); >+ } >+ $("#subgroup_filter_block").show(); >+ } else { >+ $("#subgroup_filter_block").hide(); > } >- $("#subgroup_filter_block").show(); > } else { > $("#subgroup_filter_block").hide(); > } >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/reports/issues_stats.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/reports/issues_stats.tt >index 4ef937e635..53036c74e5 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/reports/issues_stats.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/reports/issues_stats.tt >@@ -1,6 +1,9 @@ > [% USE raw %] > [% USE Koha %] > [% USE Branches %] >+[% USE Categories %] >+[% USE ItemTypes %] >+[% USE AuthorisedValues %] > [% PROCESS 'i18n.inc' %] > [% SET footerjs = 1 %] > [% INCLUDE 'doc-head-open.inc' %] >@@ -67,37 +70,66 @@ > [% END %] > [% SWITCH loopfilte.crit %] > [% CASE 0 %] >- <span>Period from [% loopfilte.filter | html %]</span> >+ <strong>Period from:</strong> [% loopfilte.filter | html %] > [% CASE 1 %] >- <span>Period to [% loopfilte.filter | html %]</span> >+ <strong>Period to:</strong> [% loopfilte.filter | html %] > [% CASE 2 %] >- <span>Patron category = [% loopfilte.filter | html %]</span> >+ <strong>Patron category:</strong> [% Categories.GetName( loopfilte.filter ) | html %] > [% CASE 3 %] >- <span>Item type = [% loopfilte.filter | html %]</span> >+ <strong>Item type:</strong> [% ItemTypes.GetDescription( loopfilte.filter ) | html %] > [% CASE 4 %] >- <span>Issuing library = [% loopfilte.filter | html %]</span> >+ <strong>Issuing library:</strong> [% Branches.GetName( loopfilte.filter ) | html %] > [% CASE 5 %] >- <span>Collection = [% loopfilte.filter | html %]</span> >+ <strong>Collection:</strong> [% AuthorisedValues.GetByCode( 'CCODE', loopfilte.filter ) | html %] > [% CASE 6 %] >- <span>Location = [% loopfilte.filter | html %]</span> >+ <strong>Location:</strong> [% AuthorisedValues.GetByCode( 'LOC', loopfilte.filter ) | html %] > [% CASE 7 %] >- <span>Item call number >= [% loopfilte.filter | html %]</span> >+ <strong>Item call number > [% loopfilte.filter | html %] </strong> > [% CASE 8 %] >- <span>Item call number < [% loopfilte.filter | html %]</span> >+ <strong>Item call number < [% loopfilte.filter | html %] </strong> > [% CASE 9 %] >- <span>Patron sort1 = [% loopfilte.filter | html %]</span> >+ <strong>Patron sort1:</strong> [% AuthorisedValues.GetByCode('Bsort1', loopfilte.filter ) | html %] > [% CASE 10 %] >- <span>Patron sort2 = [% loopfilte.filter | html %]</span> >+ <strong>Patron sort2:</strong> [% AuthorisedValues.GetByCode('Bsort2', loopfilte.filter ) | html %] > [% CASE 11 %] >- <span>Home library = [% loopfilte.filter | html %]</span> >+ <strong>Home library:</strong> [% Branches.GetName( loopfilte.filter ) | html %] > [% CASE 12 %] >- <span>Holding library = [% loopfilte.filter | html %]</span> >+ <strong>Holding library:</strong> [% Branches.GetName( loopfilte.filter ) | html %] > [% CASE 13 %] >- <span>Patron library = [% loopfilte.filter | html %]</span> >+ <strong>Patron library:</strong> [% Branches.GetName( loopfilte.filter ) | html %] > [% CASE 14 %] >- <span>Issuing library = [% loopfilte.filter | html %]</span> >+ <strong>Issuing library:</strong> [% Branches.GetName( loopfilte.filter ) | html %] >+ [% CASE "Event" %] >+ <strong>Event:</strong> >+ [% SWITCH ( loopfilte.filter ) %] >+ [% CASE "issue" %] >+ <span>Checkout</span> >+ [% CASE "return" %] >+ <span>Check-in</span> >+ [% CASE "renew" %] >+ <span>Renewal</span> >+ [% CASE %] >+ [% loopfilte.filter | html %] >+ [% END %] >+ [% CASE "Select Day" %] >+ <strong>Select day:</strong> [% loopfilte.filter | html %] >+ [% CASE "Select Month" %] >+ <strong>Select month:</strong> [% loopfilte.filter | html %] >+ [% CASE "Display by" %] >+ <strong>Group by:</strong> >+ [% SWITCH ( loopfilte.filter ) %] >+ [% CASE "1" %] >+ <span>Day of week</span> >+ [% CASE "2" %] >+ <span>Month</span> >+ [% CASE "3" %] >+ <span>Year</span> >+ [% CASE "4" %] >+ <span>Hour</span> >+ [% END %] > [% CASE %] >- <span>[% loopfilte.crit | html %] = [% loopfilte.filter | html %]</span> >+ [% loopfilte.crit | html %]: >+ [% loopfilte.filter | html %] > [% END %] > </li> > [% END %] >@@ -336,11 +368,9 @@ > <td><input type="radio" name="Line" value="borrowers.branchcode" /></td> > <td><input type="radio" name="Column" value="borrowers.branchcode" /></td> > <td> >- <select name="Filter" id="patronbranch"> >- <option value=""> </option> >- [% FOREACH branch IN branchloop %] >- <option value="[% branch.value | html %]"> [% branch.branchname | html %] </option> >- [% END %] >+ <select name="Filter" id="patron_library"> >+ <option value=""> </option> >+ [% PROCESS options_for_libraries libraries => Branches.all() %] > </select> > </td> > </tr> >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/reports/itemslost.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/reports/itemslost.tt >index 88538ebea6..8f87e2161b 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/reports/itemslost.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/reports/itemslost.tt >@@ -83,7 +83,7 @@ > [% IF csv_profiles.count %] > <th class="no-sort"></th> > [% END %] >- <th>Title</th> >+ <th class="anti-the">Title</th> > <th>Author</th> > <th>Lost status</th> > <th>Lost on</th> >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/reports/orders_by_budget.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/reports/orders_by_budget.tt >index 71ae52d44e..4f3710ab61 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/reports/orders_by_budget.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/reports/orders_by_budget.tt >@@ -58,7 +58,7 @@ > <th>Basket</th> > <th>Basket name</th> > <th>Basket by</th> >- <th>Title</th> >+ <th class="anti-the">Title</th> > <th>Currency</th> > <th>List price</th> > <th>RRP</th> >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/reports/reports-home.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/reports/reports-home.tt >index 2d8517da1c..684b5362e5 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/reports/reports-home.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/reports/reports-home.tt >@@ -1,5 +1,6 @@ > [% USE raw %] > [% USE Koha %] >+[% USE AdditionalContents %] > [% PROCESS 'i18n.inc' %] > [% INCLUDE 'doc-head-open.inc' %] > <title >@@ -109,12 +110,12 @@ > <li><a href="/cgi-bin/koha/reports/issues_avg_stats.pl">Average loan time</a></li> > [% SET koha_version = Koha.Version %] > [% IF koha_version.development %] >- <li><a href="http://schema.koha-community.org/main" target="blank">Koha database schema</a></li> >+ <li><a href="https://schema.koha-community.org/main" target="blank">Koha database schema</a></li> > [% ELSE %] >- <li><a href="http://schema.koha-community.org/[% koha_version.major | uri %]_[% koha_version.minor | uri %]" target="blank">Koha database schema</a></li> >+ <li><a href="https://schema.koha-community.org/[% koha_version.major | uri %]_[% koha_version.minor | uri %]" target="blank">Koha database schema</a></li> > [% END %] > >- <li><a href="http://wiki.koha-community.org/wiki/SQL_Reports_Library" target="blank">Koha reports library</a></li> >+ <li><a href="https://wiki.koha-community.org/wiki/SQL_Reports_Library" target="blank">Koha reports library</a></li> > </ul> > </div> > [% IF ( Koha.Preference('Mana') == 2 ) %] >@@ -126,10 +127,26 @@ > </div> > </div> > <!-- /.row --> >+ >+ [% SET StaffReportsHome = AdditionalContents.get( location => "StaffReportsHome", lang => lang, library => Branches.GetLoggedInBranchcode || default_branch ) %] >+ >+ [% IF ( StaffReportsHome.content && StaffReportsHome.content.count > 0 ) %] >+ <div class="row"> >+ <div class="col-sm-12"> >+ <div id="[% StaffReportsHome.location | html %]"> >+ [% FOREACH n IN StaffReportsHome.content %] >+ <div class="[% n.lang | html %]_item"> >+ <div class="[% n.lang | html %]_body">[% n.content | $raw %]</div> >+ </div> >+ [% END %] >+ </div> >+ <!-- /#StaffReportsHome --> >+ </div> >+ <!-- /.col-sm-12 --> >+ </div> >+ <!-- /.row --> >+ [% END # /IF StaffReportsHome %] > </div> > </div> >- <div class="row"> >- <div class="col-md-10 offset-md-1 col-lg-8 offset-lg-2" id="intranet-reports-home-html"> [% Koha.Preference('IntranetReportsHomeHTML') | $raw %] </div> >- </div> > </div> > [% INCLUDE 'intranet-bottom.inc' %] >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/serials/serials-search.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/serials/serials-search.tt >index 1ad0ab6fef..fe5eb4de85 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/serials/serials-search.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/serials/serials-search.tt >@@ -136,19 +136,17 @@ > [% IF closed %] > <a class="btn btn-default btn-xs dropdown-toggle" id="closedsubactions[% subscription.subscriptionid | html %]" role="button" data-bs-toggle="dropdown" href="#"> Actions </a> > <ul class="dropdown-menu" role="menu" aria-labelledby="closedsubactions[% subscription.subscriptionid | html %]"> >- [% IF ( routing && CAN_user_serials_routing ) %] >- [% UNLESS ( subscription.cannotedit ) %] >- <li> >- <a >- class="dropdown-item" >- href="/cgi-bin/koha/serials/serials-search.pl?subscriptionid=[% subscription.subscriptionid | uri %]&op=reopen&routing=[% subscription.routing | uri %]&searched=1&title_filter=[% title_filter | uri %]&ISSN_filter=[% ISSN_filter | uri %]&EAN_filter=[% EAN_filter | uri %]&published_filter=[% publisher_filter | uri %]&bookseller_filter=[% bookseller_filter | uri %]&branch_filter=[% branch_filter | uri %]" >- id="reopensub" >- > >- <i class="fa-solid fa-arrow-rotate-right"></i> Reopen</a >- > >- </li> >- [% END %] >- [% END # IF ( routing && CAN_user_serials_routing ) %] >+ [% UNLESS ( subscription.cannotedit ) %] >+ <li> >+ <a >+ class="dropdown-item" >+ href="/cgi-bin/koha/serials/serials-search.pl?subscriptionid=[% subscription.subscriptionid | uri %]&op=reopen&[% IF ( routing && CAN_user_serials_routing ) %]routing=[% subscription.routing | uri %]&[% END %]searched=1&title_filter=[% title_filter | uri %]&ISSN_filter=[% ISSN_filter | uri %]&EAN_filter=[% EAN_filter | uri %]&published_filter=[% publisher_filter | uri %]&bookseller_filter=[% bookseller_filter | uri %]&branch_filter=[% branch_filter | uri %]" >+ id="reopensub" >+ > >+ <i class="fa-solid fa-arrow-rotate-right"></i> Reopen</a >+ > >+ </li> >+ [% END %] > > <li> > <a class="dropdown-item" href="/cgi-bin/koha/serials/serials-collection.pl?subscriptionid=[% subscription.subscriptionid | uri %]"><i class="fa-solid fa-rectangle-list"></i> Issue history</a> >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/tools/additional-contents.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/tools/additional-contents.tt >index 330b5522ac..f62690eb20 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/tools/additional-contents.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/tools/additional-contents.tt >@@ -542,7 +542,7 @@ > [% END %] > [% END %] > </optgroup> >- [% SET staff_available_options = [ 'IntranetmainUserblock', 'RoutingListNote', 'StaffAcquisitionsHome', 'StaffAuthoritiesHome', 'StaffCataloguingHome', 'StaffListsHome', 'StaffLoginInstructions', 'StaffPatronsHome', 'StaffPOSHome', 'StaffSerialsHome' ] %] >+ [% SET staff_available_options = [ 'IntranetmainUserblock', 'StaffReportsHome', 'RoutingListNote', 'StaffAcquisitionsHome', 'StaffAuthoritiesHome', 'StaffCataloguingHome', 'StaffListsHome', 'StaffLoginInstructions', 'StaffPatronsHome', 'StaffPOSHome', 'StaffSerialsHome' ] %] > <optgroup label="Staff interface"> > [% FOREACH l IN staff_available_options.sort %] > [% IF l == location %] >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/tools/batch_extend_due_dates.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/tools/batch_extend_due_dates.tt >index e975b76132..26787e4662 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/tools/batch_extend_due_dates.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/tools/batch_extend_due_dates.tt >@@ -272,6 +272,7 @@ > dom: "t", > order: [], > paging: false, >+ pageLength: -1, > }); > > $("#extend_due_dates_form").on("submit", function (e) { >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/tools/stage-marc-import.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/tools/stage-marc-import.tt >index d5ff4771b5..e42fde81c6 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/tools/stage-marc-import.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/tools/stage-marc-import.tt >@@ -471,7 +471,7 @@ > $('#fileuploadbutton').show(); > $("#fileuploadcancel").hide(); > var filename=$('#fileToUpload').prop('files')[0].name; >- if( filename.match( new RegExp(/\.[^.]+xml$/) ) ) { >+ if( filename.match( new RegExp(/[^.]+\.xml$/) ) ) { > $('#format').val('MARCXML'); > } > $("#processfile").show(); >diff --git a/koha-tmpl/intranet-tmpl/prog/en/xslt/UNIMARCslimUtils.xsl b/koha-tmpl/intranet-tmpl/prog/en/xslt/UNIMARCslimUtils.xsl >index e344fdf7de..57494591f0 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/xslt/UNIMARCslimUtils.xsl >+++ b/koha-tmpl/intranet-tmpl/prog/en/xslt/UNIMARCslimUtils.xsl >@@ -158,7 +158,7 @@ > <xsl:template name="tag_214_s"> > <xsl:if test="marc:datafield[@tag=214]/marc:subfield[@code='s']"> > <span class="results_summary tag_214_s"> >- <span class="label">Printing and/or Publishing Information Transcribed as Found in the Colophon: </span> >+ <span class="label">Printing and/or publishing information transcribed as found in the colophon: </span> > <xsl:for-each select="marc:datafield[@tag=214]/marc:subfield[@code='s']"> > <xsl:value-of select="."/> > <xsl:choose> >@@ -177,7 +177,7 @@ > <xsl:template name="tag_214_r"> > <xsl:if test="marc:datafield[@tag=214]/marc:subfield[@code='r']"> > <span class="results_summary tag_214_r"> >- <span class="label">Printing and/or Publishing Information Transcribed as Found in the Main Source of Information: </span> >+ <span class="label">Printing and/or publishing information transcribed as found in the main source of information: </span> > <xsl:for-each select="marc:datafield[@tag=214]/marc:subfield[@code='r']"> > <xsl:value-of select="."/> > <xsl:choose> >diff --git a/koha-tmpl/intranet-tmpl/prog/js/datatables.js b/koha-tmpl/intranet-tmpl/prog/js/datatables.js >index dcfef3cf7a..bdfe317dda 100644 >--- a/koha-tmpl/intranet-tmpl/prog/js/datatables.js >+++ b/koha-tmpl/intranet-tmpl/prog/js/datatables.js >@@ -1057,25 +1057,31 @@ function _dt_add_filters(table_node, table_dt, filters_options = {}) { > } else if (typeof filters_options[i] === "function") { > filters_options[i] = filters_options[i](table_dt); > } >- $(filters_options[i]).each(function () { >- let o = $( >- '<option value="^%s$">%s</option>'.format( >- this._id, >- this._str >- ) >- ); >- // Compare with lc, or selfreg won't match ^SELFREG$ for instance, see bug 32517 >- // This is only for category, we might want to apply it only in this case. >- existing_search = existing_search.toLowerCase(); >- if ( >- existing_search === this._id || >- (existing_search && >- this._id.toLowerCase().match(existing_search)) >- ) { >- o.prop("selected", "selected"); >- } >- o.appendTo(select); >- }); >+ $(filters_options[i]) >+ .filter(function () { >+ return this._id && this._str; >+ }) >+ .each(function () { >+ let optionValue = >+ table_dt.settings()[0].ajax !== null >+ ? `^${this._id}$` >+ : this._id; >+ let o = $( >+ `<option value="${optionValue}">${this._str}</option>` >+ ); >+ >+ // Compare with lc, or selfreg won't match ^SELFREG$ for instance, see bug 32517 >+ // This is only for category, we might want to apply it only in this case. >+ existing_search = existing_search.toLowerCase(); >+ if ( >+ existing_search === this._id || >+ (existing_search && >+ this._id.toLowerCase().match(existing_search)) >+ ) { >+ o.prop("selected", "selected"); >+ } >+ o.appendTo(select); >+ }); > $(th).html(select); > } else { > var title = $(th).text(); >@@ -1122,8 +1128,12 @@ function _dt_add_delay_filters(table_dt, table_node) { > let col_input_search = DataTable.util.debounce(function (i, val) { > table_dt.column(i).search(val).draw(); > }, delay_ms); >- let col_select_search = DataTable.util.debounce(function (i, val) { >- table_dt.column(i).search(val, true, false).draw(); >+ let col_select_search = DataTable.util.debounce(function ( >+ i, >+ val, >+ regex_search = true >+ ) { >+ table_dt.column(i).search(val, regex_search, false).draw(); > }, delay_ms); > > $(table_node) >@@ -1142,8 +1152,7 @@ function _dt_add_delay_filters(table_dt, table_node) { > .find("select") > .unbind() > .bind("keyup change", function () { >- let value = this.value.length ? "^" + this.value + "$" : ""; >- col_select_search(i, this.value); >+ col_select_search(i, this.value, false); > }); > }); > } >diff --git a/koha-tmpl/intranet-tmpl/prog/js/fetch/api-client.js b/koha-tmpl/intranet-tmpl/prog/js/fetch/api-client.js >index 5b516fa445..2fe5a6dce8 100644 >--- a/koha-tmpl/intranet-tmpl/prog/js/fetch/api-client.js >+++ b/koha-tmpl/intranet-tmpl/prog/js/fetch/api-client.js >@@ -13,6 +13,7 @@ import RecallAPIClient from "./recall-api-client.js"; > import SysprefAPIClient from "./system-preferences-api-client.js"; > import TicketAPIClient from "./ticket-api-client.js"; > import AcquisitionAPIClient from "./acquisition-api-client.js"; >+import DefaultAPIClient from "./default-api-client.js"; > > export const APIClient = { > article_request: new ArticleRequestAPIClient(HttpClient), >@@ -28,4 +29,5 @@ export const APIClient = { > recall: new RecallAPIClient(HttpClient), > sysprefs: new SysprefAPIClient(HttpClient), > ticket: new TicketAPIClient(HttpClient), >+ default: new DefaultAPIClient(HttpClient), > }; >diff --git a/koha-tmpl/intranet-tmpl/prog/js/fetch/default-api-client.js b/koha-tmpl/intranet-tmpl/prog/js/fetch/default-api-client.js >new file mode 100644 >index 0000000000..714d3a3357 >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/js/fetch/default-api-client.js >@@ -0,0 +1,19 @@ >+export class DefaultAPIClient { >+ constructor(HttpClient) { >+ this.httpClient = new HttpClient({ >+ baseURL: "", >+ }); >+ } >+ >+ get koha() { >+ return { >+ get: params => this.httpClient.get(params), >+ getAll: params => this.httpClient.getAll(params), >+ post: params => this.httpClient.post(params), >+ put: params => this.httpClient.put(params), >+ delete: params => this.httpClient.delete(params), >+ }; >+ } >+} >+ >+export default DefaultAPIClient; >diff --git a/koha-tmpl/intranet-tmpl/prog/js/fetch/http-client.js b/koha-tmpl/intranet-tmpl/prog/js/fetch/http-client.js >index 50dc09f6ca..ee2722faa6 100644 >--- a/koha-tmpl/intranet-tmpl/prog/js/fetch/http-client.js >+++ b/koha-tmpl/intranet-tmpl/prog/js/fetch/http-client.js >@@ -1,16 +1,31 @@ >+function _ifDocumentAvailable(callback) { >+ if (typeof document !== "undefined" && document.getElementById) { >+ callback(); >+ } >+} >+ > class Dialog { > constructor(options = {}) {} > >+ _appendMessage(type, message) { >+ _ifDocumentAvailable(() => { >+ const messagesContainer = document.getElementById("messages"); >+ if (!messagesContainer) { >+ return; >+ } >+ >+ const htmlString = >+ `<div class="alert alert-${type}">%s</div>`.format(message); >+ messagesContainer.insertAdjacentHTML("beforeend", htmlString); >+ }); >+ } >+ > setMessage(message) { >- $("#messages").append( >- '<div class="alert alert-info">%s</div>'.format(message) >- ); >+ this._appendMessage("info", message); > } > > setError(error) { >- $("#messages").append( >- '<div class="alert alert-warning">%s</div>'.format(error) >- ); >+ this._appendMessage("warning", error); > } > } > >@@ -22,7 +37,18 @@ class HttpClient { > "Content-Type": "application/json;charset=utf-8", > "X-Requested-With": "XMLHttpRequest", > }; >- this.csrf_token = $('meta[name="csrf-token"]').attr("content"); >+ this.csrf_token = this._getCsrfToken(options); >+ } >+ >+ _getCsrfToken(options) { >+ let token = null; >+ _ifDocumentAvailable(() => { >+ const metaTag = document.querySelector('meta[name="csrf-token"]'); >+ if (metaTag) { >+ token = metaTag.getAttribute("content"); >+ } >+ }); >+ return token !== null ? token : options.csrfToken || null; > } > > async _fetchJSON( >@@ -42,6 +68,11 @@ class HttpClient { > const is_json = response.headers > .get("content-type") > ?.includes("application/json"); >+ >+ if (return_response || !is_json) { >+ return response; >+ } >+ > if (!response.ok) { > return response.text().then(text => { > let message; >@@ -57,9 +88,6 @@ class HttpClient { > throw new Error(message); > }); > } >- if (return_response || !is_json) { >- return response; >- } > return response.json(); > }) > .then(result => { >@@ -79,6 +107,40 @@ class HttpClient { > return res; > } > >+ get(params = {}) { >+ return this._fetchJSON( >+ params.endpoint, >+ params.headers, >+ { >+ ...params.options, >+ method: "GET", >+ }, >+ params.return_response ?? false, >+ params.mark_submitting ?? false >+ ); >+ } >+ >+ getAll(params = {}) { >+ let url = >+ params.endpoint + >+ "?" + >+ new URLSearchParams({ >+ _per_page: -1, >+ ...(params.params && params.params), >+ ...(params.query && { q: JSON.stringify(params.query) }), >+ }); >+ return this._fetchJSON( >+ url, >+ params.headers, >+ { >+ ...params.options, >+ method: "GET", >+ }, >+ params.return_response ?? false, >+ params.mark_submitting ?? false >+ ); >+ } >+ > post(params = {}) { > const body = params.body > ? typeof params.body === "string" >@@ -95,8 +157,8 @@ class HttpClient { > body, > method: "POST", > }, >- false, >- true >+ params.return_response ?? false, >+ params.mark_submitting ?? true > ); > } > >@@ -116,8 +178,8 @@ class HttpClient { > body, > method: "PUT", > }, >- false, >- true >+ params.return_response ?? false, >+ params.mark_submitting ?? true > ); > } > >@@ -132,8 +194,8 @@ class HttpClient { > ...params.options, > method: "DELETE", > }, >- true, >- true >+ params.return_response ?? true, >+ params.mark_submitting ?? true > ); > } > } >diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/AgreementsList.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/AgreementsList.vue >index 8e0989f35d..cfbeacdaa2 100644 >--- a/koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/AgreementsList.vue >+++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/AgreementsList.vue >@@ -121,7 +121,7 @@ export default { > table_settings: this.agreement_table_settings, > add_filters: true, > filters_options: { >- 1: () => >+ 2: () => > this.vendors.map(e => { > e["_id"] = e["id"]; > e["_str"] = e["name"]; >diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/LicensesList.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/LicensesList.vue >index 7e5409856d..4a30e83bdf 100644 >--- a/koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/LicensesList.vue >+++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/ERM/LicensesList.vue >@@ -75,14 +75,14 @@ export default { > table_settings: this.license_table_settings, > add_filters: true, > filters_options: { >- 1: () => >+ 2: () => > this.vendors.map(e => { > e["_id"] = e["id"]; > e["_str"] = e["name"]; > return e; > }), >- 3: () => this.map_av_dt_filter("av_license_types"), >- 4: () => this.map_av_dt_filter("av_license_statuses"), >+ 4: () => this.map_av_dt_filter("av_license_types"), >+ 5: () => this.map_av_dt_filter("av_license_statuses"), > }, > actions: { > 0: ["show"], >diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Vendors/VendorList.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Vendors/VendorList.vue >index aec7abad96..b689f10db0 100644 >--- a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Vendors/VendorList.vue >+++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Vendors/VendorList.vue >@@ -142,7 +142,7 @@ export default { > }, > doReceive({ id }, dt, event) { > event.preventDefault(); >- window.open(`/cgi-bin/koha/acqui/parcels.pl?booksellerid=${id}`); >+ window.location.href = `/cgi-bin/koha/acqui/parcels.pl?booksellerid=${id}`; > }, > doEdit({ id }, dt, event) { > this.$router.push({ >diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/fetch/http-client.js b/koha-tmpl/intranet-tmpl/prog/js/vue/fetch/http-client.js >index ac848c2809..23a2b6b286 100644 >--- a/koha-tmpl/intranet-tmpl/prog/js/vue/fetch/http-client.js >+++ b/koha-tmpl/intranet-tmpl/prog/js/vue/fetch/http-client.js >@@ -59,10 +59,16 @@ class HttpClient { > } > > get(params = {}) { >- return this._fetchJSON(params.endpoint, params.headers, { >- ...params.options, >- method: "GET", >- }); >+ return this._fetchJSON( >+ params.endpoint, >+ params.headers, >+ { >+ ...params.options, >+ method: "GET", >+ }, >+ params.return_response ?? false, >+ params.mark_submitting ?? false >+ ); > } > > getAll(params = {}) { >@@ -74,10 +80,16 @@ class HttpClient { > ...(params.params && params.params), > ...(params.query && { q: JSON.stringify(params.query) }), > }); >- return this._fetchJSON(url, params.headers, { >- ...params.options, >- method: "GET", >- }); >+ return this._fetchJSON( >+ url, >+ params.headers, >+ { >+ ...params.options, >+ method: "GET", >+ }, >+ params.return_response ?? false, >+ params.mark_submitting ?? false >+ ); > } > > post(params = {}) { >@@ -96,8 +108,8 @@ class HttpClient { > body, > method: "POST", > }, >- false, >- true >+ params.return_response ?? false, >+ params.mark_submitting ?? true > ); > } > >@@ -117,8 +129,8 @@ class HttpClient { > body, > method: "PUT", > }, >- false, >- true >+ params.return_response ?? false, >+ params.mark_submitting ?? true > ); > } > >@@ -133,14 +145,20 @@ class HttpClient { > ...params.options, > method: "DELETE", > }, >- true, >- true >+ params.return_response ?? true, >+ params.mark_submitting ?? true > ); > } > > count(params = {}) { > let res; >- return this._fetchJSON(params.endpoint, params.headers, {}, 1).then( >+ return this._fetchJSON( >+ params.endpoint, >+ params.headers, >+ {}, >+ params.return_response ?? true, >+ params.mark_submitting ?? false >+ ).then( > response => { > if (response) { > return response.headers.get("X-Total-Count"); >diff --git a/koha-tmpl/opac-tmpl/bootstrap/css/src/_responsive.scss b/koha-tmpl/opac-tmpl/bootstrap/css/src/_responsive.scss >index ec567fee4b..3da5baf104 100644 >--- a/koha-tmpl/opac-tmpl/bootstrap/css/src/_responsive.scss >+++ b/koha-tmpl/opac-tmpl/bootstrap/css/src/_responsive.scss >@@ -555,4 +555,9 @@ > .koha_url_text { > display: inline; > } >+ >+ #patron-virtual-card { >+ max-width: 500px; >+ min-width: 300px; >+ } > } >diff --git a/koha-tmpl/opac-tmpl/bootstrap/css/src/opac.scss b/koha-tmpl/opac-tmpl/bootstrap/css/src/opac.scss >index e00745fc32..05063f8160 100644 >--- a/koha-tmpl/opac-tmpl/bootstrap/css/src/opac.scss >+++ b/koha-tmpl/opac-tmpl/bootstrap/css/src/opac.scss >@@ -2940,38 +2940,43 @@ $star-selected: #EDB867; > } > > #patron-virtual-card { >+ border: 1px solid #BBB; >+ border-radius: 15px; >+ box-shadow: 5px 5px 6px rgba( 4, 0, 0, .2 ); >+ margin: 1rem 0; >+ padding: 1rem; > width: 100%; >- min-width: 300px; >- max-width: 500px; >+} >+ >+#image-container { >+ padding: 0 .5rem; >+} >+ >+#patron-image { > border-radius: 15px; >- box-shadow: 0 1px 6px rgba(0, 0, 0, 0.3); > display: flex; > flex-direction: column; >- align-items: flex-start; >+ flex-shrink: 1; >+ width: auto; >+} > >- #image-container { >- width: 100%; >- #patron-image { >- height: 75%; >- width: auto; >- border-radius: 15px; >- padding: 0.5rem; >- } >- } >+#card-details { >+ display: flex; >+ flex-direction: column; >+ flex-grow: 1; >+ padding: 0 .5rem; >+} > >- #barcode-container { >+#barcode-container { >+ margin-bottom: 1rem; >+ >+ svg { >+ display: block; > width: 100%; >- padding: 0.25rem 0.5rem 0.25rem 0.5rem; >- &.qrcode { >- width: 50%; // Set a smaller width for QR codes >- } > } > >- #lib-container { >- width: 100%; >- #patron-lib { >- padding-left: 0.5rem; >- } >+ &.qrcode { >+ width: 50%; // Set a smaller width for QR codes > } > } > >diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/includes/calendar.inc b/koha-tmpl/opac-tmpl/bootstrap/en/includes/calendar.inc >index 54e0e1fde9..240fa9953c 100644 >--- a/koha-tmpl/opac-tmpl/bootstrap/en/includes/calendar.inc >+++ b/koha-tmpl/opac-tmpl/bootstrap/en/includes/calendar.inc >@@ -96,7 +96,7 @@ > instance.clear(); > }) > .addClass("fa fa-fw fa-times") >- .attr("aria-hidden", true) >+ .attr("aria-hidden", false) > .attr("aria-label", _("Clear date")) > ) > .keydown(function (e) { >diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/includes/masthead.inc b/koha-tmpl/opac-tmpl/bootstrap/en/includes/masthead.inc >index 294963397c..0a658a05e3 100644 >--- a/koha-tmpl/opac-tmpl/bootstrap/en/includes/masthead.inc >+++ b/koha-tmpl/opac-tmpl/bootstrap/en/includes/masthead.inc >@@ -312,6 +312,7 @@ > > [% IF ( Koha.Preference( 'OpacAddMastheadLibraryPulldown' ) == 1 && AllPublicBranches.size > 1 ) %] > <div class="col-sm col-md-3 col-lg-2 order-3 order-sm-4"> >+ <label for="select_library" class="visually-hidden">Search the catalog in:</label> > <select name="limit" id="select_library" class="form-select"> > <option value="">All libraries</option> > >diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/includes/opac-bottom.inc b/koha-tmpl/opac-tmpl/bootstrap/en/includes/opac-bottom.inc >index 065c89c471..825846e29a 100644 >--- a/koha-tmpl/opac-tmpl/bootstrap/en/includes/opac-bottom.inc >+++ b/koha-tmpl/opac-tmpl/bootstrap/en/includes/opac-bottom.inc >@@ -22,7 +22,7 @@ > </div> <!-- / #wrapper in masthead.inc --> > <!-- prettier-ignore-end --> > >- [% IF ( Koha.Preference('OPACReportProblem') && Koha.Preference('KohaAdminEmailAddress') ) || Koha.Preference( 'CookieConsent' ) || OpacKohaUrl || ( ( OpacLangSelectorMode == 'both' || OpacLangSelectorMode == 'footer') ) %] >+ [% IF ( Koha.Preference('OPACReportProblem') && Koha.Preference('KohaAdminEmailAddress') ) || Koha.Preference( 'CookieConsent' ) || OpacKohaUrl || ( ( opaclanguagesdisplay ) && ( ! one_language_enabled ) && ( languages_loop ) && ( OpacLangSelectorMode == 'both' || OpacLangSelectorMode == 'footer') ) %] > <footer id="changelanguage" class="navbar navbar-expand navbar-light bg-light noprint"> > <div class="container-fluid"> > <div class="collapse navbar-collapse"> >@@ -49,7 +49,7 @@ > </div> > [% IF OpacKohaUrl %] > <div class="navbar-nav"> >- <a id="koha_url" class="nav-link koha_url" href="http://koha-community.org"> >+ <a id="koha_url" class="nav-link koha_url" href="https://koha-community.org"> > <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> > <path > fill="currentColor" >diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/includes/patron-restrictions.inc b/koha-tmpl/opac-tmpl/bootstrap/en/includes/patron-restrictions.inc >new file mode 100644 >index 0000000000..7b2453b7b3 >--- /dev/null >+++ b/koha-tmpl/opac-tmpl/bootstrap/en/includes/patron-restrictions.inc >@@ -0,0 +1,27 @@ >+<li class="userdebarred blocker"> >+ <strong>Your account has been frozen</strong> >+ <ul> >+ [% FOREACH restriction IN logged_in_user.restrictions %] >+ <li class="[% restriction.type.code | lower | html %]_restriction"> >+ <span class="restriction_expiration"> >+ [% IF restriction.expiration %] >+ <strong>Restriction expiring [% restriction.expiration | $KohaDates %]</strong> >+ [% ELSE %] >+ <strong>Indefinite restriction</strong> >+ [% END %] >+ </span> >+ [% IF restriction.comment.search('OVERDUES_PROCESS') %] >+ <span class="restriction_detail">Restriction added by overdues process [% restriction.comment.remove('OVERDUES_PROCESS ') | $raw | html_line_break %]</span> >+ [% ELSE %] >+ [% IF restriction.comment %] >+ <span class="restriction_detail">[%- restriction.comment | html_line_break -%]</span> >+ [% END %] >+ [% END %] >+ </li> >+ [% END %] >+ <li> >+ <span>Usually the reason for freezing an account is old overdues or damage fees. If it appears that your account is clear, please contact the library.</span> >+ <a href="/cgi-bin/koha/opac-account.pl">Check your charges page.</a> <a href="/cgi-bin/koha/opac-user.pl?opac-user-overdues=1">Check your overdues.</a> >+ </li> >+ </ul> >+</li> >diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/includes/subtypes_unimarc.inc b/koha-tmpl/opac-tmpl/bootstrap/en/includes/subtypes_unimarc.inc >index 93c52185e9..e004474b10 100644 >--- a/koha-tmpl/opac-tmpl/bootstrap/en/includes/subtypes_unimarc.inc >+++ b/koha-tmpl/opac-tmpl/bootstrap/en/includes/subtypes_unimarc.inc >@@ -1,8 +1,8 @@ > <fieldset> > <legend>Coded fields</legend> > <p> >- <label>Audience</label> >- <select name="limit" class="subtype"> >+ <label for="audience">Audience</label> >+ <select id="audience" name="limit" class="subtype"> > <option value="" selected="selected">Any</option> > <option value="aud:a">juvenile, general</option> > <option value="aud:b">pre-primary (0-5)</option> >@@ -15,8 +15,8 @@ > </select> > </p> > <p> >- <label>Physical presentation</label> >- <select name="limit" class="subtype"> >+ <label for="physical_presentation">Physical presentation</label> >+ <select id="physical_presentation" name="limit" class="subtype"> > <option value="" selected="selected">Any</option> > <option value="Material-type:r">regular print</option> > <option value="Material-type:d">large print</option> >@@ -32,8 +32,8 @@ > </select> > </p> > <p> >- <label>Literary genre</label> >- <select name="limit" class="subtype"> >+ <label for="literary_genre">Literary genre</label> >+ <select id="literary_genre" name="limit" class="subtype"> > <option value="" selected="selected">Any</option> > <option value="Literature-Code:a">fiction</option> > <option value="Literature-Code:b">drama</option> >@@ -49,8 +49,8 @@ > </select> > </p> > <p> >- <label>Biography</label> >- <select name="limit" class="subtype"> >+ <label for="biography">Biography </label> >+ <select id="biography" name="limit" class="subtype"> > <option value="">Any</option> > <option value="Biography-code:y">not a biography</option> > <option value="Biography-code:a">autobiography</option> >@@ -60,8 +60,8 @@ > </select> > </p> > <p> >- <label>Illustration</label> >- <select name="limit" class="subtype"> >+ <label for="illustration">Illustration</label> >+ <select id="illustration" name="limit" class="subtype"> > <option value="">Any</option> > <option value="Illustration-Code:a">illustrations</option> > <option value="Illustration-Code:b">maps</option> >@@ -82,8 +82,8 @@ > </select> > </p> > <p> >- <label>Content</label> >- <select name="limit" class="subtype"> >+ <label for="content">Content</label> >+ <select id="content" name="limit" class="subtype"> > <option value="">Any</option> > <option value="ctype:a">bibliography</option> > <option value="ctype:b">catalogue</option> >@@ -111,8 +111,8 @@ > </select> > </p> > <p> >- <label>Video types</label> >- <select name="limit" class="subtype"> >+ <label for="video_types">Video types </label> >+ <select id="video_types" name="limit" class="subtype"> > <option value="">Any</option> > <option value="Video-mt:a">motion picture</option> > <option value="Video-mt:b">visual projection</option> >@@ -124,8 +124,8 @@ > <fieldset> > <legend>Serials</legend> > <p> >- <label>Serial type</label> >- <select name="limit" class="subtype"> >+ <label for="serial_type">Serial type </label> >+ <select id="serial_type" name="limit" class="subtype"> > <option value="">Any type</option> > <option value="Type-Of-Serial:a">Periodical</option> > <option value="Type-Of-Serial:b">Monographic series</option> >@@ -137,8 +137,8 @@ > </select> > </p> > <p> >- <label>Periodicity</label> >- <select name="limit" class="subtype"> >+ <label for="periodicity">Periodicity</label> >+ <select id="periodicity" name="limit" class="subtype"> > <option value="">Any</option> > <option value="Frequency-code:a">Daily</option> > <option value="Frequency-code:b">Semiweekly</option> >@@ -161,8 +161,8 @@ > </select> > </p> > <p> >- <label>Regularity</label> >- <select name="limit" class="subtype"> >+ <label for="regularity">Regularity </label> >+ <select id="regularity" name="limit" class="subtype"> > <option value="">Any regularity</option> > <option value="Regularity-code:a">regular</option> > <option value="Regularity-code:b">normalised irregular</option> >@@ -174,7 +174,8 @@ > > <fieldset> > <legend>Picture</legend> >- <select name="limit" class="subtype"> >+ <label for="graphics-type" class="sr-only">Graphics type</label> >+ <select id="graphics-type" name="limit" class="subtype"> > <option value="">Any</option> > <option value="Graphics-type:a">collage</option> > <option value="Graphics-type:b">drawing</option> >@@ -187,7 +188,8 @@ > <option value="Graphics-type:k">technical drawing</option> > <option value="Graphics-type:z">other non-projected graphic type</option> > </select> >- <select name="limit" class="subtype"> >+ <label for="graphics-support" class="sr-only">Graphics support</label> >+ <select id="graphics-support" name="limit" class="subtype"> > <option value="">Any</option> > <option value="Graphics-support:a">canvas</option> > <option value="Graphics-support:b">bristol board</option> >diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/includes/usermenu.inc b/koha-tmpl/opac-tmpl/bootstrap/en/includes/usermenu.inc >index cf37dcc158..894b317dcc 100644 >--- a/koha-tmpl/opac-tmpl/bootstrap/en/includes/usermenu.inc >+++ b/koha-tmpl/opac-tmpl/bootstrap/en/includes/usermenu.inc >@@ -4,6 +4,11 @@ > <div id="menu"> > <ul> > <li [% IF userview %]class="active"[% END %]> <a href="/cgi-bin/koha/opac-user.pl">Summary</a></li> >+ >+ [% IF Koha.Preference( 'OPACVirtualCard' ) %] >+ <li [% IF virtualcardview %]class="active"[% END %]> <a href="/cgi-bin/koha/opac-virtual-card.pl">Library card</a></li> >+ [% END %] >+ > [% IF ( OPACFinesTab ) %] > <li [% IF accountview %]class="active"[% END %]> <a href="/cgi-bin/koha/opac-account.pl">Charges</a></li> > [% END %] >@@ -76,10 +81,6 @@ > [% IF logged_in_user.alert_subscriptions.count %] > <li [% IF alertsview %]class="active"[% END %]> <a href="/cgi-bin/koha/opac-alert-subscriptions.pl">Alert subscriptions ([% logged_in_user.alert_subscriptions.count | html %])</a></li> > [% END %] >- >- [% IF Koha.Preference( 'OPACVirtualCard' ) %] >- <li [% IF virtualcardview %]class="active"[% END %]> <a href="/cgi-bin/koha/opac-virtual-card.pl">My virtual card</a></li> >- [% END %] > </ul> > </div> > [% END %] >diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/modules/clubs/enroll.tt b/koha-tmpl/opac-tmpl/bootstrap/en/modules/clubs/enroll.tt >index 1f6b8d2525..01937f9c78 100644 >--- a/koha-tmpl/opac-tmpl/bootstrap/en/modules/clubs/enroll.tt >+++ b/koha-tmpl/opac-tmpl/bootstrap/en/modules/clubs/enroll.tt >@@ -2,7 +2,7 @@ > > <h2> Enroll in <em>[% club.name | html %]</em> </h2> > >-<div class="container"> >+<div class="container-fluid"> > <form method="get" id="patron-enrollment-form"> > <legend class="sr-only">Enrollment</legend> > <input type="hidden" name="id" value="[% club.id | html %]" /> >diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-addbybiblionumber.tt b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-addbybiblionumber.tt >index f0423f228d..cd0ff5f477 100644 >--- a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-addbybiblionumber.tt >+++ b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-addbybiblionumber.tt >@@ -98,7 +98,7 @@ > <legend>Add to a new list:</legend> > <label for="newvirtualshelf">List name:</label> > <input type="text" name="newvirtualshelf" id="newvirtualshelf" size="40" /> >- <label for="category">Category:</label> >+ <label for="public">Category:</label> > <select name="public" id="public"> > <option value="0">Private</option> > [% IF (OpacAllowPublicListCreation) %] >diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-browse.tt b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-browse.tt >index 6bb19f4354..bddfa9639d 100644 >--- a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-browse.tt >+++ b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-browse.tt >@@ -103,7 +103,7 @@ > <a class="expand-result" href="#" data-bs-toggle="collapse" aria-expanded="false" aria-controls="collapse"> </a> > </div> > <!-- /#heading.card-header --> >- <div id="collapse" class="collapse" aria-labelledby="heading" data-parent="#browse-searchresults"> >+ <div id="collapse" class="collapse" aria-labelledby="heading" data-bs-parent="#browse-searchresults"> > <div class="card-body"> </div> > </div> > <!-- /#collapse.collapse --> >diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-browser.tt b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-browser.tt >index f6e6e546ac..a7ecba7be2 100644 >--- a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-browser.tt >+++ b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-browser.tt >@@ -85,7 +85,7 @@ > <!-- prettier-ignore-end --> > [% ELSE %] > <div class="warning" >- >The browser table is empty. this feature is not fully set-up. See the <a href="http://wiki.koha-community.org/wiki/Opac_browse_feature">Koha Wiki</a> for more information on what it does and how to configure >+ >The browser table is empty. this feature is not fully set-up. See the <a href="https://wiki.koha-community.org/wiki/Opac_browse_feature">Koha Wiki</a> for more information on what it does and how to configure > it.</div > > > [% END # / IF have_hierarchy %] >diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-illrequests.tt b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-illrequests.tt >index d72022a045..109e0cb480 100644 >--- a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-illrequests.tt >+++ b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-illrequests.tt >@@ -207,7 +207,7 @@ > <li> > <label for="notesopac">Notes:</label> > [% IF !request.completed %] >- <textarea name="notesopac" rows="5" cols="50">[% request.notesopac | html %]</textarea> >+ <textarea id="notesopac" name="notesopac" rows="5" cols="50">[% request.notesopac | html %]</textarea> > [% ELSE %] > [% request.notesopac | html %] > [% END %] >diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-patron-consent.tt b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-patron-consent.tt >index 41e844034a..7fbad0355a 100644 >--- a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-patron-consent.tt >+++ b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-patron-consent.tt >@@ -57,14 +57,14 @@ > [% END %] > <fieldset> > [% IF consent.given_on %] >- <input type="radio" name="check_[% consent_type | html %]" value="1" checked="checked" /> Yes<br /> >- <input type="radio" name="check_[% consent_type | html %]" value="0" /> No >+ <label><input type="radio" name="check_[% consent_type | html %]" value="1" checked="checked" /> Yes</label><br /> >+ <label><input type="radio" name="check_[% consent_type | html %]" value="0" /> No</label> > [% ELSIF consent.refused_on %] >- <input type="radio" name="check_[% consent_type | html %]" value="1" /> Yes<br /> >- <input type="radio" name="check_[% consent_type | html %]" value="0" checked="checked" /> No >+ <label> <input type="radio" name="check_[% consent_type | html %]" value="1" /> Yes</label><br /> >+ <label><input type="radio" name="check_[% consent_type | html %]" value="0" checked="checked" /> No </label> > [% ELSE %] >- <input type="radio" name="check_[% consent_type | html %]" value="1" /> Yes<br /> >- <input type="radio" name="check_[% consent_type | html %]" value="0" /> No >+ <label><input type="radio" name="check_[% consent_type | html %]" value="1" /> Yes</label><br /> >+ <label><input type="radio" name="check_[% consent_type | html %]" value="0" /> No</label> > [% END %] > [% IF consent.given_on %] > <p class="consent_info"><strong>Your consent was registered on [% consent.given_on | html %].</strong></p> >diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-reserve.tt b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-reserve.tt >index d07b582690..6a7fb94e4f 100644 >--- a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-reserve.tt >+++ b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-reserve.tt >@@ -26,12 +26,12 @@ > [% END %] > [% END #/ WRAPPER breadcrumbs %] > >- <div class="container"> >+ <div class="container-fluid"> > <div id="holds" class="maincontent"> > <h1>Placing a hold</h1> > [% IF ( message ) %] >- <div id="holdmessages" class="alert"> >- <p>Sorry, you cannot place holds.</p> >+ <div id="holdmessages" class="alert alert-warning"> >+ <h2>Sorry, you cannot place holds.</h2> > <ul> > [% IF ( GNA ) %] > <li id="gna"> >@@ -49,41 +49,7 @@ > [% END %] > > [% IF ( debarred ) %] >- <li id="debarred"> >- Your account has been frozen. >- [% IF debarred_comment %] >- Comment:<br /> >- <span id="userdebarred_comment"> >- <strong> >- [% IF debarred_comment.search('OVERDUES_PROCESS') %] >- Restriction added by overdues process [% debarred_comment.remove('OVERDUES_PROCESS ') | html_line_break %] >- [% ELSE %] >- [% FOR restriction IN logged_in_user.restrictions %] >- <div class="patron_restriction"> >- [%- restriction.type.display_text | html -%][%- IF restriction.comment -%] >- : <span class="restriction_comment">[% restriction.comment | html_line_break %]</span>, frozen until: >- <span class="restriction_expiration"> >- [%- IF restriction.expiration -%] >- [%- restriction.expiration | $KohaDates -%] >- [%- ELSE -%] >- <strong>Indefinite</strong> >- [%- END -%] >- </span> >- [% END %] >- </div> >- [% END %] >- [% END %] >- </strong> >- </span> >- [% END %] >- >- [% IF debarred_date && debarred_date != '9999-12-31' %] >- End date: >- <span id="userdebarred_date">[% debarred_date | $KohaDates %]</span> >- [% END %] >- <br /><em>Usually the reason for freezing an account is old overdues or damage fees. If shows your account to be clear, please contact the library.</em> >- <a href="/cgi-bin/koha/opac-account.pl">Go to your charges page</a> >- </li> >+ [% INCLUDE "patron-restrictions.inc" %] > [% END %] > > [% IF ( too_much_oweing ) %] >@@ -273,9 +239,9 @@ > [% END %] > </select> > [% IF at_least_one_library_not_available_for_pickup %] >- <div class="at_least_one_library_not_available_note" >- >Note: Library policy does not allow hold/pickup of an item available locally. Please come to the library to retrieve these items</div >- > >+ <div class="at_least_one_library_not_available_note"> >+ Note: Library policy does not allow hold/pickup of an item available locally. Please come to the library to retrieve these items >+ </div> > [% END %] > [% END # / UNLESS bibitemloo.holdable %] > </li> >diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-results.tt b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-results.tt >index 0ab92b663d..2713c1bf2b 100644 >--- a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-results.tt >+++ b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-results.tt >@@ -432,18 +432,14 @@ > [% END # / IF total %] > > [%# Display "Not finding what you're looking for" for suggestion or ILL %] >- [% IF suggestion || ( Koha.Preference( 'ILLModule' ) == 1 && ( loggedinusername || Koha.Preference( 'ILLOpacUnauthenticatedRequest') ) ) %] >+ [% IF Koha.Preference( 'suggestion' ) || Koha.Preference( 'ILLModule' ) %] > <div class="suggestion"> > Not finding what you're looking for? > <ul> >- [% IF suggestion %] >- [% IF Koha.Preference( 'AnonSuggestions' ) == 1 %] >- <li>Make a <a href="/cgi-bin/koha/opac-suggestions.pl?op=add_form">purchase suggestion</a></li> >- [% ELSIF loggedinusername %] >- <li>Make a <a href="/cgi-bin/koha/opac-suggestions.pl?op=add_form">purchase suggestion</a></li> >- [% END %] >+ [% IF Koha.Preference( 'suggestion' ) %] >+ <li>Make a <a href="/cgi-bin/koha/opac-suggestions.pl?op=add_form">purchase suggestion</a></li> > [% END %] >- [% IF Koha.Preference( 'ILLModule' ) == 1 && ( loggedinusername || Koha.Preference( 'ILLOpacUnauthenticatedRequest' ) ) %] >+ [% IF Koha.Preference( 'ILLModule' ) %] > [% IF Koha.Preference( 'AutoILLBackendPriority' ) %] > <li>Make an <a href="/cgi-bin/koha/opac-illrequests.pl?op=create&backend=Standard">interlibrary loan request</a></li> > [% ELSE %] >diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-suggestions.tt b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-suggestions.tt >index 1110e7e77e..bd8775cf11 100644 >--- a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-suggestions.tt >+++ b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-suggestions.tt >@@ -449,7 +449,7 @@ > [% END %] > <td> > <p> >- <label for="id[% suggestions_loo.suggestionid | html %]"> >+ <label for="id[% suggestion.suggestionid | html %]"> > [% IF suggestion.biblionumber %] > <strong><a href="/cgi-bin/koha/opac-detail.pl?biblionumber=[% suggestion.biblionumber | uri %]">[% suggestion.title | html %]</a></strong> > [% ELSE %] >diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-tags.tt b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-tags.tt >index 961e63c745..9acf783097 100644 >--- a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-tags.tt >+++ b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-tags.tt >@@ -109,7 +109,7 @@ > > <form method="get" action="opac-tags.pl" class="row"> > <div class="col-auto gx-2 ms-2 my-1"> >- <label> >+ <label for="limit-tag"> > [% IF Koha.Preference( 'opacuserlogin' ) == 1 %] > <span>Tags to show from other users:</span> > [% ELSE %] >@@ -118,7 +118,7 @@ > </label> > </div> > <div class="col-auto gx-2 my-1"> >- <input type="text" name="limit" class="form-control form-control-sm" maxlength="4" size="4" value="[% limit or '100' | html %]" /> >+ <input id="limit-tag" type="text" name="limit" class="form-control form-control-sm" maxlength="4" size="4" value="[% limit or '100' | html %]" /> > </div> > <div class="col-auto gx-2 my-1"> > <input type="submit" value="OK" class="btn btn-sm btn-primary" /> >@@ -185,7 +185,7 @@ > data-title="[% MY_TAG.title | html %]" > data-tagname="[% MY_TAG.term | html %]" > data-tagid="[% MY_TAG.tag_id | html %]" >- aria-label="[% check_title | html %]" >+ aria-label="[% MY_TAG.term | html %]" > /> > </td> > <td class="tagterm"> >diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-tags_subject.tt b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-tags_subject.tt >index 5aa3710bc2..b543fcb7e5 100644 >--- a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-tags_subject.tt >+++ b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-tags_subject.tt >@@ -38,7 +38,7 @@ > <fieldset> > <legend>Show</legend> > <form class="form-inline" action="/cgi-bin/koha/opac-tags_subject.pl" method="get"> >- <p>up to <input type="text" name="number" value="[% number | html %]" size="4" maxlength="4" /> subjects <input type="submit" class="btn btn-primary" value="OK" /></p> >+ <label>up to <input type="text" name="number" value="[% number | html %]" size="4" maxlength="4" /> </label> <label>subjects <input type="submit" class="btn btn-primary" value="OK" /></label> > </form> > </fieldset> > >diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-user.tt b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-user.tt >index 2bc2eb3631..1c63a78d03 100644 >--- a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-user.tt >+++ b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-user.tt >@@ -167,36 +167,7 @@ > [% IF ( discharge_available ) %] > <li id="discharged"> <strong>Please note:</strong> Your account is frozen because it has been discharged. <a href="/cgi-bin/koha/opac-discharge.pl?op=get">Get your discharge</a> </li> > [% ELSE %] >- <li id="userdebarred"> >- <strong>Please note:</strong> Your account has been frozen. >- [% IF ( borrower.debarredcomment ) %] >- Comment:<br /> >- <ul id="userdebarred_comment"> >- [% FOREACH restriction IN logged_in_user.restrictions %] >- <li class="patron_restriction"> >- <strong>[%- restriction.type.display_text | html -%]: </strong> >- [% IF restriction.comment.search('OVERDUES_PROCESS') %] >- Restriction added by overdues process [% restriction.comment.remove('OVERDUES_PROCESS ') | html_line_break %] >- [% ELSE %] >- [%- IF restriction.comment -%] >- <span class="restriction_comment">[% restriction.comment | html_line_break %]</span>. >- [%- END -%] >- [% END %] >- [%- IF restriction.expiration -%] >- Account frozen until <span class="restriction_expiration">[%- restriction.expiration | $KohaDates -%]</span> >- [%- ELSE -%] >- <strong>Account frozen indefinitely</strong> >- [%- END -%] >- </li> >- [% END %] >- </ul> >- [% END %] >- >- <p >- ><em>Usually the reason for freezing an account is old overdues or damage fees. If your account shows to be clear, please contact the library.</em> >- <a href="/cgi-bin/koha/opac-account.pl">Go to your charges page</a></p >- > >- </li> >+ [% INCLUDE "patron-restrictions.inc" %] > [% END %] > [% END %] > [% IF ( borrower.gonenoaddress ) %] >@@ -1093,8 +1064,9 @@ > $(document).ready(function(){ > [% IF ( opac_user_holds ) %] > $("#opac-user-views a[href='#opac-user-holds_panel']").tab("show"); >- [% END %] >- [% IF ( opac_user_article_requests ) %] >+ [% ELSIF ( opac_user_overdues ) %] >+ $("#opac-user-views a[href='#opac-user-overdues_panel']").tab("show"); >+ [% ELSIF ( opac_user_article_requests ) %] > $("#opac-user-views a[href='#opac-user-article-requests_panel']").tab("show"); > [% END %] > $('#article-requests-table caption .count').html(AR_CAPTION_COUNT.format('[% current_article_requests.size | html %]')); >diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-virtual-card.tt b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-virtual-card.tt >index 9d2e4e23ad..b0a9980c3b 100644 >--- a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-virtual-card.tt >+++ b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-virtual-card.tt >@@ -3,10 +3,11 @@ > [% USE Asset %] > [% USE Koha %] > [% USE Branches %] >+[% USE KohaDates %] > [% SET OpacNav = AdditionalContents.get( location => "OpacNav", lang => lang, library => logged_in_user.branchcode || default_branch, blocktitle => 0 ) %] > [% SET OpacNavBottom = AdditionalContents.get( location => "OpacNavBottom", lang => lang, library => logged_in_user.branchcode || default_branch, blocktitle => 0 ) %] > [% INCLUDE 'doc-head-open.inc' %] >-<title>My virtual card › [% IF ( LibraryNameTitle ) %][% LibraryNameTitle | html %][% ELSE %]Koha online[% END %] catalog</title> >+<title>Your library card › [% IF ( LibraryNameTitle ) %][% LibraryNameTitle | html %][% ELSE %]Koha online[% END %] catalog</title> > [% INCLUDE 'doc-head-close.inc' %] > [% BLOCK cssinclude %] > [% END %] >@@ -20,7 +21,7 @@ > <a href="/cgi-bin/koha/opac-user.pl">[% INCLUDE 'patron-title.inc' patron = logged_in_user %]</a> > [% END %] > [% WRAPPER breadcrumb_item bc_active= 1 %] >- <span>My virtual card</span> >+ <span>Your library card</span> > [% END %] > [% END #/ WRAPPER breadcrumbs %] > >@@ -29,19 +30,27 @@ > <div class="col-lg-2"> > <div id="navigation"> [% INCLUDE 'navigation.inc' IsPatronPage=1 %] </div> > </div> >- <div class="col-10 order-first order-lg-2"> >- <h1>My virtual card </h1> >+ <div class="col-lg-10 order-first order-lg-2"> >+ <h1>Your library card </h1> > <div id="patron-virtual-card"> > [% IF ( display_patron_image ) %] > <div id="image-container"> > <img id="patron-image" src="/cgi-bin/koha/opac-patron-image.pl" alt="" /> > </div> > [% END %] >- <div id="barcode-container"> >- <svg id="patron-barcode" data-barcode="[% patron.cardnumber | html %]" data-barcode-format="[% barcode_format | html %]"></svg> >- </div> >- <div id="lib-container"> >- <p id="patron-lib"><strong>Library:</strong> [% Branches.GetName( patron.branchcode ) | html %]</p> >+ <div id="card-details"> >+ <div id="barcode-container"> >+ <svg id="patron-barcode" data-barcode="[% patron.cardnumber | html %]" data-barcode-format="[% barcode_format | html %]"></svg> >+ </div> >+ <div id="lib-container"> >+ <p id="patron-lib"><strong>Library:</strong> [% Branches.GetName( patron.branchcode ) | html %]</p> >+ </div> >+ <div id="cardnumber-container"> >+ <p id="patron-cardnumber"><strong>Card number:</strong> [% patron.cardnumber | html %]</p> >+ </div> >+ <div id="dateexpiry-container"> >+ <p id="patron-dateexpiry"><strong>Expiration date:</strong> [% patron.dateexpiry | $KohaDates %]</p> >+ </div> > </div> > </div> > </div> >diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/modules/sci/sci-main.tt b/koha-tmpl/opac-tmpl/bootstrap/en/modules/sci/sci-main.tt >index 1f9664fc3d..098313f040 100644 >--- a/koha-tmpl/opac-tmpl/bootstrap/en/modules/sci/sci-main.tt >+++ b/koha-tmpl/opac-tmpl/bootstrap/en/modules/sci/sci-main.tt >@@ -189,7 +189,7 @@ > </main> > <!-- / .main --> > >-[% # Help modal %] >+[%# Help modal %] > <div id="helpModal" class="modal" tabindex="-1" role="dialog" aria-labelledby="helpModalLabel" aria-hidden="true"> > <div class="modal-dialog"> > <div class="modal-content"> >@@ -222,7 +222,6 @@ > > [% INCLUDE 'opac-bottom.inc' %] > [% BLOCK jsinclude %] >- [% Asset.js('js/timeout.js') | $raw %] > <script> > function mungeHistory() { > // prevent back button from allowing form resubmission >@@ -273,7 +272,6 @@ > $("#sci_barcodes_table").show(); > $('#sci_checkin_button').show(); > $('#sci_refresh_button').show(); >- login_timeout(); > > // Add barcode to the array > barcodes.push(barcode); >@@ -293,24 +291,31 @@ > dofocus(); > }); > >- document.addEventListener("DOMContentLoaded",function(){ >- if ( document.querySelector('#sci_finish_button,#sci_append_button') ){ >- login_timeout(); >- } >- }); >- function login_timeout(){ >- //NOTE: There can only be 1 sci_login_timer at a time >- if ( ! window.sci_login_timer ){ >- const idleTimeout = "[% Koha.Preference('SelfCheckInTimeOut') || 120 | html %]"; >- const home_href = "/cgi-bin/koha/sci/sci-main.pl"; >- const sci_timer = new sc_timer({ >- "idle_timeout": idleTimeout, >- "redirect_url": home_href >+ >+ var idleTime = 0; >+ $(document).ready(function () { >+ //Increment the idle time counter every second >+ var idleInterval = setInterval(timerIncrement, 1000); >+ >+ //Zero the idle timer on mouse movement. >+ $(this).mousemove(function (e) { >+ idleTime = 0; >+ }); >+ $(this).keypress(function (e) { >+ idleTime = 0; > }); >- window.sci_login_timer = sci_timer; >- sci_timer.start_timer(); >+ }); >+ >+ function timerIncrement() { >+ if ( $("#sci_finish_button").is(":visible") || $("#sci_refresh_button").is(":visible") ) { >+ idleTime = idleTime + 1; >+ idleTimeout = [% refresh_timeout | html %]; >+ if (idleTime >= idleTimeout ) { >+ location.href = '/cgi-bin/koha/sci/sci-main.pl'; >+ } >+ } > } >- } >+ > > function checkBarcodeInput() { > var inputField = document.getElementById("barcode_input"); >diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/modules/sco/sco-main.tt b/koha-tmpl/opac-tmpl/bootstrap/en/modules/sco/sco-main.tt >index 52227a6b42..2e8d7b46c4 100644 >--- a/koha-tmpl/opac-tmpl/bootstrap/en/modules/sco/sco-main.tt >+++ b/koha-tmpl/opac-tmpl/bootstrap/en/modules/sco/sco-main.tt >@@ -55,7 +55,6 @@ > [% END %] > [% Asset.js('js/Gettext.js') | $raw %] > [% Asset.js('js/i18n.js') | $raw %] >-[% Asset.js('js/timeout.js') | $raw %] > </head> > <body id="sco_main" class="sco"> > <div id="wrapper"> >@@ -339,7 +338,7 @@ > <input type="password" id="patronpw" class="form-control" size="20" name="patronpw" autocomplete="off" /> > </div> > <div class="col-md-12"> >- <button id="sco_patron_login" type="submit" class="btn btn-primary">Log in</button> >+ <button type="submit" class="btn btn-primary">Log in</button> > </div> > </div> > <!-- /.row --> >@@ -352,7 +351,7 @@ > <input type="text" id="patronid" class="form-control focus" size="20" name="patronid" autocomplete="off" /> > </div> > <div class="col-md-12"> >- <button id="sco_patron_login" type="submit" class="btn btn-primary">Submit</button> >+ <button type="submit" class="btn btn-primary">Submit</button> > </div> > </div> > <!-- /.row --> >@@ -412,23 +411,11 @@ > history.replaceState(null, document.title, window.location.href); > } > } >- document.addEventListener("DOMContentLoaded",function(){ >- if ( document.querySelector('#sco_patron_login,#logout_form') ){ >- login_timeout(); >- } >- }); >- function login_timeout(){ >- //NOTE: There can only be 1 sco_login_timer at a time >- if ( ! window.sco_login_timer ){ >- const idleTimeout = "[% Koha.Preference('SelfCheckTimeout') || 120 | html %]"; >- const home_href = "/cgi-bin/koha/sco/sco-main.pl?op=logout"; >- const sco_timer = new sc_timer({ >- "idle_timeout": idleTimeout, >- "redirect_url": home_href >- }); >- window.sco_login_timer = sco_timer; >- sco_timer.start_timer(); >- } >+ var mainTimeout; >+ function sco_init() { >+ mainTimeout = setTimeout(function() { >+ location.href = '/cgi-bin/koha/sco/sco-main.pl?op=logout'; >+ }, [% SelfCheckTimeout | html %]); > } > function dofocus() { // named function req'd for body onload event by some FF and IE7 security models > // alert("dofocus called"); >@@ -481,6 +468,7 @@ > > $(document).ready(function() { > dofocus(); >+ [% IF ( patronid ) %]sco_init();[% END %] > > var dTables = $("#loanTable, #holdst, #finestable"); > dTables.each(function(){ >@@ -512,6 +500,7 @@ > > $("#logout_form").on("click", function(e){ > e.preventDefault(e); >+ clearTimeout(mainTimeout); > [% IF Koha.Preference('SelfCheckReceiptPrompt') %] > confirmModal("", _("Would you like to print a receipt?"), _("Print receipt and end session"), _("End session"), function(result) { > if ( result ){ >diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/xslt/UNIMARCslimUtils.xsl b/koha-tmpl/opac-tmpl/bootstrap/en/xslt/UNIMARCslimUtils.xsl >index 25874e25f8..50ce39f99b 100644 >--- a/koha-tmpl/opac-tmpl/bootstrap/en/xslt/UNIMARCslimUtils.xsl >+++ b/koha-tmpl/opac-tmpl/bootstrap/en/xslt/UNIMARCslimUtils.xsl >@@ -319,7 +319,7 @@ > <xsl:template name="tag_214_s"> > <xsl:if test="marc:datafield[@tag=214]/marc:subfield[@code='s']"> > <span class="results_summary tag_214_s"> >- <span class="label">Printing and/or Publishing Information Transcribed as Found in the Colophon: </span> >+ <span class="label">Printing and/or publishing information transcribed as found in the colophon: </span> > <xsl:for-each select="marc:datafield[@tag=214]/marc:subfield[@code='s']"> > <xsl:value-of select="."/> > <xsl:choose> >@@ -338,7 +338,7 @@ > <xsl:template name="tag_214_r"> > <xsl:if test="marc:datafield[@tag=214]/marc:subfield[@code='r']"> > <span class="results_summary tag_214_r"> >- <span class="label">Printing and/or Publishing Information Transcribed as Found in the Main Source of Information: </span> >+ <span class="label">Printing and/or publishing information transcribed as found in the main source of information: </span> > <xsl:for-each select="marc:datafield[@tag=214]/marc:subfield[@code='r']"> > <xsl:value-of select="."/> > <xsl:choose> >diff --git a/koha-tmpl/opac-tmpl/bootstrap/js/browse.js b/koha-tmpl/opac-tmpl/bootstrap/js/browse.js >index b1ce79c4bf..cc37b89245 100644 >--- a/koha-tmpl/opac-tmpl/bootstrap/js/browse.js >+++ b/koha-tmpl/opac-tmpl/bootstrap/js/browse.js >@@ -68,7 +68,7 @@ $(document).ready(function () { > card.find(".card-header") > .attr("id", "heading" + index) > .find("a") >- .attr("data-target", "#collapse" + index) >+ .attr("data-bs-target", "#collapse" + index) > .attr("aria-controls", "collapse" + index) > .text(object.text); > card.find(".collapse") >@@ -96,7 +96,7 @@ $(document).ready(function () { > } > > var link = $(this); >- var target = link.data("target"); >+ var target = link.data("bs-target"); > var term = link.text(); > > var field = $("#browse-searchresults").data("field"); >diff --git a/koha-tmpl/opac-tmpl/bootstrap/js/timeout.js b/koha-tmpl/opac-tmpl/bootstrap/js/timeout.js >deleted file mode 100644 >index 6c508d14c0..0000000000 >--- a/koha-tmpl/opac-tmpl/bootstrap/js/timeout.js >+++ /dev/null >@@ -1,39 +0,0 @@ >-class sc_timer { >- constructor(args) { >- const idle_timeout = args["idle_timeout"]; >- const redirect_url = args["redirect_url"]; >- if (idle_timeout) { >- this.idle_timeout = idle_timeout; >- } >- if (redirect_url) { >- this.redirect_url = redirect_url; >- } >- this.idle_time = 0; >- } >- >- start_timer() { >- const self = this; >- //Increment the idle time counter every 1 second >- const idle_interval = setInterval(function () { >- self._timer_increment(); >- }, 1000); >- >- document.addEventListener("mousemove", function () { >- self.reset_timer(); >- }); >- document.addEventListener("keypress", function () { >- self.reset_timer(); >- }); >- } >- >- reset_timer() { >- this.idle_time = 0; >- } >- >- _timer_increment() { >- this.idle_time++; >- if (this.idle_time >= this.idle_timeout) { >- location.href = this.redirect_url; >- } >- } >-} >diff --git a/members/holdshistory.pl b/members/holdshistory.pl >index 9980542f0e..1a72ec2368 100755 >--- a/members/holdshistory.pl >+++ b/members/holdshistory.pl >@@ -44,13 +44,6 @@ unless ($patron) { > exit; > } > >-if ( $borrowernumber eq C4::Context->preference('AnonymousPatron') ) { >- >- # use of 'eq' in the above comparison is intentional -- the >- # system preference value could be blank >- $template->param( is_anonymous => 1 ); >-} >- > $template->param( > holdshistoryview => 1, > patron => $patron, >diff --git a/members/memberentry.pl b/members/memberentry.pl >index 5846b3b1c6..6b39df2996 100755 >--- a/members/memberentry.pl >+++ b/members/memberentry.pl >@@ -560,6 +560,9 @@ if ( ( !$nok ) and $nodouble and ( $op eq 'cud-insert' or $op eq 'cud-save' ) ) > > delete $newdata{password2}; > >+ delete $newdata{guarantor_id}; >+ delete $newdata{guarantor_relationship}; >+ > try { > $patron->set( \%newdata )->store( { guarantors => \@guarantors } ) if scalar( keys %newdata ) > 1; > >@@ -838,10 +841,14 @@ if ( C4::Context->preference('ExtendedPatronAttributes') ) { > } > > if ( C4::Context->preference('EnhancedMessagingPreferences') ) { >- if ( $op eq 'add_form' ) { >- C4::Form::MessagingPreferences::set_form_values( { categorycode => $categorycode }, $template ); >+ unless ( $nok && $input->param('setting_messaging_prefs') ) { >+ if ( $op eq 'add_form' ) { >+ C4::Form::MessagingPreferences::set_form_values( { categorycode => $categorycode }, $template ); >+ } else { >+ C4::Form::MessagingPreferences::set_form_values( { borrowernumber => $borrowernumber }, $template ); >+ } > } else { >- C4::Form::MessagingPreferences::set_form_values( { borrowernumber => $borrowernumber }, $template ); >+ C4::Form::MessagingPreferences::restore_form_values( $input, $template ); > } > $template->param( SMSSendDriver => C4::Context->preference("SMSSendDriver") ); > $template->param( SMSnumber => $data{'smsalertnumber'} ); >diff --git a/members/moremember.pl b/members/moremember.pl >index 2c7a32b88b..49b87e97d6 100755 >--- a/members/moremember.pl >+++ b/members/moremember.pl >@@ -78,10 +78,6 @@ output_and_exit_if_error( > > my $category_type = $patron->category->category_type; > >-if ( $patron->borrowernumber eq C4::Context->preference("AnonymousPatron") ) { >- $template->param( is_anonymous => 1 ); >-} >- > for (qw(gonenoaddress lost borrowernotes is_debarred)) { > $patron->$_ and $template->param( flagged => 1 ) and last; > } >diff --git a/members/readingrec.pl b/members/readingrec.pl >index 907f3155fa..b18ef5e054 100755 >--- a/members/readingrec.pl >+++ b/members/readingrec.pl >@@ -86,10 +86,7 @@ if ( $op eq 'export_barcodes' ) { > } > > # Do not request the old issues of anonymous patron >-if ( $patron->borrowernumber eq C4::Context->preference('AnonymousPatron') ) { >- >- # use of 'eq' in the above comparison is intentional -- the >- # system preference value could be blank >+if ( $patron->is_anonymous ) { > $template->param( is_anonymous => 1 ); > } else { > $template->param( >diff --git a/misc/cronjobs/overdue_notices.pl b/misc/cronjobs/overdue_notices.pl >index 298e1ed414..f0ca534e5d 100755 >--- a/misc/cronjobs/overdue_notices.pl >+++ b/misc/cronjobs/overdue_notices.pl >@@ -1034,9 +1034,6 @@ sub _get_html_start { > <style type='text/css'> > pre {page-break-after: always;} > pre {white-space: pre-wrap;} >-pre {white-space: -moz-pre-wrap;} >-pre {white-space: -o-pre-wrap;} >-pre {word-wrap: break-work;} > </style> > </head> > <body>"; >diff --git a/misc/devel/Koha/Schema/Result/ReportsBranch.pm b/misc/devel/Koha/Schema/Result/ReportsBranch.pm >new file mode 100644 >index 0000000000..970c25eabc >--- /dev/null >+++ b/misc/devel/Koha/Schema/Result/ReportsBranch.pm >@@ -0,0 +1,85 @@ >+use utf8; >+ >+package Koha::Schema::Result::ReportsBranch; >+ >+# Created by DBIx::Class::Schema::Loader >+# DO NOT MODIFY THE FIRST PART OF THIS FILE >+ >+=head1 NAME >+ >+Koha::Schema::Result::ReportsBranch >+ >+=cut >+ >+use strict; >+use warnings; >+ >+use base 'DBIx::Class::Core'; >+ >+=head1 TABLE: C<reports_branches> >+ >+=cut >+ >+__PACKAGE__->table("reports_branches"); >+ >+=head1 ACCESSORS >+ >+=head2 report_id >+ >+ data_type: 'integer' >+ is_foreign_key: 1 >+ is_nullable: 0 >+ >+=head2 branchcode >+ >+ data_type: 'varchar' >+ is_foreign_key: 1 >+ is_nullable: 0 >+ size: 10 >+ >+=cut >+ >+__PACKAGE__->add_columns( >+ "report_id", >+ { data_type => "integer", is_foreign_key => 1, is_nullable => 0 }, >+ "branchcode", >+ { data_type => "varchar", is_foreign_key => 1, is_nullable => 0, size => 10 }, >+); >+ >+=head1 RELATIONS >+ >+=head2 branchcode >+ >+Type: belongs_to >+ >+Related object: L<Koha::Schema::Result::Branch> >+ >+=cut >+ >+__PACKAGE__->belongs_to( >+ "branchcode", >+ "Koha::Schema::Result::Branch", >+ { branchcode => "branchcode" }, >+ { is_deferrable => 1, on_delete => "CASCADE", on_update => "RESTRICT" }, >+); >+ >+=head2 report >+ >+Type: belongs_to >+ >+Related object: L<Koha::Schema::Result::SavedSql> >+ >+=cut >+ >+__PACKAGE__->belongs_to( >+ "report", >+ "Koha::Schema::Result::SavedSql", >+ { id => "report_id" }, >+ { is_deferrable => 1, on_delete => "CASCADE", on_update => "RESTRICT" }, >+); >+ >+# Created by DBIx::Class::Schema::Loader v0.07051 @ 2025-07-06 17:02:11 >+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:SJqUxMgLJHAjbX3GJlpB+A >+ >+# You can replace this text with custom code or comments, and it will be preserved on regeneration >+1; >diff --git a/misc/devel/tidy.pl b/misc/devel/tidy.pl >index 2ab5c20c33..f8829044ee 100755 >--- a/misc/devel/tidy.pl >+++ b/misc/devel/tidy.pl >@@ -8,6 +8,8 @@ use File::Slurp qw( read_file write_file ); > use IPC::Cmd qw( run ); > use Parallel::ForkManager; > >+use Koha::Devel::Files; >+ > my ( $perl_files, $js_files, $tt_files, $nproc, $no_write, $silent, $help ); > > our $perltidyrc = '.perltidyrc'; >@@ -34,13 +36,7 @@ pod2usage("--no-write can only be passed with a single file") if $no_write && @f > pod2usage("--perl, --js and --tt can only be passed without any other files in parameter") > if @files && ( $perl_files || $js_files || $tt_files ); > >-my $exceptions = { >- pl => [qw(Koha/Schema/Result Koha/Schema.pm)], >- js => [ >- qw(koha-tmpl/intranet-tmpl/lib koha-tmpl/intranet-tmpl/js/Gettext.js koha-tmpl/opac-tmpl/lib Koha/ILL/Backend/) >- ], >- tt => [qw(Koha/ILL/Backend/ *doc-head-open.inc misc/cronjobs/rss)], >-}; >+my $dev_files = Koha::Devel::Files->new( { context => 'tidy' } ); > > my @original_files = @files; > if (@files) { >@@ -48,8 +44,8 @@ if (@files) { > # This is inefficient if the list of files is long but most of the time we will have only one > @files = map { > my $file = $_; >- my $filetype = get_filetype($file); >- my $cmd = sprintf q{git ls-files %s | grep %s}, build_git_exclude($filetype), $file; >+ my $filetype = $dev_files->get_filetype($file); >+ my $cmd = sprintf q{git ls-files %s | grep %s}, $dev_files->build_git_exclude($filetype), $file; > my $output = qx{$cmd}; > chomp $output; > $output ? $file : (); >@@ -69,14 +65,14 @@ if (@files) { > } > } > } else { >- push @files, get_perl_files() if $perl_files; >- push @files, get_js_files() if $js_files; >- push @files, get_tt_files() if $tt_files; >+ push @files, $dev_files->ls_perl_files() if $perl_files; >+ push @files, $dev_files->ls_js_files() if $js_files; >+ push @files, $dev_files->ls_tt_files() if $tt_files; > > unless (@files) { >- push @files, get_perl_files(); >- push @files, get_js_files(); >- push @files, get_tt_files(); >+ push @files, $dev_files->ls_perl_files(); >+ push @files, $dev_files->ls_js_files(); >+ push @files, $dev_files->ls_tt_files(); > } > } > >@@ -124,7 +120,7 @@ if (@errors) { > sub tidy { > my ($file) = @_; > >- my $filetype = get_filetype($file); >+ my $filetype = $dev_files->get_filetype($file); > > if ( $filetype eq 'pl' ) { > return tidy_perl($file); >@@ -137,32 +133,6 @@ sub tidy { > } > } > >-sub build_git_exclude { >- my ($filetype) = @_; >- return join( " ", map( "':(exclude)$_'", @{ $exceptions->{$filetype} } ) ); >-} >- >-sub get_perl_files { >- my $cmd = sprintf q{git ls-files '*.pl' '*.PL' '*.pm' '*.t' svc opac/svc %s}, build_git_exclude('pl'); >- my @files = qx{$cmd}; >- chomp for @files; >- return @files; >-} >- >-sub get_js_files { >- my $cmd = sprintf q{git ls-files '*.js' '*.ts' '*.vue' %s}, build_git_exclude('js'); >- my @files = qx{$cmd}; >- chomp for @files; >- return @files; >-} >- >-sub get_tt_files { >- my $cmd = sprintf q{git ls-files '*.tt' '*.inc' %s}, build_git_exclude('tt'); >- my @files = qx{$cmd}; >- chomp for @files; >- return @files; >-} >- > sub tidy_perl { > my ($file) = @_; > my $cmd = >@@ -221,19 +191,6 @@ sub tidy_tt { > return ( $success, $error_message, $full_buf, $stdout_buf, $stderr_buf ); > } > >-sub get_filetype { >- my ($file) = @_; >- return 'pl' if $file =~ m{^svc} || $file =~ m{^opac/svc}; >- return 'pl' if $file =~ m{\.pl$} || $file =~ m{\.pm} || $file =~ m{\.t$}; >- return 'pl' if $file =~ m{\.PL$}; >- >- return 'js' if $file =~ m{\.js$} || $file =~ m{\.ts$} || $file =~ m{\.vue$}; >- >- return 'tt' if $file =~ m{\.inc$} || $file =~ m{\.tt$}; >- >- die sprintf 'Cannot guess filetype for %s', $file; >-} >- > sub l { > say shift unless $silent; > } >diff --git a/misc/maintenance/search_for_data_inconsistencies.pl b/misc/maintenance/search_for_data_inconsistencies.pl >index 72d9656118..31ee49716c 100755 >--- a/misc/maintenance/search_for_data_inconsistencies.pl >+++ b/misc/maintenance/search_for_data_inconsistencies.pl >@@ -390,7 +390,7 @@ use C4::Biblio qw( GetMarcFromKohaField ); > "Column " . $rs->result_source->name . "." . $column . " contains $invalid_dates invalid dates" ); > > if ( $invalid_dates > 0 ) { >- new_hint("You may change the dates with script: misc/cronjobs/fix_invalid_dates.pl (-c -v)"); >+ new_hint("You may change the dates with script: misc/maintenance/fix_invalid_dates.pl (-c -v)"); > } > > } >diff --git a/misc/sip_cli_emulator.pl b/misc/sip_cli_emulator.pl >index a09ed952c6..23fd231784 100755 >--- a/misc/sip_cli_emulator.pl >+++ b/misc/sip_cli_emulator.pl >@@ -53,8 +53,8 @@ my $fee_amount; > my $fee_identifier; > my $transaction_id; > my $pickup_location; >-my $hold_mode; >-my $no_block = 'N'; >+my $hold_mode = '+'; >+my $no_block = 'N'; > my $start_item; > my $end_item; > >@@ -306,6 +306,31 @@ my $handlers = { > }, > }; > >+my $param_to_cli = { >+ currency_type => 'currency-type', >+ current_location => 'location', >+ end_item => 'end-item', >+ fee_acknowledged => 'fee-acknowledged', >+ fee_amount => 'fee-amount', >+ fee_identifier => 'fee-identifier', >+ fee_type => 'fee-type', >+ hold_mode => 'hold-mode', >+ institution_id => 'location', >+ item_identifier => 'item', >+ location_code => 'location', >+ login_password => 'sip_pass', >+ login_user_id => 'sip_user', >+ no_block => 'no-block', >+ patron_identifier => 'patron', >+ patron_password => 'password', >+ payment_type => 'payment-type', >+ pickup_location => 'pickup-location', >+ start_item => 'start-item', >+ summary => 'summary', >+ terminal_password => 'sip_pass', >+ transaction_id => 'transaction-id', >+}; >+ > my $data = run_command_message('login'); > > if ( $data =~ '^941' ) { ## we are logged in >@@ -335,7 +360,8 @@ sub build_command_message { > foreach my $key ( keys %$parameters ) { > unless ( $parameters->{$key} ) { > unless ( $optional{$key} ) { >- say "$key is required for $message"; >+ my $param_name = $param_to_cli->{$key} // $key; >+ say "$param_name is required for $message"; > return; > } > } >@@ -356,7 +382,7 @@ sub run_command_message { > > my $data = <$socket>; > >- say "READ: $data"; >+ say "READ: " . ( defined $data ? $data : 'undef' ); > > return $data; > } >diff --git a/opac/opac-readingrecord.pl b/opac/opac-readingrecord.pl >index 4fe1622bc1..ffd830711c 100755 >--- a/opac/opac-readingrecord.pl >+++ b/opac/opac-readingrecord.pl >@@ -57,7 +57,7 @@ if ( $order eq 'title' ) { > } elsif ( $order eq 'author' ) { > $template->param( orderbyauthor => 1 ); > } else { >- $order = "date_due desc"; >+ $order = { -desc => "date_due" }; > $template->param( orderbydate => 1 ); > } > >@@ -66,23 +66,23 @@ $limit //= ''; > $limit = ( $limit eq 'full' ) ? 0 : 50; > > my $checkouts = [ >- $patron->checkouts( >+ $patron->checkouts->search( > {}, > { > order_by => $order, > prefetch => { item => { biblio => 'biblioitems' } }, >- ( $limit ? ( limit => $limit ) : () ), >+ ( $limit ? ( rows => $limit ) : () ), > } > )->as_list > ]; > $limit -= scalar(@$checkouts) if $limit; > my $old_checkouts = [ >- $patron->old_checkouts( >+ $patron->old_checkouts->search( > {}, > { > order_by => $order, > prefetch => { item => { biblio => 'biblioitems' } }, >- ( $limit ? ( limit => $limit ) : () ), >+ ( $limit ? ( rows => $limit ) : () ), > } > )->as_list > ]; >diff --git a/opac/opac-reserve.pl b/opac/opac-reserve.pl >index 7ffd8e08ac..6db213f298 100755 >--- a/opac/opac-reserve.pl >+++ b/opac/opac-reserve.pl >@@ -39,6 +39,7 @@ use Koha::Items; > use Koha::ItemTypes; > use Koha::Checkouts; > use Koha::Libraries; >+use Koha::Logger; > use Koha::Patrons; > use List::MoreUtils qw( uniq ); > >@@ -112,65 +113,42 @@ if ( $#biblionumbers < 0 && $op ne 'cud-place_reserve' ) { > # Here we check that the borrower can actually make reserves Stage 1. > # > # >-my $noreserves = 0; >-if ( $category->effective_BlockExpiredPatronOpacActions_contains('hold') ) { >- if ( $patron->is_expired ) { > >- # cannot reserve, their card has expired and the rules set mean this is not allowed >- $noreserves = 1; >- $template->param( message => 1, expired_patron => 1 ); >- } >-} >- >-my $maxoutstanding = C4::Context->preference("maxoutstanding"); >-my $amountoutstanding = $patron->account->balance; >-if ( $amountoutstanding && ( $amountoutstanding > $maxoutstanding ) ) { >- my $amount = sprintf "%.02f", $amountoutstanding; >- $template->param( message => 1 ); >- $noreserves = 1; >- $template->param( too_much_oweing => $amount ); >-} >- >-if ( $patron->gonenoaddress && ( $patron->gonenoaddress == 1 ) ) { >- $noreserves = 1; >- $template->param( >- message => 1, >- GNA => 1 >- ); >-} >+my $can_place_holds = $patron->can_place_holds( { no_short_circuit => 1 } ); > >-if ( $patron->lost && ( $patron->lost == 1 ) ) { >- $noreserves = 1; >- $template->param( >- message => 1, >- lost => 1 >- ); >-} >+if ( !$can_place_holds ) { > >-if ( $patron->is_debarred ) { >- $noreserves = 1; >- $template->param( >- message => 1, >- debarred => 1, >- debarred_comment => $patron->debarredcomment, >- debarred_date => $patron->debarred, >- ); >-} >- >-my $holds = $patron->holds; >-my $reserves_count = $holds->count; >-$template->param( RESERVES => $holds->unblessed ); >-if ( $maxreserves && ( $reserves_count >= $maxreserves ) ) { > $template->param( message => 1 ); >- $noreserves = 1; >- $template->param( too_many_reserves => $holds->count ); >-} > >-if ($noreserves) { >+ my $messages = $can_place_holds->messages(); >+ foreach my $msg ( @{$messages} ) { >+ if ( $msg->message eq 'expired' ) { >+ $template->param( expired_patron => 1 ); >+ } elsif ( $msg->message eq 'debt_limit' ) { >+ $template->param( too_much_oweing => sprintf( "%.02f", $msg->{payload}->{total_outstanding} ) ); >+ } elsif ( $msg->message eq 'bad_address' ) { >+ $template->param( GNA => 1 ); >+ } elsif ( $msg->message eq 'card_lost' ) { >+ $template->param( lost => 1 ); >+ } elsif ( $msg->message eq 'restricted' ) { >+ $template->param( >+ debarred => 1, >+ debarred_comment => $patron->debarredcomment, >+ debarred_date => $patron->debarred, >+ ); >+ } elsif ( $msg->message eq 'hold_limit' ) { >+ $template->param( too_many_reserves => $msg->{payload}->{total_holds} ); >+ } else { >+ Koha::Logger->get->warn( sprintf( "Unhandled 'can_place_holds' error code: %s", $msg->message ) ); >+ } >+ } >+ > output_html_with_http_headers $query, $cookie, $template->output, undef, { force_no_caching => 1 }; > exit; > } > >+my $reserves_count = $patron->holds->count; >+ > # pass the pickup branch along.... > my $branch = $query->param('branch') || $patron->branchcode || C4::Context->userenv->{branch} || ''; > $template->param( branch => $branch ); >diff --git a/opac/opac-user.pl b/opac/opac-user.pl >index 9d7109bcac..be5f63896b 100755 >--- a/opac/opac-user.pl >+++ b/opac/opac-user.pl >@@ -430,6 +430,7 @@ $template->param( > OpacHoldNotes => C4::Context->preference('OpacHoldNotes'), > failed_holds => scalar $query->param('failed_holds'), > opac_user_holds => scalar $query->param('opac-user-holds') || 0, >+ opac_user_overdues => scalar $query->param('opac-user-overdues') || 0, > opac_user_article_requests => scalar $query->param('opac-user-article-requests') || 0, > ); > >diff --git a/opac/sci/sci-main.pl b/opac/sci/sci-main.pl >index d254347b5b..34d433a53f 100755 >--- a/opac/sci/sci-main.pl >+++ b/opac/sci/sci-main.pl >@@ -102,4 +102,8 @@ if ( $op eq 'cud-check_in' ) { > $template->param( success => \@success, errors => \@errors, checkins => 1 ); > } > >+# Make sure timeout has a reasonable value >+my $timeout = C4::Context->preference('SelfCheckInTimeout') || 120; >+$template->param( refresh_timeout => $timeout ); >+ > output_html_with_http_headers $cgi, $cookie, $template->output, undef, { force_no_caching => 1 }; >diff --git a/opac/sco/sco-main.pl b/opac/sco/sco-main.pl >index e1cc93d802..9ecae7c76c 100755 >--- a/opac/sco/sco-main.pl >+++ b/opac/sco/sco-main.pl >@@ -74,6 +74,13 @@ my ( $template, $loggedinuser, $cookie ) = get_template_and_user( > } > ); > >+# Get the self checkout timeout preference, or use 120 seconds as a default >+my $selfchecktimeout = 120000; >+if ( C4::Context->preference('SelfCheckTimeout') ) { >+ $selfchecktimeout = C4::Context->preference('SelfCheckTimeout') * 1000; >+} >+$template->param( SelfCheckTimeout => $selfchecktimeout ); >+ > # Checks policy laid out by SCOAllowCheckin, defaults to 'on' if preference is undefined > my $allowselfcheckreturns = 1; > if ( defined C4::Context->preference('SCOAllowCheckin') ) { >diff --git a/package.json b/package.json >index d16166e856..66748e8699 100644 >--- a/package.json >+++ b/package.json >@@ -19,7 +19,6 @@ > "bootstrap": "^5.3.3", > "css-loader": "^6.6.0", > "cypress": "^12.17.2", >- "cypress-mysql": "^1.0.0", > "datatables.net-buttons": "^2.3.4", > "datatables.net-vue3": "^2.0.0", > "gulp": "^4.0.2", >@@ -82,6 +81,7 @@ > "globals": "^16.0.0", > "gulp-tap": "^1.0.1", > "html-webpack-plugin": "^5.5.0", >+ "mysql2": "^3.14.1", > "node-sass-tilde-importer": "^1.0.2", > "postcss": "^8.4.14", > "postcss-selector-parser": "^6.0.10", >diff --git a/reports/guided_reports.pl b/reports/guided_reports.pl >index fe47501ee7..25b703514a 100755 >--- a/reports/guided_reports.pl >+++ b/reports/guided_reports.pl >@@ -42,6 +42,7 @@ use Koha::Notice::Templates; > use Koha::TemplateUtils qw( process_tt ); > use C4::ClassSource qw( GetClassSources ); > use C4::Scrubber; >+use Data::Dumper; > > =head1 NAME > >@@ -57,6 +58,9 @@ my $input = CGI->new; > my $usecache = Koha::Caches->get_instance->memcached_cache; > > my $op = $input->param('op') // ''; >+print STDERR "DEBUG op: $op\n"; >+ >+# my @branches = grep { $_ ne q{} } $input->multi_param('branches'); > my $flagsrequired; > if ( ( $op eq 'add_form' ) > || ( $op eq 'add_form_sql' ) >@@ -127,7 +131,7 @@ if ( !$op ) { > 'showsql' => 1, > 'mana_success' => scalar $input->param('mana_success'), > 'mana_id' => $report->{mana_id}, >- 'mana_comments' => $report->{comments} >+ 'mana_comments' => $report->{comments}, > ); > > } elsif ( $op eq 'edit_form' ) { >@@ -136,6 +140,7 @@ if ( !$op ) { > my $group = $report->report_group; > my $subgroup = $report->report_subgroup; > my $tables = get_tables(); >+ > $template->param( > 'sql' => $report->savedsql, > 'reportname' => $report->report_name, >@@ -148,11 +153,13 @@ if ( !$op ) { > 'editsql' => 1, > 'mana_id' => $report->{mana_id}, > 'mana_comments' => $report->{comments}, >- 'tables' => $tables >+ 'tables' => $tables, >+ 'report' => $report, > ); > > } elsif ( $op eq 'cud-update_sql' || $op eq 'cud-update_and_run_sql' ) { > my $id = $input->param('id'); >+ my $report = Koha::Reports->find($id); > my $sql = $input->param('sql'); > my $reportname = $input->param('reportname'); > my $group = $input->param('group'); >@@ -163,9 +170,9 @@ if ( !$op ) { > my $public = $input->param('public'); > my $save_anyway = $input->param('save_anyway'); > my @errors; >- my $tables = get_tables(); >+ my $tables = get_tables(); >+ my @branches = grep { $_ ne q{} } $input->multi_param('branches'); > >- # if we have the units, then we came from creating a report from SQL and thus need to handle converting units > if ($cache_expiry_units) { > if ( $cache_expiry_units eq "minutes" ) { > $cache_expiry *= 60; >@@ -211,6 +218,8 @@ if ( !$op ) { > 'problematic_authvals' => $problematic_authvals, > 'warn_authval_problem' => 1, > 'phase_update' => 1, >+ 'branches' => @branches, >+ 'report' => $report, > ); > > } else { >@@ -226,9 +235,14 @@ if ( !$op ) { > notes => $notes, > public => $public, > cache_expiry => $cache_expiry, >+ >+ # report => $report, > } > ); > >+ $report->store; >+ $report->replace_library_limits( \@branches ); >+ > my $editsql = 1; > if ( $op eq 'cud-update_and_run_sql' ) { > $editsql = 0; >@@ -245,7 +259,10 @@ if ( !$op ) { > 'cache_expiry' => $cache_expiry, > 'public' => $public, > 'usecache' => $usecache, >- 'tables' => $tables >+ 'tables' => $tables, >+ >+ # 'branches' => @branches, >+ 'report' => $report, > ); > logaction( "REPORTS", "MODIFY", $id, "$reportname | $sql" ) if C4::Context->preference("ReportsLog"); > } >@@ -489,6 +506,7 @@ if ( !$op ) { > my $area = $input->param('area'); > my $sql = $input->param('sql'); > my $type = $input->param('type'); >+ > $template->param( > 'save' => 1, > 'area' => $area, >@@ -514,6 +532,7 @@ if ( !$op ) { > my $public = $input->param('public'); > my $save_anyway = $input->param('save_anyway'); > my $tables = get_tables(); >+ my @branches = grep { $_ ne q{} } $input->multi_param('branches'); > > # if we have the units, then we came from creating a report from SQL and thus need to handle converting units > if ($cache_expiry_units) { >@@ -591,6 +610,9 @@ if ( !$op ) { > public => $public, > } > ); >+ my $report = Koha::Reports->find($id); >+ $report->replace_library_limits( \@branches ); >+ > logaction( "REPORTS", "ADD", $id, "$name | $sql" ) if C4::Context->preference("ReportsLog"); > $template->param( > 'save_successful' => 1, >@@ -603,7 +625,8 @@ if ( !$op ) { > 'cache_expiry' => $cache_expiry, > 'public' => $public, > 'usecache' => $usecache, >- 'tables' => $tables >+ 'tables' => $tables, >+ 'report' => $report, > ); > } > } >@@ -746,20 +769,24 @@ if ( !$op ) { > > } elsif ( $op eq 'add_form_sql' || $op eq 'duplicate' ) { > >- my ( $group, $subgroup, $sql, $reportname, $notes ); >+ my ( $group, $subgroup, $sql, $reportname, $notes, @branches, $report ); > if ( $input->param('sql') ) { > $group = $input->param('report_group'); > $subgroup = $input->param('report_subgroup'); > $sql = $input->param('sql') // ''; > $reportname = $input->param('reportname') // ''; > $notes = $input->param('notes') // ''; >+ @branches = grep { $_ ne q{} } $input->multi_param('branches'); >+ > } elsif ( my $report_id = $input->param('id') ) { >- my $report = Koha::Reports->find($report_id); >+ $report = Koha::Reports->find($report_id); > $group = $report->report_group; > $subgroup = $report->report_subgroup; > $sql = $report->savedsql // ''; > $reportname = $report->report_name // ''; > $notes = $report->notes // ''; >+ @branches = grep { $_ ne q{} } $input->multi_param('branches'); >+ > } > > my $tables = get_tables(); >@@ -775,6 +802,9 @@ if ( !$op ) { > 'usecache' => $usecache, > 'tables' => $tables, > >+ # 'branches' => \@branches, >+ 'report' => $report, >+ > ); > } > >@@ -1081,24 +1111,51 @@ if ( $op eq 'list' || $op eq 'convert' ) { > my $subgroup = $input->param('subgroup'); > $filter->{group} = $group; > $filter->{subgroup} = $subgroup; >- my $reports = get_saved_reports($filter); >- my $has_obsolete_reports; >- for my $report (@$reports) { >- $report->{results} = C4::Reports::Guided::get_results( $report->{id} ); >- if ( $report->{savedsql} =~ m|biblioitems| and $report->{savedsql} =~ m|marcxml| ) { >- $report->{seems_obsolete} = 1; >- $has_obsolete_reports++; >+ >+ my $pref_enable_filtering_reports = C4::Context->preference("EnableFilteringReports"); >+ if ( $pref_enable_filtering_reports == "1" ) { >+ my $reports_with_library_limits_results = >+ Koha::Reports->search_with_library_limits( {}, {}, C4::Context::mybranch() ); >+ my $reports_list = $reports_with_library_limits_results->unblessed; >+ my $has_obsolete_reports; >+ while ( my $report = $reports_with_library_limits_results->next ) { >+ $report->{results} = C4::Reports::Guided::get_results( $report->{id} ); >+ if ( $report->{savedsql} =~ m|biblioitems| and $report->{savedsql} =~ m|marcxml| ) { >+ $report->{seems_obsolete} = 1; >+ $has_obsolete_reports++; >+ } >+ $template->param( >+ 'manamsg' => $input->param('manamsg') || '', >+ 'saved1' => 1, >+ 'savedreports' => $reports_list, >+ 'usecache' => $usecache, >+ 'groups_with_subgroups' => groups_with_subgroups( $group, $subgroup ), >+ filters => $filter, >+ has_obsolete_reports => $has_obsolete_reports, >+ ); > } >+ } else { >+ >+ my $reports = get_saved_reports($filter); >+ my $has_obsolete_reports; >+ >+ for my $report (@$reports) { >+ $report->{results} = C4::Reports::Guided::get_results( $report->{id} ); >+ if ( $report->{savedsql} =~ m|biblioitems| and $report->{savedsql} =~ m|marcxml| ) { >+ $report->{seems_obsolete} = 1; >+ $has_obsolete_reports++; >+ } >+ } >+ $template->param( >+ 'manamsg' => $input->param('manamsg') || '', >+ 'saved1' => 1, >+ 'savedreports' => $reports, >+ 'usecache' => $usecache, >+ 'groups_with_subgroups' => groups_with_subgroups( $group, $subgroup ), >+ filters => $filter, >+ has_obsolete_reports => $has_obsolete_reports, >+ ); > } >- $template->param( >- 'manamsg' => $input->param('manamsg') || '', >- 'saved1' => 1, >- 'savedreports' => $reports, >- 'usecache' => $usecache, >- 'groups_with_subgroups' => groups_with_subgroups( $group, $subgroup ), >- filters => $filter, >- has_obsolete_reports => $has_obsolete_reports, >- ); > } > > # pass $sth, get back an array of names for the column headers >diff --git a/reports/orders_by_fund.pl b/reports/orders_by_fund.pl >index dcf833758e..29cb175afe 100755 >--- a/reports/orders_by_fund.pl >+++ b/reports/orders_by_fund.pl >@@ -1,22 +1,22 @@ > #!/usr/bin/perl > >-# This file is part of Koha. >+# Copyright Frédérick Capovilla, 2011 - SYS-TECH >+# Copyright Ãlyse Morin, 2012 - Libéo > # >-# Author : Frédérick Capovilla, 2011 - SYS-TECH >-# Modified by : Ãlyse Morin, 2012 - Libéo >+# This file is part of Koha. > # >-# Koha 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 3 of the License, or (at your option) any later >-# version. >+# Koha 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 3 of the License, or >+# (at your option) any later version. > # >-# Koha 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. >+# Koha 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 >-# Koha; if not, write to the Free Software Foundation, Inc., 59 Temple Place, >-# Suite 330, Boston, MA 02111-1307 USA >+# You should have received a copy of the GNU General Public License >+# along with Koha; if not, see <http://www.gnu.org/licenses>. > > =head1 orders_by_budget > >diff --git a/rspack.config.js b/rspack.config.js >index 630aa6a513..9b10583d51 100644 >--- a/rspack.config.js >+++ b/rspack.config.js >@@ -161,4 +161,38 @@ module.exports = [ > "datatables.net-buttons/js/buttons.colVis": "DataTable", > }, > }, >+ { >+ entry: { >+ "api-client.cjs": >+ "./koha-tmpl/intranet-tmpl/prog/js/fetch/api-client.js", >+ }, >+ output: { >+ filename: "[name].js", >+ path: path.resolve(__dirname, "t/cypress/plugins/dist/"), >+ library: { >+ type: "commonjs", >+ }, >+ globalObject: "global", >+ }, >+ target: "node", >+ module: { >+ rules: [ >+ { >+ test: /\.js$/, >+ loader: "builtin:swc-loader", >+ options: { >+ jsc: { >+ parser: { >+ syntax: "ecmascript", >+ }, >+ }, >+ }, >+ exclude: [/node_modules/], >+ type: "javascript/auto", >+ }, >+ ], >+ }, >+ externals: [], >+ plugins: [], >+ }, > ]; >diff --git a/t/CookieManager.t b/t/CookieManager.t >index b4d3604bff..639b681d12 100755 >--- a/t/CookieManager.t >+++ b/t/CookieManager.t >@@ -19,9 +19,10 @@ > > use Modern::Perl; > use CGI; >-use Data::Dumper qw(Dumper); >+ >+#use Data::Dumper qw(Dumper); > use Test::NoWarnings; >-use Test::More tests => 4; >+use Test::More tests => 5; > > use t::lib::Mocks; > >@@ -29,29 +30,32 @@ use C4::Context; > use Koha::CookieManager; > > subtest 'new' => sub { >- plan tests => 3; >+ plan tests => 4; > >- t::lib::Mocks::mock_config( Koha::CookieManager::DENY_LIST_VAR, 'just_one' ); >+ t::lib::Mocks::mock_config( Koha::CookieManager::KEEP_COOKIE_CONF_VAR, 'just_one' ); > my $cmgr = Koha::CookieManager->new; >- is( scalar keys %{ $cmgr->{_remove_unless} }, 1, 'one entry' ); >- is( exists $cmgr->{_secure}, 1, 'secure key found' ); >+ is( scalar @{ $cmgr->{_keep_list} }, 1, 'one entry to keep' ); >+ is( exists $cmgr->{_secure}, 1, 'secure key found' ); > >- t::lib::Mocks::mock_config( Koha::CookieManager::DENY_LIST_VAR, [ 'two', 'entries' ] ); >+ t::lib::Mocks::mock_config( Koha::CookieManager::KEEP_COOKIE_CONF_VAR, [ 'two', 'entries' ] ); >+ t::lib::Mocks::mock_config( Koha::CookieManager::REMOVE_COOKIE_CONF_VAR, ['test'] ); > $cmgr = Koha::CookieManager->new; >- is( scalar keys %{ $cmgr->{_remove_unless} }, 2, 'two entries' ); >+ is( scalar @{ $cmgr->{_keep_list} }, 2, 'two entries to keep' ); >+ is( scalar @{ $cmgr->{_remove_list} }, 1, 'one entry to remove' ); > }; > > subtest 'clear_unless' => sub { >- plan tests => 17; >+ plan tests => 14; > >- t::lib::Mocks::mock_config( Koha::CookieManager::DENY_LIST_VAR, [ 'aap', 'noot' ] ); >+ t::lib::Mocks::mock_config( Koha::CookieManager::KEEP_COOKIE_CONF_VAR, [ 'aap', 'noot' ] ); >+ t::lib::Mocks::mock_config( Koha::CookieManager::REMOVE_COOKIE_CONF_VAR, ['mies'] ); > > my $q = CGI->new; > my $cmgr = Koha::CookieManager->new; > > my $cookie1 = $q->cookie( -name => 'aap', -value => 'aap', -expires => '+1d' ); > my $cookie2 = $q->cookie( -name => 'noot', -value => 'noot' ); >- my $cookie3 = $q->cookie( -name => 'wim', -value => q{wim}, -HttpOnly => 1 ); >+ my $cookie3 = $q->cookie( -name => 'wim', -value => q{wim}, -HttpOnly => 0 ); > my $cookie4 = $q->cookie( -name => 'aap', -value => q{aap2}, -HttpOnly => 1 ); > my $list = [ $cookie1, $cookie2, $cookie3, $cookie4, 'mies', 'zus' ]; # 4 cookies, 2 names > >@@ -59,54 +63,47 @@ subtest 'clear_unless' => sub { > is( @{ $cmgr->clear_unless }, 0, 'Empty list' ); > is( @{ $cmgr->clear_unless( { hash => 1 }, ['array'], $q ) }, 0, 'Empty list for invalid arguments' ); > >- # Pass list, expect 5 cookies (3 cleared, last aap kept) >+ # Pass list, expecting 4 cookies (2 kept, 1 untouched, 1 cleared); duplicate aap and zus discarded > my @rv = @{ $cmgr->clear_unless(@$list) }; >- is( @rv, 5, '5 expected' ); >+ is( @rv, 4, '4 expected' ); > is( $rv[0]->name, 'noot', '1st cookie' ); > is( $rv[1]->name, 'wim', '2nd cookie' ); > is( $rv[2]->name, 'aap', '3rd cookie' ); > is( $rv[3]->name, 'mies', '4th cookie' ); >- is( $rv[4]->name, 'zus', '5th cookie' ); >- is( $rv[0]->value, q{noot}, 'noot not empty' ); >- is( $rv[1]->value, q{}, 'wim empty' ); >- is( $rv[2]->value, q{aap2}, 'aap not empty' ); >- is( $rv[3]->value, q{}, 'mies empty' ); >- is( $rv[4]->value, q{}, 'zus empty' ); >- is( $rv[1]->httponly, 0, 'cleared wim is not httponly' ); >+ is( $rv[0]->value, q{noot}, 'noot kept' ); >+ is( $rv[1]->value, q{wim}, 'wim untouched' ); >+ is( $rv[2]->value, q{aap2}, 'aap kept, last entry' ); >+ is( $rv[3]->value, q{}, 'mies cleared' ); >+ is( $rv[1]->httponly, undef, 'wim still not httponly' ); > is( $rv[2]->httponly, 1, 'aap httponly' ); > >- # Test with numeric suffix (via regex) >- t::lib::Mocks::mock_config( Koha::CookieManager::DENY_LIST_VAR, ['catalogue_editor_\d+'] ); >+ # Test with prefix (note trailing underscore) >+ t::lib::Mocks::mock_config( Koha::CookieManager::KEEP_COOKIE_CONF_VAR, 'catalogue_editor_' ); >+ t::lib::Mocks::mock_config( Koha::CookieManager::REMOVE_COOKIE_CONF_VAR, 'catalogue_editor' ); > $cmgr = Koha::CookieManager->new; >- $cookie1 = $q->cookie( -name => 'catalogue_editor_abc', -value => '1', -expires => '+1y' ); >- $cookie2 = $q->cookie( -name => 'catalogue_editor_345', -value => '1', -expires => '+1y' ); >- $cookie3 = $q->cookie( -name => 'catalogue_editor_', -value => '1', -expires => '+1y' ); >- $cookie4 = $q->cookie( -name => 'catalogue_editor_123x', -value => '1', -expires => '+1y' ); >- >- $list = [ $cookie1, $cookie2, $cookie3, $cookie4 ]; >- @rv = @{ $cmgr->clear_unless(@$list) }; >- is_deeply( >- [ map { $_->value ? $_->name : () } @rv ], >- ['catalogue_editor_345'], >- 'Cookie2 should be found only' >- ); >- >- # Test with another regex (yes, highly realistic examples :) >- t::lib::Mocks::mock_config( Koha::CookieManager::DENY_LIST_VAR, ['next_\w+_number\d{2}_(now|never)'] ); >- $cmgr = Koha::CookieManager->new; >- my $cookie5; >- $cookie1 = $q->cookie( -name => 'next_mynewword_number99_never', -value => '1', -expires => '+1y' ); #fine >- $cookie2 = $q->cookie( -name => 'prefixed_next_mynewword_number99_never', -value => '1', -expires => '+1y' ) >- ; # wrong prefix >- $cookie3 = $q->cookie( -name => 'next_mynew-word_number99_never', -value => '1', -expires => '+1y' ) >- ; # wrong: hyphen in word >- $cookie4 = >- $q->cookie( -name => 'mynewword_number999_never', -value => '1', -expires => '+1y' ); # wrong: three digits >- $cookie5 = >- $q->cookie( -name => 'next_mynewword_number99_always', -value => '1', -expires => '+1y' ); # wrong: always >- @rv = @{ $cmgr->clear_unless( $cookie1, $cookie2, $cookie3, $cookie4, $cookie5 ) }; >- is_deeply( [ map { $_->value ? $_->name : () } @rv ], ['next_mynewword_number99_never'], 'Only cookie1 matched' ); >+ $cookie1 = $q->cookie( -name => 'catalogue_editor', -value => '1' ); >+ $cookie2 = $q->cookie( -name => 'catalogue_editor2', -value => '2' ); >+ $cookie3 = $q->cookie( -name => 'catalogue_editor_3', -value => '3' ); >+ >+ $list = [ $cookie1, $cookie2, $cookie3, 'catalogue_editor4' ]; >+ my $result = [ map { defined( $_->max_age ) ? () : $_->name } @{ $cmgr->clear_unless(@$list) } ]; >+ is_deeply( $result, ['catalogue_editor_3'], 'Only cookie3 is kept (not expired)' ); >+}; > >+subtest 'path exception' => sub { >+ plan tests => 4; >+ >+ t::lib::Mocks::mock_config( Koha::CookieManager::REMOVE_COOKIE_CONF_VAR, ['always_show_holds'] ); >+ my $q = CGI->new; >+ my $cmgr = Koha::CookieManager->new; >+ my $cookie1 = $q->cookie( -name => 'always_show_holds', -value => 'DO', path => '/cgi-bin/koha/reserve' ); >+ my @rv = @{ $cmgr->clear_unless($cookie1) }; >+ is( $rv[0]->name, 'always_show_holds', 'Check name' ); >+ is( $rv[0]->path, '/cgi-bin/koha/reserve', 'Check path' ); >+ is( $rv[0]->max_age, 0, 'Check max_age' ); >+ my $cookie2 = $q->cookie( -name => 'always_show_holds', -value => 'DONT' ); # default path >+ @rv = @{ $cmgr->clear_unless($cookie2) }; >+ is( $rv[0]->path, '/cgi-bin/koha/reserve', 'Check path cookie2, corrected here' ); > }; > > subtest 'replace_in_list' => sub { >diff --git a/t/Form_MessagingPreferences.t b/t/Form_MessagingPreferences.t >index 40a68dd402..fc0552ae86 100755 >--- a/t/Form_MessagingPreferences.t >+++ b/t/Form_MessagingPreferences.t >@@ -1,15 +1,73 @@ > #!/usr/bin/perl >-# >-# This Koha test module is a stub! >-# Add more tests here!!! > >-use strict; >-use warnings; >+use Modern::Perl; >+use CGI; >+use Template; > >-use Test::NoWarnings; >-use Test::More tests => 2; >+use Test::More tests => 1; >+use Test::MockModule; > >-BEGIN { >- use_ok('C4::Form::MessagingPreferences'); >-} >+#use Test::NoWarnings; >+use t::lib::TestBuilder; >+use t::lib::Mocks; > >+use C4::Form::MessagingPreferences; >+ >+my $builder = t::lib::TestBuilder->new; >+my $schema = Koha::Database->new->schema; >+ >+subtest 'restore_form_values' => sub { >+ >+ plan tests => 2; >+ >+ my $cgi = CGI->new; >+ my $template_module = Test::MockModule->new('Template'); >+ my $vars = {}; >+ $template_module->mock( 'param', sub { my ( $self, $key, $val ) = @_; $vars->{$key} = $val; } ); >+ my $template = Template->new( ENCODING => 'UTF-8' ); >+ >+ $schema->storage->txn_begin; >+ >+ my $patron = $builder->build_object( { class => 'Koha::Patrons' } ); >+ t::lib::Mocks::mock_preference( 'EnhancedMessagingPreferences', 1 ); >+ >+ C4::Form::MessagingPreferences::set_form_values( { borrowernumber => $patron->id }, $template ); >+ my $set_form_values_vars = {%$vars}; >+ $vars = {}; >+ >+ C4::Form::MessagingPreferences::restore_form_values( $cgi, $template ); >+ my $restore_form_values_vars = {%$vars}; >+ >+ is_deeply( >+ $set_form_values_vars, $restore_form_values_vars, >+ "Default messaging preferences don't change when handled with restore_form_values." >+ ); >+ >+ C4::Members::Messaging::SetMessagingPreference( >+ { >+ borrowernumber => $patron->id, >+ message_transport_types => ['email'], >+ message_attribute_id => 2, >+ days_in_advance => 10, >+ wants_digest => 1 >+ } >+ ); >+ >+ C4::Form::MessagingPreferences::set_form_values( { borrowernumber => $patron->id }, $template ); >+ $set_form_values_vars = {%$vars}; >+ $vars = {}; >+ >+ $cgi->param( -name => '2', -value => 'email' ); >+ $cgi->param( -name => '2-DAYS', -value => '10' ); >+ $cgi->param( -name => 'digest', -value => '2' ); >+ >+ C4::Form::MessagingPreferences::restore_form_values( $cgi, $template ); >+ $restore_form_values_vars = {%$vars}; >+ >+ is_deeply( >+ $set_form_values_vars, $restore_form_values_vars, >+ "Patrons messaging preferences don't change when handled with restore_form_values." >+ ); >+ >+ $schema->storage->txn_rollback; >+}; >diff --git a/t/ImportBatch.t b/t/ImportBatch.t >index 749f9b0640..0258e4e5c3 100755 >--- a/t/ImportBatch.t >+++ b/t/ImportBatch.t >@@ -17,11 +17,12 @@ > > use Modern::Perl; > >+use Test::More tests => 4; >+use Test::NoWarnings; > use File::Temp qw|tempfile|; > use MARC::Field; > use MARC::File::XML; > use MARC::Record; >-use Test::More tests => 3; > use t::lib::Mocks; > > BEGIN { >@@ -55,11 +56,28 @@ subtest 'RecordsFromMARCXMLFile' => sub { > > my ( $errors, $recs ); > my $file = create_file( { whitespace => 1, format => 'marcxml' } ); >- ( $errors, $recs ) = C4::ImportBatch::RecordsFromMARCXMLFile( $file, 'UTF-8' ); >+ { >+ # Ignore the following warning >+ # Use of uninitialized value in concatenation (.) or string at /usr/share/perl5/MARC/File/XML.pm line 399, <__ANONIO__> chunk 1. >+ # We do not want to expect it (using Test::Warn): it is a bug from MARC::File::XML >+ local $SIG{__WARN__} = sub { }; >+ my $dup_err; >+ local *STDERR; >+ open STDERR, ">>", \$dup_err; >+ ( $errors, $recs ) = C4::ImportBatch::RecordsFromMARCXMLFile( $file, 'UTF-8' ); >+ close STDERR; >+ } > is( @$recs, 0, 'No records from empty marcxml file' ); > > $file = create_file( { garbage => 1, format => 'marcxml' } ); >- ( $errors, $recs ) = C4::ImportBatch::RecordsFromMARCXMLFile( $file, 'UTF-8' ); >+ { >+ local $SIG{__WARN__} = sub { }; >+ my $dup_err; >+ local *STDERR; >+ open STDERR, ">>", \$dup_err; >+ ( $errors, $recs ) = C4::ImportBatch::RecordsFromMARCXMLFile( $file, 'UTF-8' ); >+ close STDERR; >+ } > is( @$recs, 0, 'Garbage returns no records' ); > > $file = create_file( { two => 1, format => 'marcxml' } ); >diff --git a/t/Koha/I18N.t b/t/Koha/I18N.t >index 45a34d3a0c..55e1425680 100755 >--- a/t/Koha/I18N.t >+++ b/t/Koha/I18N.t >@@ -2,7 +2,7 @@ > > use Modern::Perl; > use Test::NoWarnings; >-use Test::More tests => 36; >+use Test::More tests => 37; > use Test::MockModule; > use FindBin qw($Bin); > use Encode; >@@ -61,3 +61,49 @@ my @tests = ( > foreach my $test (@tests) { > is( $test->[0], decode_utf8( $test->[1] ), $test->[1] ); > } >+ >+subtest 'available_locales' => sub { >+ plan tests => 6; >+ >+ # Test basic functionality >+ my $locales = Koha::I18N::available_locales(); >+ >+ # Should return an arrayref >+ is( ref($locales), 'ARRAY', 'available_locales returns an arrayref' ); >+ >+ # Should have at least the default option >+ ok( scalar(@$locales) >= 1, 'At least one locale returned (default)' ); >+ >+ # First locale should be default >+ is( $locales->[0]->{value}, 'default', 'First locale is default' ); >+ is( $locales->[0]->{text}, 'Default Unicode collation', 'Default locale has correct text' ); >+ >+ # All locales should have value and text keys >+ my $all_have_keys = 1; >+ for my $locale (@$locales) { >+ unless ( exists $locale->{value} && exists $locale->{text} ) { >+ $all_have_keys = 0; >+ last; >+ } >+ } >+ ok( $all_have_keys, 'All locales have value and text keys' ); >+ >+ # Test structure for real system locales (if any) >+ my $system_locales = [ grep { $_->{value} ne 'default' } @$locales ]; >+ if (@$system_locales) { >+ >+ # Should have friendly display names for common locales >+ my $has_friendly_name = 0; >+ for my $locale (@$system_locales) { >+ if ( $locale->{text} =~ /^[A-Z][a-z]+ \([^)]+\) - / ) { >+ $has_friendly_name = 1; >+ last; >+ } >+ } >+ ok( $has_friendly_name, 'System locales have friendly display names' ) if @$system_locales; >+ } else { >+ >+ # If no system locales, just pass this test >+ ok( 1, 'No system locales found (test environment)' ); >+ } >+}; >diff --git a/t/Koha/SearchEngine/Elasticsearch/Search.t b/t/Koha/SearchEngine/Elasticsearch/Search.t >index 2cb164a277..06b1c157ed 100755 >--- a/t/Koha/SearchEngine/Elasticsearch/Search.t >+++ b/t/Koha/SearchEngine/Elasticsearch/Search.t >@@ -18,14 +18,110 @@ > use Modern::Perl; > > use Test::NoWarnings; >-use Test::More tests => 3; >+use Test::More tests => 5; > use Test::MockModule; > use t::lib::Mocks; > use Encode qw( encode ); > use MIME::Base64 qw( encode_base64 ); > >+use utf8; >+ > use_ok('Koha::SearchEngine::Elasticsearch::Search'); > >+subtest '_sort_facets' => sub { >+ plan tests => 3; >+ t::lib::Mocks::mock_preference( 'SearchEngine', 'Elasticsearch' ); >+ >+ my $facets = _get_facets(); >+ >+ my @normal_sort_facets = sort { $a->{facet_label_value} cmp $b->{facet_label_value} } @$facets; >+ my @normal_expected_facets = ( >+ { facet_label_value => 'Ari' }, >+ { facet_label_value => 'Fairy' }, >+ { facet_label_value => 'Harry' }, >+ { facet_label_value => 'Mary' }, >+ { facet_label_value => 'Zambidis' }, >+ { facet_label_value => 'ari' }, >+ { facet_label_value => 'fairy' }, >+ { facet_label_value => 'harry' }, >+ { facet_label_value => 'mary' }, >+ { facet_label_value => 'Ãberg, Erik' }, >+ { facet_label_value => 'Ãuthor' }, >+ { facet_label_value => 'étienne' }, >+ { facet_label_value => 'Å ostakovitÅ¡, Dmitri' }, >+ ); >+ >+ #NOTE: stringwise/bytewise is not UTF-8 friendly >+ is_deeply( \@normal_sort_facets, \@normal_expected_facets, "Perl's built-in sort is stringwise/bytewise." ); >+ >+ my $search = Koha::SearchEngine::Elasticsearch::Search->new( >+ { index => $Koha::SearchEngine::Elasticsearch::AUTHORITIES_INDEX } ); >+ >+ #NOTE: The 'default' locale uses the Default Unicode Collation Element Table, which >+ #is used for the locales of English (en) and French (fr). >+ my $sorted_facets = $search->_sort_facets( { facets => $facets, locale => 'default' } ); >+ my $expected = [ >+ { facet_label_value => 'Ãberg, Erik' }, >+ { facet_label_value => 'ari' }, >+ { facet_label_value => 'Ari' }, >+ { facet_label_value => 'Ãuthor' }, >+ { facet_label_value => 'étienne' }, >+ { facet_label_value => 'fairy' }, >+ { facet_label_value => 'Fairy' }, >+ { facet_label_value => 'harry' }, >+ { facet_label_value => 'Harry' }, >+ { facet_label_value => 'mary' }, >+ { facet_label_value => 'Mary' }, >+ { facet_label_value => 'Å ostakovitÅ¡, Dmitri' }, >+ { facet_label_value => 'Zambidis' }, >+ ]; >+ is_deeply( $sorted_facets, $expected, "Facets sorted correctly with default locale" ); >+ >+ # Test system preference integration >+ t::lib::Mocks::mock_preference( 'FacetSortingLocale', 'en_US.utf8' ); >+ my $sorted_facets_syspref = $search->_sort_facets( { facets => $facets } ); >+ >+ # Should return sorted facets (exact order may vary by system locale availability) >+ is( ref($sorted_facets_syspref), 'ARRAY', "System preference integration works" ); >+ >+ #NOTE: If "locale" is not provided to _sort_facets, it will look up the LC_COLLATE >+ #for the local system. This is what allows this function to work well in production. >+ #However, since LC_COLLATE could vary from system to system running these unit tests, >+ #we can't test it reliably here. >+}; >+ >+subtest '_sort_facets_zebra with fi_FI locale' => sub { >+ plan tests => 1; >+ my $locale_map = _get_locale_map(); >+SKIP: { >+ skip( "fi_FI.utf8 locale not available on this system", 1 ) unless $locale_map->{"fi_FI.utf8"}; >+ >+ my $facets = _get_facets(); >+ >+ my $search = Koha::SearchEngine::Elasticsearch::Search->new( >+ { index => $Koha::SearchEngine::Elasticsearch::AUTHORITIES_INDEX } ); >+ >+ # Test with explicit locale parameter >+ my $sorted_facets_explicit = $search->_sort_facets( { facets => $facets, locale => 'fi_FI' } ); >+ my $expected = [ >+ { facet_label_value => 'ari' }, >+ { facet_label_value => 'Ari' }, >+ { facet_label_value => 'étienne' }, >+ { facet_label_value => 'fairy' }, >+ { facet_label_value => 'Fairy' }, >+ { facet_label_value => 'harry' }, >+ { facet_label_value => 'Harry' }, >+ { facet_label_value => 'mary' }, >+ { facet_label_value => 'Mary' }, >+ { facet_label_value => 'Å ostakovitÅ¡, Dmitri' }, >+ { facet_label_value => 'Zambidis' }, >+ { facet_label_value => 'Ãberg, Erik' }, >+ { facet_label_value => 'Ãuthor' }, >+ ]; >+ is_deeply( $sorted_facets_explicit, $expected, "Zebra facets sorted correctly with explicit locale" ); >+ } >+}; >+ > subtest 'search_auth_compat' => sub { > plan tests => 7; > >@@ -102,4 +198,33 @@ subtest 'search_auth_compat' => sub { > is( @$results[0]->{series}, 1, 'Valid main heading with ShowHeadingUse' ); > }; > >+sub _get_facets { >+ my $facets = [ >+ { facet_label_value => 'Mary' }, >+ { facet_label_value => 'Harry' }, >+ { facet_label_value => 'Fairy' }, >+ { facet_label_value => 'Ari' }, >+ { facet_label_value => 'mary' }, >+ { facet_label_value => 'harry' }, >+ { facet_label_value => 'Ãberg, Erik' }, >+ { facet_label_value => 'Ãuthor' }, >+ { facet_label_value => 'fairy' }, >+ { facet_label_value => 'ari' }, >+ { facet_label_value => 'étienne' }, >+ { facet_label_value => 'Å ostakovitÅ¡, Dmitri' }, >+ { facet_label_value => 'Zambidis' }, >+ ]; >+ return $facets; >+} >+ >+sub _get_locale_map { >+ my $map = {}; >+ my @locales = `locale -a`; >+ foreach my $locale (@locales) { >+ chomp($locale); >+ $map->{$locale} = 1; >+ } >+ return $map; >+} >+ > 1; >diff --git a/t/Labels.t b/t/Labels.t >index 1a5d8b53b1..5facaaed78 100755 >--- a/t/Labels.t >+++ b/t/Labels.t >@@ -17,11 +17,11 @@ > # > # for context, see http://bugs.koha-community.org/bugzilla3/show_bug.cgi?id=2691 > >-use strict; >-use warnings; >+use Modern::Perl; > > use C4::ClassSplitRoutine::LCC qw( split_callnumber ); >-use Test::More tests => 11; >+use Test::More tests => 12; >+use Test::NoWarnings; > > BEGIN { > use_ok( 'C4::Labels::Label', qw( _get_text_fields _check_params _guide_box ) ); >diff --git a/t/RecordProcessor.t b/t/RecordProcessor.t >index c5a967788e..5c22d6fe6d 100755 >--- a/t/RecordProcessor.t >+++ b/t/RecordProcessor.t >@@ -215,7 +215,7 @@ subtest 'options() tests' => sub { > > subtest "'TrimFields' filter tests" => sub { > >- plan tests => 2; >+ plan tests => 4; > > # Test default values with a MARC::Record record > my $record = MARC::Record->new(); >@@ -225,6 +225,8 @@ subtest "'TrimFields' filter tests" => sub { > [ '150', ' ', ' ', a => 'Test' ], > [ '520', ' ', ' ', a => "This is\na test!\t" ], > [ '521', ' ', ' ', a => "This is a\t test!\t" ], >+ [ '522', ' ', ' ', a => "This is a test!", b => " " ], >+ [ '523', ' ', ' ', a => " " ], > ); > > my $p = Koha::RecordProcessor->new( { filters => ['TrimFields'] } ); >@@ -235,4 +237,10 @@ subtest "'TrimFields' filter tests" => sub { > > my $get521a = $record->subfield( '521', 'a' ); > is( $get521a, "This is a\t test!", "Trailing tabs are stripped while inner tabs are kept" ); >+ >+ my $get522b = $record->subfield( '522', 'b' ); >+ isnt( $get522b, "", "Subfield containing spaces only removed from the field" ); >+ >+ my $get523 = $record->field('523'); >+ is( $get523, undef, "Field with only a subfield containing spaces removed from the record" ); > }; >diff --git a/t/Scrubber.t b/t/Scrubber.t >index 42c1a074fc..cf2de7d0f1 100755 >--- a/t/Scrubber.t >+++ b/t/Scrubber.t >@@ -1,94 +1,158 @@ > #!/usr/bin/perl > >+# Copyright 2025 Koha Development team >+# >+# This file is part of Koha >+# >+# Koha 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 3 of the License, or >+# (at your option) any later version. >+# >+# Koha 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 Koha; if not, see <http://www.gnu.org/licenses> >+ > use Modern::Perl; > >-$| = 1; >+use Test::More tests => 7; > use Test::NoWarnings; >-use Test::More tests => 27; >+use Test::Exception; > use Test::Warn; > > BEGIN { >- use FindBin; >- use lib $FindBin::Bin; > use_ok('C4::Scrubber'); > } > >-sub pretty_line { >- my $max = 54; >- (@_) or return "#" x $max . "\n"; >- my $phrase = " " . shift() . " "; >- my $half = "#" x ( ( $max - length($phrase) ) / 2 ); >- return $half . $phrase . $half . "\n"; >-} >+subtest 'new() constructor tests' => sub { >+ plan tests => 8; > >-my ( $scrubber, $html, $result, @types, $collapse ); >-$collapse = 1; >-@types = qw(default comment note); >-$html = q| >-<![CDATA[selfdestruct]]]> >-<?php echo(" EVIL EVIL EVIL "); ?> <!-- COMMENT --> >-<hr> <!-- TMPL_VAR NAME="password" --> >-<style type="text/css">body{display:none;}</style> >-<link media="screen" type="text/css" rev="stylesheet" rel="stylesheet" href="css.css"> >-<I FAKE="attribute" > I am ITALICS with fake="attribute" </I><br /> >-<em FAKE="attribute" > I am em with fake="attribute" </em><br /> >-<B> I am BOLD </B><br /> >-<span style="background-image: url(http://hackersite.cn/porno.jpg);"> I am a span w/ style. Bad style.</span> >-<span> I am a span trying to inject a link: <a href="badlink.html"> link </a></span> >-<br> >-<A NAME="evil"> >- <A HREF="javascript:alert('OMG YOO R HACKED');">I am a link firing javascript.</A> >- <br /> >- <A HREF="image/bigone.jpg" ONMOUSEOVER="alert('OMG YOO R HACKED');"> >- <IMG SRC="image/smallone.jpg" ALT="ONMOUSEOVER JAVASCRIPT"> >- </A> >-</A> <br> >-At the end here, I actually have some regular text. >-|; >- >-ok( $scrubber = C4::Scrubber->new(), "Constructor: C4::Scrubber->new()" ); >- >-isa_ok( $scrubber, 'HTML::Scrubber', 'Constructor returns HTML::Scrubber object' ); >- >-warning_like { $scrubber->default() } '', "\$scrubber->default ran without fault."; >-warning_like { $scrubber->comment() } '', "\$scrubber->comment ran without fault."; >-warning_like { $scrubber->process() } '', "\$scrubber->process ran without fault."; >- >-ok( $result = $scrubber->scrub($html), "Getting scrubbed text (type: [default])" ); >- >-foreach (@types) { >- ok( $scrubber = C4::Scrubber->new($_), "testing Constructor: C4::Scrubber->new($_)" ); >- >- warning_like { $scrubber->default() } '', "\$scrubber->default ran without fault."; >- warning_like { $scrubber->comment() } '', "\$scrubber->comment ran without fault."; >- warning_like { $scrubber->process() } '', "\$scrubber->process ran without fault."; >- >- ok( $result = $scrubber->scrub($html), "Getting scrubbed text (type: $_)" ); >-} >+ my $scrubber; >+ lives_ok { $scrubber = C4::Scrubber->new() } 'Constructor with no parameters succeeds'; >+ isa_ok( $scrubber, 'HTML::Scrubber', 'Constructor returns HTML::Scrubber object' ); >+ >+ lives_ok { $scrubber = C4::Scrubber->new('default') } 'Constructor with default type succeeds'; >+ isa_ok( $scrubber, 'HTML::Scrubber', 'Constructor with default type returns HTML::Scrubber object' ); > >-#Test for invalid new entry >-eval { >- C4::Scrubber->new(""); >- fail("test should fail on entry of ''"); >+ lives_ok { $scrubber = C4::Scrubber->new('comment') } 'Constructor with comment type succeeds'; >+ isa_ok( $scrubber, 'HTML::Scrubber', 'Constructor with comment type returns HTML::Scrubber object' ); >+ >+ lives_ok { $scrubber = C4::Scrubber->new('note') } 'Constructor with note type succeeds'; >+ isa_ok( $scrubber, 'HTML::Scrubber', 'Constructor with note type returns HTML::Scrubber object' ); > }; >-if ($@) { >- pass("Test should have failed on entry of '' (empty string) and it did. YAY!"); >-} > >-eval { >- C4::Scrubber->new("Client"); >- fail("test should fail on entry of 'Client'"); >+subtest 'constructor error handling' => sub { >+ plan tests => 5; >+ >+ my $scrubber; >+ lives_ok { $scrubber = C4::Scrubber->new(undef) } 'Constructor with undef type succeeds (treated as default)'; >+ isa_ok( $scrubber, 'HTML::Scrubber', 'Constructor with undef type returns HTML::Scrubber object' ); >+ >+ throws_ok { >+ C4::Scrubber->new(''); >+ } >+ qr/New called with unrecognized type/, 'Constructor throws exception for empty string type'; >+ >+ throws_ok { >+ C4::Scrubber->new('invalid_type'); >+ } >+ qr/New called with unrecognized type/, 'Constructor throws exception for invalid type'; >+ >+ throws_ok { >+ C4::Scrubber->new('Client'); >+ } >+ qr/New called with unrecognized type/, 'Constructor throws exception for Client type'; >+}; >+ >+subtest 'default scrubber functionality' => sub { >+ plan tests => 5; >+ >+ my $scrubber = C4::Scrubber->new('default'); >+ >+ my $malicious_html = q| >+ <![CDATA[selfdestruct]]]> >+ <?php echo("EVIL EVIL EVIL"); ?> >+ <script>alert('XSS Attack!');</script> >+ <style type="text/css">body{display:none;}</style> >+ <link href="evil.css" rel="stylesheet"> >+ <img src="x" onerror="alert('XSS')"> >+ <a href="javascript:alert('XSS')">Click me</a> >+ <p onclick="alert('XSS')">Paragraph</p> >+ <div style="background:url(javascript:alert('XSS'))">Content</div> >+ Plain text content >+ |; >+ >+ my $result = $scrubber->scrub($malicious_html); >+ >+ unlike( $result, qr/<script/i, 'Script tags are removed' ); >+ unlike( $result, qr/<style/i, 'Style tags are removed' ); >+ unlike( $result, qr/<link/i, 'Link tags are removed' ); >+ unlike( $result, qr/javascript:/i, 'JavaScript URLs are removed' ); >+ like( $result, qr/Plain text content/, 'Plain text content is preserved' ); >+}; >+ >+subtest 'comment scrubber functionality' => sub { >+ plan tests => 10; >+ >+ my $scrubber = C4::Scrubber->new('comment'); >+ >+ my $test_html = >+ '<p>Paragraph</p><b>Bold</b><i>Italic</i><em>Emphasis</em><big>Big</big><small>Small</small><strong>Strong</strong><br><u>Underline</u><hr><span>Span</span><div>Div</div><script>Evil</script>'; >+ >+ my $result = $scrubber->scrub($test_html); >+ >+ like( $result, qr/<b>Bold<\/b>/, 'Bold tags are preserved' ); >+ like( $result, qr/<i>Italic<\/i>/, 'Italic tags are preserved' ); >+ like( $result, qr/<em>Emphasis<\/em>/, 'Em tags are preserved' ); >+ like( $result, qr/<big>Big<\/big>/, 'Big tags are preserved' ); >+ like( $result, qr/<small>Small<\/small>/, 'Small tags are preserved' ); >+ like( $result, qr/<strong>Strong<\/strong>/, 'Strong tags are preserved' ); >+ like( $result, qr/<br>/, 'Break tags are preserved' ); >+ >+ unlike( $result, qr/<p>/, 'Paragraph tags are removed' ); >+ unlike( $result, qr/<span>/, 'Span tags are removed' ); >+ unlike( $result, qr/<script>/, 'Script tags are removed' ); > }; >-if ($@) { >- pass("Test should have failed on entry of 'Client' and it did. YAY!"); >-} > >-my $scrub_text = >- '<div><span><p><b>bold</b><i>ital</i><em>emphatic</em><big>embiggen</big><small>shrink</small><strong>strongbad</strong><br><u>under</u><hr></p></span></div>'; >-my $scrub_comment = >- '<b>bold</b><i>ital</i><em>emphatic</em><big>embiggen</big><small>shrink</small><strong>strongbad</strong><br>under'; >-is( C4::Scrubber->new('comment')->scrub($scrub_text), $scrub_comment, "Comment scrubber removes expected elements" ); >-is( >- C4::Scrubber->new('note')->scrub($scrub_text), $scrub_text, >- "Note scrubber removes (additional) expected elements" >-); >+subtest 'note scrubber functionality' => sub { >+ plan tests => 22; >+ >+ my $scrubber = C4::Scrubber->new('note'); >+ >+ my $comprehensive_html = >+ '<div><span><p><b>Bold</b><i>Italic</i><em>Emphasis</em><big>Big</big><small>Small</small><strong>Strong</strong><br><u>Underline</u><hr><ol><li>Ordered item 1</li><li>Ordered item 2</li></ol><ul><li>Unordered item 1</li><li>Unordered item 2</li></ul><dl><dt>Term</dt><dd>Definition</dd></dl></p></span></div>'; >+ >+ my $result = $scrubber->scrub($comprehensive_html); >+ >+ like( $result, qr/<div>/, 'Div tags are preserved' ); >+ like( $result, qr/<span>/, 'Span tags are preserved' ); >+ like( $result, qr/<p>/, 'Paragraph tags are preserved' ); >+ like( $result, qr/<b>Bold<\/b>/, 'Bold tags are preserved' ); >+ like( $result, qr/<i>Italic<\/i>/, 'Italic tags are preserved' ); >+ like( $result, qr/<em>Emphasis<\/em>/, 'Em tags are preserved' ); >+ like( $result, qr/<big>Big<\/big>/, 'Big tags are preserved' ); >+ like( $result, qr/<small>Small<\/small>/, 'Small tags are preserved' ); >+ like( $result, qr/<strong>Strong<\/strong>/, 'Strong tags are preserved' ); >+ like( $result, qr/<br>/, 'Break tags are preserved' ); >+ like( $result, qr/<u>Underline<\/u>/, 'Underline tags are preserved' ); >+ like( $result, qr/<hr>/, 'HR tags are preserved' ); >+ like( $result, qr/<ol>/, 'Ordered list tags are preserved' ); >+ like( $result, qr/<ul>/, 'Unordered list tags are preserved' ); >+ like( $result, qr/<li>/, 'List item tags are preserved' ); >+ like( $result, qr/<dl>/, 'Description list tags are preserved' ); >+ like( $result, qr/<dt>Term<\/dt>/, 'Description term tags are preserved' ); >+ like( $result, qr/<dd>Definition<\/dd>/, 'Description definition tags are preserved' ); >+ >+ is( $result, $comprehensive_html, 'All allowed tags in note scrubber are preserved exactly' ); >+ >+ my $malicious_note = '<p>Safe content</p><script>alert("XSS")</script><iframe src="evil.html"></iframe>'; >+ my $safe_result = $scrubber->scrub($malicious_note); >+ >+ like( $safe_result, qr/<p>Safe content<\/p>/, 'Safe content is preserved' ); >+ unlike( $safe_result, qr/<script>/, 'Script tags are removed from notes' ); >+ unlike( $safe_result, qr/<iframe>/, 'Iframe tags are removed from notes' ); >+}; >diff --git a/t/Test/Mock/Logger.t b/t/Test/Mock/Logger.t >index 1563205f8c..015cdcae34 100755 >--- a/t/Test/Mock/Logger.t >+++ b/t/Test/Mock/Logger.t >@@ -1,5 +1,6 @@ > use Modern::Perl; >-use Test::More tests => 10; >+use Test::More tests => 11; >+use Test::NoWarnings; > use Test::Warn; > > # Module under test >@@ -132,16 +133,25 @@ subtest 'Method chaining tests' => sub { > isa_ok( $result, 't::lib::Mocks::Logger', 'Method chaining returns the logger object' ); > }; > >-# Test diag method (output capture is complex, just verify it runs) > subtest 'Diag method test' => sub { > plan tests => 1; > > $logger->clear(); > $mocked_logger->debug('Debug message'); > >- # Just make sure it doesn't throw an exception >- eval { $logger->diag(); }; >- is( $@, '', 'diag() method executed without errors' ); >+ # Capture Test::Builder diag output >+ my $diag_output = ''; >+ open my $fake_fh, '>', \$diag_output or die "Can't open: $!"; >+ >+ my $tb = Test::More->builder; >+ my $original_fh = $tb->failure_output; >+ $tb->failure_output($fake_fh); # Redirect diag output >+ >+ $logger->diag(); >+ >+ $tb->failure_output($original_fh); >+ >+ like( $diag_output, qr/debug:\n#\s*"Debug message"/xms, 'Captured diag output' ); > }; > > # Test handling of empty log buffers >diff --git a/t/cypress/fixtures/sample.mrc b/t/cypress/fixtures/sample.xml >similarity index 100% >rename from t/cypress/fixtures/sample.mrc >rename to t/cypress/fixtures/sample.xml >diff --git a/t/cypress/integration/Acquisitions/Vendors_spec.ts b/t/cypress/integration/Acquisitions/Vendors_spec.ts >index 43a073d2d3..f39079932d 100644 >--- a/t/cypress/integration/Acquisitions/Vendors_spec.ts >+++ b/t/cypress/integration/Acquisitions/Vendors_spec.ts >@@ -233,3 +233,69 @@ describe("Vendor CRUD operations", () => { > .contains("deleted"); > }); > }); >+ >+describe("Vendor module", () => { >+ beforeEach(() => { >+ cy.login(); >+ cy.title().should("eq", "Koha staff interface"); >+ >+ cy.task("buildSampleObject", { >+ object: "vendor", >+ values: { active: 1 }, >+ }) >+ .then(generatedVendor => { >+ delete generatedVendor.list_currency; >+ delete generatedVendor.invoice_currency; >+ return cy.task("insertObject", { >+ type: "vendor", >+ object: generatedVendor, >+ }); >+ }) >+ .then(vendor => { >+ cy.wrap(vendor).as("vendor"); >+ return cy.task("buildSampleObject", { >+ object: "basket", >+ values: { vendor_id: vendor.id }, >+ }); >+ }) >+ .then(generatedBasket => { >+ return cy.task("insertObject", { >+ type: "basket", >+ object: generatedBasket, >+ }); >+ }) >+ .then(basket => { >+ cy.wrap(basket).as("basket"); >+ }); >+ }); >+ >+ afterEach(function () { >+ cy.task("deleteSampleObjects", [ >+ { vendor: this.vendor, basket: this.basket }, >+ ]); >+ }); >+ >+ it("receive should open in the same tab", function () { >+ cy.visit("/cgi-bin/koha/acquisition/vendors"); >+ >+ // table_id is currently 'DataTables_Table_0', and it should be fixed >+ cy.get("#vendors_list table.dataTable") >+ .invoke("attr", "id") >+ .then(table_id => { >+ cy.intercept("GET", "/api/v1/acquisitions/vendors*").as( >+ "get-vendors" >+ ); >+ cy.get(`#${table_id}_wrapper input.dt-input`).type( >+ this.vendor.name >+ ); >+ cy.wait("@get-vendors"); >+ cy.get(`#${table_id} tbody tr:first`) >+ .contains("Receive shipments") >+ .click(); >+ cy.url().should( >+ "contain", >+ `/cgi-bin/koha/acqui/parcels.pl?booksellerid=${this.vendor.id}` >+ ); >+ }); >+ }); >+}); >diff --git a/t/cypress/integration/Auth/csrf.ts b/t/cypress/integration/Auth/csrf.ts >index c17763090b..c24b691d04 100644 >--- a/t/cypress/integration/Auth/csrf.ts >+++ b/t/cypress/integration/Auth/csrf.ts >@@ -5,7 +5,7 @@ const branchname = "test_branchname"; > > function cleanup() { > const sql = "DELETE FROM branches WHERE branchcode=?"; >- cy.query(sql, branchcode); >+ cy.task("query", { sql, values: [branchcode] }); > } > > describe("CSRF", () => { >@@ -32,10 +32,10 @@ describe("CSRF", () => { > .find(".alert") > .contains(/No CSRF token passed for POST/); > >- cy.query( >- "SELECT COUNT(*) as count FROM branches WHERE branchcode=?", >- branchcode >- ).then(result => { >+ cy.task("query", { >+ sql: "SELECT COUNT(*) as count FROM branches WHERE branchcode=?", >+ values: [branchcode], >+ }).then(result => { > expect(result[0].count).to.equal(0); > }); > }); >@@ -53,10 +53,10 @@ describe("CSRF", () => { > .find(".alert") > .contains(/Wrong CSRF token/); > >- cy.query( >- "SELECT COUNT(*) as count FROM branches WHERE branchcode=?", >- branchcode >- ).then(result => { >+ cy.task("query", { >+ sql: "SELECT COUNT(*) as count FROM branches WHERE branchcode=?", >+ values: [branchcode], >+ }).then(result => { > expect(result[0].count).to.equal(0); > }); > }); >@@ -89,10 +89,10 @@ describe("CSRF", () => { > // We do not want Wrong CSRF token here > cy.get(".message").should("not.exist"); > >- cy.query( >- "SELECT COUNT(*) as count FROM branches WHERE branchcode=?", >- branchcode >- ).then(result => { >+ cy.task("query", { >+ sql: "SELECT COUNT(*) as count FROM branches WHERE branchcode=?", >+ values: [branchcode], >+ }).then(result => { > expect(result[0].count).to.equal(0); > }); > }); >@@ -112,19 +112,19 @@ describe("CSRF", () => { > cy.get("select[name='libraries_length']").select("-1"); > cy.get("td").contains(branchcode); > >- cy.query( >- "SELECT COUNT(*) as count FROM branches WHERE branchcode=?", >- branchcode >- ).then(result => { >+ cy.task("query", { >+ sql: "SELECT COUNT(*) as count FROM branches WHERE branchcode=?", >+ values: [branchcode], >+ }).then(result => { > expect(result[0].count).to.equal(1); > }); > }); > > it("Delete without CSRF", () => { >- cy.query("INSERT INTO branches(branchcode, branchname) VALUES (?, ?)", [ >- branchcode, >- branchname, >- ]); >+ cy.task("query", { >+ sql: "INSERT INTO branches(branchcode, branchname) VALUES (?, ?)", >+ values: [branchcode, branchname], >+ }); > > cy.visit("/cgi-bin/koha/admin/branches.pl"); > cy.get("select[name='libraries_length']").select("-1"); >@@ -141,19 +141,19 @@ describe("CSRF", () => { > .find(".alert") > .contains(/No CSRF token passed for POST/); > >- cy.query( >- "SELECT COUNT(*) as count FROM branches WHERE branchcode=?", >- branchcode >- ).then(result => { >+ cy.task("query", { >+ sql: "SELECT COUNT(*) as count FROM branches WHERE branchcode=?", >+ values: [branchcode], >+ }).then(result => { > expect(result[0].count).to.equal(1); > }); > }); > > it("Delete", () => { >- cy.query("INSERT INTO branches(branchcode, branchname) VALUES (?, ?)", [ >- branchcode, >- branchname, >- ]); >+ cy.task("query", { >+ sql: "INSERT INTO branches(branchcode, branchname) VALUES (?, ?)", >+ values: [branchcode, branchname], >+ }); > > cy.visit("/cgi-bin/koha/admin/branches.pl"); > cy.get("select[name='libraries_length']").select("-1"); >@@ -165,10 +165,10 @@ describe("CSRF", () => { > .find(".alert") > .contains(/Library deleted successfully/); > >- cy.query( >- "SELECT COUNT(*) as count FROM branches WHERE branchcode=?", >- branchcode >- ).then(result => { >+ cy.task("query", { >+ sql: "SELECT COUNT(*) as count FROM branches WHERE branchcode=?", >+ values: [branchcode], >+ }).then(result => { > expect(result[0].count).to.equal(0); > }); > }); >diff --git a/t/cypress/integration/ERM/Agreements_spec.ts b/t/cypress/integration/ERM/Agreements_spec.ts >index 6bbcb2cdf3..0b55448ee6 100644 >--- a/t/cypress/integration/ERM/Agreements_spec.ts >+++ b/t/cypress/integration/ERM/Agreements_spec.ts >@@ -569,9 +569,10 @@ describe("Agreement CRUD operations", () => { > "X-Base-Total-Count": "1", > "X-Total-Count": "1", > }, >- }); >+ }).as("get-agreements"); > cy.intercept("GET", "/api/v1/erm/agreements/*", agreement); > cy.visit("/cgi-bin/koha/erm/agreements"); >+ cy.wait("@get-agreements"); > > cy.get("#agreements_list table tbody tr:first") > .contains("Delete") >@@ -608,14 +609,8 @@ describe("Agreement CRUD operations", () => { > > // Delete from show > // Click the "name" link from the list >- cy.intercept("GET", "/api/v1/erm/agreements*", { >- statusCode: 200, >- body: agreements, >- headers: { >- "X-Base-Total-Count": "1", >- "X-Total-Count": "1", >- }, >- }); >+ cy.visit("/cgi-bin/koha/erm/agreements"); >+ cy.wait("@get-agreements"); > cy.intercept("GET", "/api/v1/erm/agreements/*", agreement).as( > "get-agreement" > ); >@@ -629,7 +624,6 @@ describe("Agreement CRUD operations", () => { > name_link.should("have.text", agreement.name); > name_link.click(); > cy.wait("@get-agreement"); >- cy.wait(500); // Cypress is too fast! Vue hasn't populated the form yet! > cy.get("#agreements_show h2").contains( > "Agreement #" + agreement.agreement_id > ); >diff --git a/t/cypress/integration/ERM/DataProviders_spec.ts b/t/cypress/integration/ERM/DataProviders_spec.ts >index 9c2ba4ecc5..f3cacef726 100644 >--- a/t/cypress/integration/ERM/DataProviders_spec.ts >+++ b/t/cypress/integration/ERM/DataProviders_spec.ts >@@ -636,7 +636,7 @@ describe("Data provider tab options", () => { > } > ); > >- cy.get("#files > form > fieldset > input[type=submit]").click(); >+ cy.get("#files > form > fieldset > button").click(); > > cy.get("main div[class='alert alert-info']").should( > "have.text", >diff --git a/t/cypress/integration/ERM/UsageReports_spec.ts b/t/cypress/integration/ERM/UsageReports_spec.ts >index ddd5402299..fb237a0fb1 100644 >--- a/t/cypress/integration/ERM/UsageReports_spec.ts >+++ b/t/cypress/integration/ERM/UsageReports_spec.ts >@@ -105,7 +105,9 @@ describe("Saved reports", () => { > { force: true } > ); > >- cy.get("#report_builder .default-report .action input").click(); >+ cy.get("#report_builder .default-report .action button") >+ .contains("Submit") >+ .click(); > > cy.url({ decode: true }).then(url => { > const urlParams = url.split("viewer?")[1].split("data=")[1]; >diff --git a/t/cypress/integration/Islands/AcquisitionsMenu_spec.ts b/t/cypress/integration/Islands/AcquisitionsMenu_spec.ts >index 8d2413324d..121e1c9402 100644 >--- a/t/cypress/integration/Islands/AcquisitionsMenu_spec.ts >+++ b/t/cypress/integration/Islands/AcquisitionsMenu_spec.ts >@@ -23,12 +23,14 @@ describe("Acquisitions menu", () => { > it("Should show/hide links based on permissions", () => { > cy.get(".sidebar_menu").should("be.visible"); > >- cy.query( >- "UPDATE borrowers SET flags=2052 WHERE borrowernumber=51" >- ).then(() => { >+ cy.task("query", { >+ sql: "UPDATE borrowers SET flags=2052 WHERE borrowernumber=51", >+ }).then(() => { > cy.reload(true); > cy.get(".sidebar_menu a").should("have.length", 8); >- cy.query("UPDATE borrowers SET flags=1 WHERE borrowernumber=51"); >+ cy.task("query", { >+ sql: "UPDATE borrowers SET flags=1 WHERE borrowernumber=51", >+ }); > }); > }); > it("Should correctly apply the 'current' class", () => { >diff --git a/t/cypress/integration/KohaTable/CirculationHistory_spec.ts b/t/cypress/integration/KohaTable/CirculationHistory_spec.ts >new file mode 100644 >index 0000000000..401ca93c7b >--- /dev/null >+++ b/t/cypress/integration/KohaTable/CirculationHistory_spec.ts >@@ -0,0 +1,23 @@ >+describe("members/readingrec", () => { >+ const table_id = "table_readingrec"; >+ beforeEach(() => { >+ cy.login(); >+ cy.title().should("eq", "Koha staff interface"); >+ cy.task("insertSampleCheckout").then(objects_checkout => { >+ cy.wrap(objects_checkout).as("objects_checkout"); >+ }); >+ }); >+ >+ afterEach(function () { >+ cy.task("deleteSampleObjects", [this.objects_checkout]); >+ }); >+ >+ it("'Type' column should be hidden", function () { >+ cy.visit( >+ `/cgi-bin/koha/members/readingrec.pl?borrowernumber=${this.objects_checkout.patron.patron_id}` >+ ); >+ >+ cy.get(`#${table_id} th`).contains("Type").should("not.exist"); >+ cy.get(`#${table_id} th:first`).contains("Date"); >+ }); >+}); >diff --git a/t/cypress/integration/KohaTable/Holdings_spec.ts b/t/cypress/integration/KohaTable/Holdings_spec.ts >index 25928874b1..8c77f67830 100644 >--- a/t/cypress/integration/KohaTable/Holdings_spec.ts >+++ b/t/cypress/integration/KohaTable/Holdings_spec.ts >@@ -1,7 +1,7 @@ > const RESTdefaultPageSize = "20"; // FIXME Mock this >-const baseTotalCount = "42"; >+const baseTotalCount = "21"; > >-describe("catalogue/detail/holdings_table", () => { >+describe("catalogue/detail/holdings_table with items", () => { > const table_id = "holdings_table"; > beforeEach(() => { > cy.login(); >@@ -10,144 +10,31 @@ describe("catalogue/detail/holdings_table", () => { > win.localStorage.clear(); > }); > >- // FIXME All the following code should not be reused as it >- // It must be moved to a Cypress command or task "buildSampleBiblio" or even "insertSampleBiblio" >- let generated_objects = {}; >- const objects = [{ object: "library" }, { object: "item_type" }]; >- cy.wrap(Promise.resolve()) >- .then(() => { >- return objects.reduce((chain, { object }) => { >- return chain.then(() => { >- return cy >- .task("buildSampleObject", { object }) >- .then(attributes => { >- generated_objects[object] = attributes; >- }); >- }); >- }, Promise.resolve()); >- }) >- .then(() => { >- const library = generated_objects["library"]; >- const item_type = generated_objects["item_type"]; >- const queries = [ >- { >- query: "INSERT INTO branches(branchcode, branchname) VALUES (?, ?)", >- values: [library.library_id, library.name], >- }, >- { >- query: "INSERT INTO itemtypes(itemtype, description) VALUES (?, ?)", >- values: [item_type.item_type_id, item_type.description], >- }, >- ]; >- cy.wrap(Promise.resolve()) >- .then(() => { >- return queries.reduce((chain, { query, values }) => { >- return chain.then(() => cy.query(query, values)); >- }, Promise.resolve()); >- }) >- .then(() => { >- let biblio = { >- leader: " nam a22 7a 4500", >- fields: [ >- { "005": "20250120101920.0" }, >- { >- "245": { >- ind1: "", >- ind2: "", >- subfields: [{ a: "Some boring read" }], >- }, >- }, >- { >- "100": { >- ind1: "", >- ind2: "", >- subfields: [ >- { c: "Some boring author" }, >- ], >- }, >- }, >- { >- "942": { >- ind1: "", >- ind2: "", >- subfields: [ >- { c: item_type.item_type_id }, >- ], >- }, >- }, >- ], >- }; >- cy.request({ >- method: "POST", >- url: "/api/v1/biblios", >- headers: { >- "Content-Type": "application/marc-in-json", >- "x-confirm-not-duplicate": 1, >- }, >- body: biblio, >- }).then(response => { >- const biblio_id = response.body.id; >- cy.wrap(biblio_id).as("biblio_id"); >- cy.request({ >- method: "POST", >- url: `/api/v1/biblios/${biblio_id}/items`, >- headers: { >- "Content-Type": "application/json", >- }, >- body: { >- home_library_id: library.library_id, >- holding_library_id: library.library_id, >- }, >- }); >- }); >- }); >- }); >- cy.query( >- "SELECT value FROM systempreferences WHERE variable='AlwaysShowHoldingsTableFilters'" >- ).then(value => { >+ cy.task("query", { >+ sql: "SELECT value FROM systempreferences WHERE variable='AlwaysShowHoldingsTableFilters'", >+ }).then(value => { > cy.wrap(value).as("syspref_AlwaysShowHoldingsTableFilters"); > }); >- }); > >- afterEach( >- () => >- function () { >- cleanup(); >- cy.set_syspref( >- "AlwaysShowHoldingsTableFilters", >- this.syspref_AlwaysShowHoldingsTableFilters >- ); >+ cy.task("insertSampleBiblio", { item_count: baseTotalCount }).then( >+ objects => { >+ cy.wrap(objects).as("objects"); > } >- ); >+ ); >+ }); > >- it("Correctly init the table", function () { >- // Do not use `() => {` or this.biblio_id won't be retrieved >- const biblio_id = this.biblio_id; >- cy.task("buildSampleObjects", { >- object: "item", >- count: RESTdefaultPageSize, >- values: { >- biblio_id, >- checkout: null, >- transfer: null, >- lost_status: 0, >- withdrawn: 0, >- damaged_status: 0, >- not_for_loan_status: 0, >- course_item: null, >- cover_image_ids: [], >- _status: ["available"], >- }, >- }).then(items => { >- cy.intercept("get", `/api/v1/biblios/${biblio_id}/items*`, { >- statuscode: 200, >- body: items, >- headers: { >- "X-Base-Total-Count": baseTotalCount, >- "X-Total-Count": baseTotalCount, >- }, >- }); >+ afterEach(function () { >+ cy.task("deleteSampleObjects", [this.objects]); >+ cy.set_syspref( >+ "AlwaysShowHoldingsTableFilters", >+ this.syspref_AlwaysShowHoldingsTableFilters >+ ); >+ }); > >+ it("Correctly init the table", function () { >+ // Do not use `() => {` or this.objects won't be retrieved >+ const biblio_id = this.objects.biblio.biblio_id; >+ cy.set_syspref("AlwaysShowHoldingsTableFilters", 1).then(() => { > cy.visit( > "/cgi-bin/koha/catalogue/detail.pl?biblionumber=" + biblio_id > ); >@@ -164,248 +51,247 @@ describe("catalogue/detail/holdings_table", () => { > }); > > it("Show filters", function () { >- // Do not use `() => {` or this.biblio_id won't be retrieved >- const biblio_id = this.biblio_id; >- cy.task("buildSampleObjects", { >- object: "item", >- count: RESTdefaultPageSize, >- values: { >- biblio_id, >- checkout: null, >- transfer: null, >- lost_status: 0, >- withdrawn: 0, >- damaged_status: 0, >- not_for_loan_status: 0, >- course_item: null, >- cover_image_ids: [], >- _status: ["available"], >- }, >- }).then(items => { >- cy.intercept("get", `/api/v1/biblios/${biblio_id}/items*`, { >- statuscode: 200, >- body: items, >- headers: { >- "X-Base-Total-Count": baseTotalCount, >- "X-Total-Count": baseTotalCount, >- }, >- }); >+ // Do not use `() => {` or this.objects won't be retrieved >+ const biblio_id = this.objects.biblio.biblio_id; > >- cy.set_syspref("AlwaysShowHoldingsTableFilters", 0).then(() => { >- cy.visit( >- "/cgi-bin/koha/catalogue/detail.pl?biblionumber=" + >- biblio_id >- ); >+ cy.set_syspref("AlwaysShowHoldingsTableFilters", 0).then(() => { >+ cy.visit( >+ "/cgi-bin/koha/catalogue/detail.pl?biblionumber=" + biblio_id >+ ); > >- // Hide the 'URL' column >- cy.mock_table_settings( >- { >- columns: { uri: { is_hidden: 1 } }, >- }, >- "items_table_settings.holdings" >- ); >+ // Hide the 'URL' column >+ cy.mock_table_settings( >+ { >+ columns: { uri: { is_hidden: 1 } }, >+ }, >+ "items_table_settings.holdings" >+ ); > >- cy.get("@columns").then(columns => { >- cy.get(`#${table_id}_wrapper tbody tr`).should( >- "have.length", >- RESTdefaultPageSize >- ); >+ cy.get("@columns").then(columns => { >+ cy.get(`#${table_id}_wrapper tbody tr`).should( >+ "have.length", >+ RESTdefaultPageSize >+ ); > >- // Filters are not displayed >- cy.get(`#${table_id} thead tr`).should("have.length", 1); >+ // Filters are not displayed >+ cy.get(`#${table_id} thead tr`).should("have.length", 1); > >- cy.get(`#${table_id} th`).contains("Status"); >- cy.get(`#${table_id} th`) >- .contains("URL") >- .should("not.exist"); >- cy.get(`#${table_id} th`) >- .contains("Course reserves") >- .should("not.exist"); >+ cy.get(`#${table_id} th`).contains("Status"); >+ cy.get(`#${table_id} th`).contains("URL").should("not.exist"); >+ cy.get(`#${table_id} th`) >+ .contains("Course reserves") >+ .should("not.exist"); > >- cy.get(`.${table_id}_table_controls .show_filters`).click(); >- cy.get(`#${table_id}_wrapper .dt-info`).contains( >- `Showing 1 to ${RESTdefaultPageSize} of ${baseTotalCount} entries` >- ); >- // Filters are displayed >- cy.get(`#${table_id} thead tr`).should("have.length", 2); >- >- cy.get(`#${table_id} th`).contains("Status"); >- cy.get(`#${table_id} th`) >- .contains("URL") >- .should("not.exist"); >- cy.get(`#${table_id} th`) >- .contains("Course reserves") >- .should("not.exist"); >- }); >+ cy.get(`.${table_id}_table_controls .show_filters`).click(); >+ cy.get(`#${table_id}_wrapper .dt-info`).contains( >+ `Showing 1 to ${RESTdefaultPageSize} of ${baseTotalCount} entries` >+ ); >+ // Filters are displayed >+ cy.get(`#${table_id} thead tr`).should("have.length", 2); >+ >+ cy.get(`#${table_id} th`).contains("Status"); >+ cy.get(`#${table_id} th`).contains("URL").should("not.exist"); >+ cy.get(`#${table_id} th`) >+ .contains("Course reserves") >+ .should("not.exist"); > }); >+ }); > >- cy.set_syspref("AlwaysShowHoldingsTableFilters", 1).then(() => { >- cy.visit( >- "/cgi-bin/koha/catalogue/detail.pl?biblionumber=" + >- biblio_id >- ); >+ cy.set_syspref("AlwaysShowHoldingsTableFilters", 1).then(() => { >+ cy.visit( >+ "/cgi-bin/koha/catalogue/detail.pl?biblionumber=" + biblio_id >+ ); > >- // Hide the 'URL' column >- cy.mock_table_settings( >- { >- columns: { uri: { is_hidden: 1 } }, >- }, >- "items_table_settings.holdings" >- ); >+ // Hide the 'URL' column >+ cy.mock_table_settings( >+ { >+ columns: { uri: { is_hidden: 1 } }, >+ }, >+ "items_table_settings.holdings" >+ ); > >- cy.get("@columns").then(columns => { >- cy.get(`#${table_id}_wrapper tbody tr`).should( >- "have.length", >- RESTdefaultPageSize >- ); >+ cy.get("@columns").then(columns => { >+ cy.get(`#${table_id}_wrapper tbody tr`).should( >+ "have.length", >+ RESTdefaultPageSize >+ ); > >- // Filters are displayed >- cy.get(`#${table_id} thead tr`).should("have.length", 2); >+ // Filters are displayed >+ cy.get(`#${table_id} thead tr`).should("have.length", 2); > >- cy.get(`.${table_id}_table_controls .hide_filters`).click(); >+ cy.get(`.${table_id}_table_controls .hide_filters`).click(); > >- // Filters are not displayed >- cy.get(`#${table_id} thead tr`).should("have.length", 1); >- }); >+ // Filters are not displayed >+ cy.get(`#${table_id} thead tr`).should("have.length", 1); > }); > }); > }); > > it("Filters by code and description", function () { >- // Do not use `() => {` or this.biblio_id won't be retrieved >- const biblio_id = this.biblio_id; >- cy.task("buildSampleObjects", { >- object: "item", >- count: RESTdefaultPageSize, >- values: { >- biblio_id, >- checkout: null, >- transfer: null, >- lost_status: 0, >- withdrawn: 0, >- damaged_status: 0, >- not_for_loan_status: 0, >- course_item: null, >- cover_image_ids: [], >- _status: ["available"], >- }, >- }).then(items => { >- cy.intercept("get", `/api/v1/biblios/${biblio_id}/items*`, { >- statuscode: 200, >- body: items, >- headers: { >- "X-Base-Total-Count": baseTotalCount, >- "X-Total-Count": baseTotalCount, >- }, >- }).as("searchItems"); >- >- cy.visit( >- "/cgi-bin/koha/catalogue/detail.pl?biblionumber=" + biblio_id >- ); >+ // Do not use `() => {` or this.objects won't be retrieved >+ const biblio_id = this.objects.biblio.biblio_id; >+ >+ cy.intercept("get", `/api/v1/biblios/${biblio_id}/items*`).as( >+ "searchItems" >+ ); >+ >+ cy.visit("/cgi-bin/koha/catalogue/detail.pl?biblionumber=" + biblio_id); >+ >+ cy.wait("@searchItems"); >+ >+ cy.task("query", { >+ sql: "SELECT homebranch FROM items WHERE biblionumber=? LIMIT 1", >+ values: [biblio_id], >+ }).then(result => { >+ let library_id = result[0].homebranch; >+ cy.task("query", { >+ sql: "SELECT branchname FROM branches WHERE branchcode=?", >+ values: [library_id], >+ }).then(result => { >+ let library_name = result[0].branchname; >+ cy.get(`#${table_id}_wrapper input.dt-input`).type(library_id); >+ >+ cy.wait("@searchItems").then(interception => { >+ const q = interception.request.query.q; >+ expect(q).to.match( >+ new RegExp( >+ `"me.home_library_id":{"like":"%${library_id}%"}` >+ ) >+ ); >+ }); > >- cy.window().then(win => { >- win.coded_values.library = new Map( >- items.map(i => [ >- i.home_library.name, >- i.home_library.library_id, >- ]) >- ); >- win.coded_values.item_type = new Map( >- items.map(i => [ >- i.item_type.description, >- i.item_type.item_type_id, >- ]) >+ cy.get(`#${table_id}_wrapper input.dt-input`).clear(); >+ cy.wait("@searchItems"); >+ cy.get(`#${table_id}_wrapper input.dt-input`).type( >+ library_name > ); >+ >+ cy.wait("@searchItems").then(interception => { >+ const q = interception.request.query.q; >+ expect(q).to.match( >+ new RegExp(`"me.home_library_id":\\["${library_id}"\\]`) >+ ); >+ }); > }); >- cy.wait("@searchItems"); >- >- let library_id = items[0].home_library.library_id; >- let library_name = items[0].home_library.name; >- cy.get(`#${table_id}_wrapper input.dt-input`).type(library_id); >- >- cy.wait("@searchItems").then(interception => { >- const q = interception.request.query.q; >- expect(q).to.match( >- new RegExp( >- `"me.home_library_id":{"like":"%${library_id}%"}` >- ) >+ }); >+ >+ cy.task("query", { >+ sql: "SELECT itype FROM items WHERE biblionumber=? LIMIT 1", >+ values: [biblio_id], >+ }).then(result => { >+ let item_type_id = result[0].itype; >+ cy.task("query", { >+ sql: "SELECT description FROM itemtypes WHERE itemtype=?", >+ values: [item_type_id], >+ }).then(result => { >+ let item_type_description = result[0].description; >+ >+ cy.get(`#${table_id}_wrapper input.dt-input`).clear(); >+ cy.wait("@searchItems"); >+ cy.get(`#${table_id}_wrapper input.dt-input`).type( >+ item_type_id > ); >- }); > >- cy.get(`#${table_id}_wrapper input.dt-input`).clear(); >- cy.wait("@searchItems"); >- cy.get(`#${table_id}_wrapper input.dt-input`).type(library_name); >+ cy.wait("@searchItems").then(interception => { >+ const q = interception.request.query.q; >+ expect(q).to.match( >+ new RegExp( >+ `"me.item_type_id":{"like":"%${item_type_id}%"}` >+ ) >+ ); >+ }); > >- cy.wait("@searchItems").then(interception => { >- const q = interception.request.query.q; >- expect(q).to.match( >- new RegExp(`"me.home_library_id":\\["${library_id}"\\]`) >+ cy.get(`#${table_id}_wrapper input.dt-input`).clear(); >+ cy.wait("@searchItems"); >+ cy.get(`#${table_id}_wrapper input.dt-input`).type( >+ item_type_description > ); >- }); > >- let item_type_id = items[0].item_type.item_type_id; >- let item_type_description = items[0].item_type.description; >- cy.get(`#${table_id}_wrapper input.dt-input`).clear(); >- cy.wait("@searchItems"); >- cy.get(`#${table_id}_wrapper input.dt-input`).type(item_type_id); >+ cy.wait("@searchItems").then(interception => { >+ const q = interception.request.query.q; >+ expect(q).to.match( >+ new RegExp(`"me.item_type_id":\\["${item_type_id}"\\]`) >+ ); >+ }); > >- cy.wait("@searchItems").then(interception => { >- const q = interception.request.query.q; >- expect(q).to.match( >- new RegExp(`"me.item_type_id":{"like":"%${item_type_id}%"}`) >- ); >+ cy.viewport(2999, 2999); >+ cy.get(`#${table_id}_wrapper input.dt-input`).clear(); >+ cy.wait("@searchItems"); >+ // Show filters if not there already >+ cy.get(`.${table_id}_table_controls .show_filters`) >+ .then(link => { >+ if (link.is(":visible")) { >+ cy.wrap(link).click(); >+ cy.wait("@searchItems"); >+ } >+ }) >+ .then(() => { >+ // Select first (non-empty) option >+ cy.get( >+ `#${table_id}_wrapper th#holdings_itype select` >+ ).then(select => { >+ const raw_value = select.find("option").eq(1).val(); >+ expect(raw_value).to.match(/^\^/); >+ expect(raw_value).to.match(/\$$/); >+ item_type_id = raw_value.replace(/^\^|\$$/g, ""); // Remove ^ and $ >+ }); >+ cy.get( >+ `#${table_id}_wrapper th#holdings_itype select option` >+ ) >+ .eq(1) >+ .then(o => { >+ cy.get( >+ `#${table_id}_wrapper th#holdings_itype select` >+ ).select(o.val(), { force: true }); >+ }); >+ cy.wait("@searchItems").then(interception => { >+ const q = interception.request.query.q; >+ expect(q).to.match( >+ new RegExp( >+ `{"me.item_type_id":"${item_type_id}"}` >+ ) >+ ); >+ }); >+ }); > }); >+ }); >+ }); >+}); > >- cy.get(`#${table_id}_wrapper input.dt-input`).clear(); >- cy.wait("@searchItems"); >- cy.get(`#${table_id}_wrapper input.dt-input`).type( >- item_type_description >- ); >+describe("catalogue/detail/holdings_table without items", () => { >+ const table_id = "holdings_table"; >+ beforeEach(() => { >+ cy.login(); >+ cy.title().should("eq", "Koha staff interface"); >+ cy.window().then(win => { >+ win.localStorage.clear(); >+ }); > >- cy.wait("@searchItems").then(interception => { >- const q = interception.request.query.q; >- expect(q).to.match( >- new RegExp(`"me.item_type_id":\\["${item_type_id}"\\]`) >- ); >- }); >+ cy.task("query", { >+ sql: "SELECT value FROM systempreferences WHERE variable='AlwaysShowHoldingsTableFilters'", >+ }).then(value => { >+ cy.wrap(value).as("syspref_AlwaysShowHoldingsTableFilters"); >+ }); > >- cy.viewport(2999, 2999); >- cy.get(`#${table_id}_wrapper input.dt-input`).clear(); >- cy.wait("@searchItems"); >- // Show filters if not there already >- cy.get(`.${table_id}_table_controls .show_filters`) >- .then(link => { >- if (link.is(":visible")) { >- cy.wrap(link).click(); >- cy.wait("@searchItems"); >- } >- }) >- .then(() => { >- // Select first (non-empty) option >- cy.get( >- `#${table_id}_wrapper th#holdings_itype select` >- ).then(select => { >- const raw_value = select.find("option").eq(1).val(); >- expect(raw_value).to.match(/^\^/); >- expect(raw_value).to.match(/\$$/); >- item_type_id = raw_value.replace(/^\^|\$$/g, ""); // Remove ^ and $ >- }); >- cy.get( >- `#${table_id}_wrapper th#holdings_itype select option` >- ) >- .eq(1) >- .then(o => { >- cy.get( >- `#${table_id}_wrapper th#holdings_itype select` >- ).select(o.val(), { force: true }); >- }); >- cy.wait("@searchItems").then(interception => { >- const q = interception.request.query.q; >- expect(q).to.match( >- new RegExp(`{"me.item_type_id":"${item_type_id}"}`) >- ); >- }); >- }); >+ cy.task("insertSampleBiblio", { item_count: 0 }).then(objects => { >+ cy.wrap(objects).as("objects"); > }); > }); >+ >+ afterEach(function () { >+ cy.task("deleteSampleObjects", [this.objects]); >+ cy.set_syspref( >+ "AlwaysShowHoldingsTableFilters", >+ this.syspref_AlwaysShowHoldingsTableFilters >+ ); >+ }); >+ >+ it("Do not display the table", function () { >+ // Do not use `() => {` or this.objects won't be retrieved >+ const biblio_id = this.objects.biblio.biblio_id; >+ >+ cy.visit("/cgi-bin/koha/catalogue/detail.pl?biblionumber=" + biblio_id); >+ >+ cy.get(`#${table_id}_wrapper`).should("not.exist"); >+ }); > }); >diff --git a/t/cypress/integration/KohaTable/KohaTable_spec.ts b/t/cypress/integration/KohaTable/KohaTable_spec.ts >index 035404c11a..3c0ca843fd 100644 >--- a/t/cypress/integration/KohaTable/KohaTable_spec.ts >+++ b/t/cypress/integration/KohaTable/KohaTable_spec.ts >@@ -333,7 +333,7 @@ describe("kohaTable (using REST API)", () => { > > cy.window().then(win => { > win.categories_map = patrons.reduce((map, p) => { >- map[p.category_id] = p.category_id; >+ map[p.category_id.toLowerCase()] = p.category_id; > return map; > }, {}); > }); >@@ -399,7 +399,7 @@ describe("kohaTable (using REST API)", () => { > > cy.window().then(win => { > win.categories_map = patrons.reduce((map, p) => { >- map[p.category_id] = p.category_id; >+ map[p.category_id.toLowerCase()] = p.category_id; > return map; > }, {}); > }); >diff --git a/t/cypress/integration/KohaTable/OPACCirculationHistory_spec.ts b/t/cypress/integration/KohaTable/OPACCirculationHistory_spec.ts >new file mode 100644 >index 0000000000..4e6786ca4f >--- /dev/null >+++ b/t/cypress/integration/KohaTable/OPACCirculationHistory_spec.ts >@@ -0,0 +1,53 @@ >+describe("opac-readingrecord", () => { >+ beforeEach(() => { >+ cy.loginOpac(); >+ let objects_to_cleanup = []; >+ cy.task("apiGet", { >+ endpoint: "/api/v1/patrons/51", >+ }) >+ .then(patron => { >+ [...Array(51)].forEach(() => { >+ cy.task("insertSampleCheckout", { >+ patron: patron, >+ }).then(objects_checkout => { >+ cy.task("query", { >+ sql: "INSERT INTO old_issues SELECT * FROM issues WHERE issue_id=?", >+ values: [objects_checkout.checkout.checkout_id], >+ }) >+ .then(() => { >+ cy.task("query", { >+ sql: "DELETE FROM issues WHERE issue_id=?", >+ values: [ >+ objects_checkout.checkout.checkout_id, >+ ], >+ }); >+ }) >+ .then(() => { >+ objects_checkout.old_checkout = >+ objects_checkout.checkout; >+ delete objects_checkout.checkout; >+ objects_to_cleanup.push(objects_checkout); >+ }); >+ }); >+ }); >+ }) >+ .then(() => { >+ cy.wrap(objects_to_cleanup).as("objects_to_cleanup"); >+ }); >+ }); >+ >+ afterEach(function () { >+ cy.task("deleteSampleObjects", this.objects_to_cleanup); >+ }); >+ >+ it("50 items should be displayed by default", function () { >+ cy.visitOpac("/cgi-bin/koha/opac-readingrecord.pl"); >+ >+ cy.contains("Showing 1 to 50 of 50 entries"); >+ cy.get("table#readingrec tbody tr").should("have.length", 50); >+ >+ cy.contains("Show all items").click(); >+ cy.contains("Showing 1 to 51 of 51 entries"); >+ cy.get("table#readingrec tbody tr").should("have.length", 51); >+ }); >+}); >diff --git a/t/cypress/integration/KohaTable/PatronSearch_spec.ts b/t/cypress/integration/KohaTable/PatronSearch_spec.ts >index b706f1eb80..782cb45f4f 100644 >--- a/t/cypress/integration/KohaTable/PatronSearch_spec.ts >+++ b/t/cypress/integration/KohaTable/PatronSearch_spec.ts >@@ -9,7 +9,7 @@ const patron_attr_type = "attribute_type4TEST"; > > function cleanup() { > const sql = "DELETE FROM borrower_attribute_types WHERE code=?"; >- cy.query(sql, patron_attr_type); >+ cy.task("query", { sql, values: [patron_attr_type] }); > } > describe("ExtendedPatronAttributes", () => { > beforeEach(() => { >@@ -19,23 +19,20 @@ describe("ExtendedPatronAttributes", () => { > cy.window().then(win => { > win.localStorage.clear(); > }); >- cy.query( >- "SELECT value FROM systempreferences WHERE variable='ExtendedPatronAttributes'" >- ).then(value => { >+ cy.task("query", { >+ sql: "SELECT value FROM systempreferences WHERE variable='ExtendedPatronAttributes'", >+ }).then(value => { > cy.wrap(value).as("syspref_ExtendedPatronAttributes"); > }); > }); > >- afterEach( >- () => >- function () { >- cleanup(); >- cy.set_syspref( >- "ExtendedPatronAttributes", >- this.syspref_ExtendedPatronAttributes >- ); >- } >- ); >+ afterEach(function () { >+ cleanup(); >+ cy.set_syspref( >+ "ExtendedPatronAttributes", >+ this.syspref_ExtendedPatronAttributes >+ ); >+ }); > > const table_id = "memberresultst"; > >@@ -48,9 +45,9 @@ describe("ExtendedPatronAttributes", () => { > cy.get("#search_patron_filter").type("something"); > cy.get("form.patron_search_form input[type='submit']").click(); > >- cy.query( >- "select count(*) as nb_searchable from borrower_attribute_types where staff_searchable=1" >- ).then(result => { >+ cy.task("query", { >+ sql: "select count(*) as nb_searchable from borrower_attribute_types where staff_searchable=1", >+ }).then(result => { > const has_searchable = result[0].nb_searchable; > cy.wait("@searchPatrons").then(interception => { > const q = interception.request.query.q; >@@ -58,10 +55,10 @@ describe("ExtendedPatronAttributes", () => { > }); > }); > >- cy.query( >- "INSERT INTO borrower_attribute_types(code, description, staff_searchable, searched_by_default) VALUES (?, 'only for tests', 1, 1)", >- patron_attr_type >- ).then(() => { >+ cy.task("query", { >+ sql: "INSERT INTO borrower_attribute_types(code, description, staff_searchable, searched_by_default) VALUES (?, 'only for tests', 1, 1)", >+ values: [patron_attr_type], >+ }).then(() => { > cy.visit("/cgi-bin/koha/members/members-home.pl"); > > cy.get("#search_patron_filter").type("something"); >@@ -83,9 +80,9 @@ describe("ExtendedPatronAttributes", () => { > cy.get("#search_patron_filter").type("something"); > cy.get("form.patron_search_form input[type='submit']").click(); > >- cy.query( >- "select count(*) as nb_searchable from borrower_attribute_types where staff_searchable=1 AND searched_by_default=1" >- ).then(result => { >+ cy.task("query", { >+ sql: "select count(*) as nb_searchable from borrower_attribute_types where staff_searchable=1 AND searched_by_default=1", >+ }).then(result => { > const has_searchable = result[0].nb_searchable; > cy.wait("@searchPatrons").then(interception => { > const q = interception.request.query.q; >@@ -97,10 +94,10 @@ describe("ExtendedPatronAttributes", () => { > }); > }); > >- cy.query( >- "INSERT INTO borrower_attribute_types(code, description, staff_searchable, searched_by_default) VALUES (?, 'only for tests', 1, 1)", >- patron_attr_type >- ).then(() => { >+ cy.task("query", { >+ sql: "INSERT INTO borrower_attribute_types(code, description, staff_searchable, searched_by_default) VALUES (?, 'only for tests', 1, 1)", >+ values: [patron_attr_type], >+ }).then(() => { > cy.visit("/cgi-bin/koha/members/members-home.pl"); > > cy.get("#search_patron_filter").type("something"); >diff --git a/t/cypress/integration/KohaTable/PendingHolds_spec.ts b/t/cypress/integration/KohaTable/PendingHolds_spec.ts >new file mode 100644 >index 0000000000..7b5ce67580 >--- /dev/null >+++ b/t/cypress/integration/KohaTable/PendingHolds_spec.ts >@@ -0,0 +1,101 @@ >+describe("circ/pendingreserves/holdst", () => { >+ const table_id = "holdst"; >+ beforeEach(() => { >+ cy.login(); >+ cy.title().should("eq", "Koha staff interface"); >+ cy.task("insertSampleBiblio", { >+ item_count: 2, >+ options: { different_libraries: true }, >+ }).then(objects_biblio_1 => { >+ cy.wrap(objects_biblio_1).as("objects_biblio_1"); >+ const commonLibraryId = objects_biblio_1.items[0].home_library_id; >+ cy.task("insertSampleHold", { >+ biblio: objects_biblio_1.biblio, >+ library_id: commonLibraryId, >+ }).then(objects_hold_1 => { >+ cy.wrap(objects_hold_1).as("objects_hold_1"); >+ }); >+ cy.task("insertSampleBiblio", { item_count: 1 }).then( >+ objects_biblio_2 => { >+ cy.wrap(objects_biblio_2).as("objects_biblio_2"); >+ cy.task("insertSampleHold", { >+ biblio: objects_biblio_2.biblio, >+ library_id: objects_biblio_2.items[0].home_library_id, >+ }).then(objects_hold_2 => { >+ cy.wrap(objects_hold_2).as("objects_hold_2"); >+ }); >+ cy.task("query", { >+ sql: "UPDATE items SET homebranch=?, holdingbranch=? WHERE itemnumber=?", >+ values: [ >+ commonLibraryId, >+ commonLibraryId, >+ objects_biblio_2.items[0].item_id, >+ ], >+ }); >+ } >+ ); >+ }); >+ }); >+ >+ afterEach(function () { >+ cy.task("deleteSampleObjects", [ >+ this.objects_hold_1, >+ this.objects_hold_2, >+ this.objects_biblio_1, >+ this.objects_biblio_2, >+ ]); >+ }); >+ >+ it("Should render library filters", function () { >+ cy.visit( >+ "/cgi-bin/koha/circ/pendingreserves.pl?from=2000-01-01&to=2999-12-31&run_report=Submit" >+ ); >+ >+ cy.get(`#${table_id} thead tr:eq(1) th:eq(5)`).should( >+ "have.text", >+ "Libraries" >+ ); >+ >+ // select has library names in value and text, this table is not using server-side processing >+ let libraries = this.objects_biblio_1.libraries >+ .map(library => library.name) >+ .sort(); >+ cy.get(`#${table_id} thead tr:eq(1) th:eq(5) select`) >+ .children() >+ .should("have.length", 3) >+ .then(options => { >+ expect(options.eq(0).val()).to.eq(""); >+ expect(options.eq(1).val()).to.eq(libraries[0]); >+ expect(options.eq(2).val()).to.eq(libraries[1]); >+ }); >+ }); >+ >+ it("Should filter table on library", function () { >+ cy.visit( >+ "/cgi-bin/koha/circ/pendingreserves.pl?from=2000-01-01&to=2999-12-31&run_report=Submit" >+ ); >+ >+ cy.get(`#${table_id} thead tr:eq(1) th:eq(5) select`).select( >+ this.objects_biblio_1.libraries[0].name, >+ { force: true } >+ ); >+ cy.get(`#${table_id} tbody tr:eq(0) td:eq(4)`).should( >+ "contain", >+ this.objects_biblio_1.biblio.title >+ ); >+ cy.get(`#${table_id} tbody tr:eq(1) td:eq(4)`).should( >+ "contain", >+ this.objects_biblio_2.biblio.title >+ ); >+ cy.get(`#${table_id} tbody tr`).should("have.length", 2); >+ >+ cy.get(`#${table_id} thead tr:eq(1) th:eq(5) select`).select( >+ this.objects_biblio_1.libraries[1].name >+ ); >+ cy.get(`#${table_id} tbody tr:eq(0) td:eq(4)`).should( >+ "contain", >+ this.objects_biblio_1.biblio.title >+ ); >+ cy.get(`#${table_id} tbody tr`).should("have.length", 1); >+ }); >+}); >diff --git a/t/cypress/integration/Toolbar_spec.ts b/t/cypress/integration/Toolbar_spec.ts >index 385634ad52..029aee98e4 100644 >--- a/t/cypress/integration/Toolbar_spec.ts >+++ b/t/cypress/integration/Toolbar_spec.ts >@@ -1,39 +1,9 @@ >-describe("Sticky toolbar", () => { >+describe("Sticky toolbar - basic behavior", () => { > beforeEach(() => { > cy.login(); > cy.title().should("eq", "Koha staff interface"); > }); > >- it("Should open non-Vue links correctly in the same tab", () => { >- const vendor = cy.getVendor(); >- vendor.baskets_count = 1; >- // Click the "name" link from the list >- cy.intercept("GET", "/api/v1/acquisitions/vendors\?*", { >- statusCode: 200, >- body: [vendor], >- headers: { >- "X-Base-Total-Count": "1", >- "X-Total-Count": "1", >- }, >- }).as("get-vendors"); >- cy.intercept( >- "GET", >- new RegExp("/api/v1/acquisitions/vendors/(?!config$).+"), >- vendor >- ).as("get-vendor"); >- >- cy.visit("/cgi-bin/koha/acquisition/vendors"); >- cy.wait("@get-vendors"); >- >- const name_link = cy.get( >- "#vendors_list table tbody tr:first td:first a" >- ); >- name_link.click(); >- cy.wait("@get-vendor"); >- cy.get("#toolbar a").contains("Receive shipments").click(); >- cy.get("h1").contains("Receive shipment from vendor " + vendor.name); >- }); >- > it("Should stick on scroll", () => { > cy.visit("/cgi-bin/koha/acqui/acqui-home.pl"); > >@@ -46,3 +16,52 @@ describe("Sticky toolbar", () => { > cy.get("#toolbar").should("not.have.class", "floating"); > }); > }); >+ >+describe("Sticky toolbar - vendors", () => { >+ beforeEach(() => { >+ cy.login(); >+ cy.title().should("eq", "Koha staff interface"); >+ >+ cy.task("buildSampleObject", { >+ object: "vendor", >+ values: { active: 1 }, >+ }) >+ .then(generatedVendor => { >+ delete generatedVendor.list_currency; >+ delete generatedVendor.invoice_currency; >+ return cy.task("insertObject", { >+ type: "vendor", >+ object: generatedVendor, >+ }); >+ }) >+ .then(vendor => { >+ cy.wrap(vendor).as("vendor"); >+ return cy.task("buildSampleObject", { >+ object: "basket", >+ values: { vendor_id: vendor.id }, >+ }); >+ }) >+ .then(generatedBasket => { >+ return cy.task("insertObject", { >+ type: "basket", >+ object: generatedBasket, >+ }); >+ }) >+ .then(basket => { >+ cy.wrap(basket).as("basket"); >+ }); >+ }); >+ afterEach(function () { >+ cy.task("deleteSampleObjects", [ >+ { vendor: this.vendor, basket: this.basket }, >+ ]); >+ }); >+ it("Should open non-Vue links correctly in the same tab", function () { >+ cy.visit(`/cgi-bin/koha/acquisition/vendors/${this.vendor.id}`); >+ >+ cy.get("#toolbar a").contains("Receive shipments").click(); >+ cy.get("h1").contains( >+ `Receive shipment from vendor ${this.vendor.name}` >+ ); >+ }); >+}); >diff --git a/t/cypress/integration/Tools/ManageMarcImport_spec.ts b/t/cypress/integration/Tools/ManageMarcImport_spec.ts >index 4141a99ff2..e27f3cf9a1 100644 >--- a/t/cypress/integration/Tools/ManageMarcImport_spec.ts >+++ b/t/cypress/integration/Tools/ManageMarcImport_spec.ts >@@ -27,65 +27,53 @@ describe("loads the manage MARC import page", () => { > it("upload a MARC record", () => { > cy.visit("/cgi-bin/koha/tools/stage-marc-import.pl"); > >- cy.get('input[type="file"]').selectFile( >- "t/cypress/fixtures/sample.mrc" >- ); >- cy.get('form[id="uploadfile"]').within(() => { >- cy.get('button[id="fileuploadbutton"]').click(); >- }); >+ cy.fixture("sample.xml", null).as("sample_xml"); >+ cy.get("input[type=file]").selectFile("@sample_xml"); >+ cy.get("#fileuploadbutton").click(); > >- //wait after file upload, it can go to quickly here >- cy.wait(2000); >+ cy.get("#fileuploadstatus").contains("100%"); >+ cy.get("legend") >+ .contains("Look for existing records in catalog?") >+ .should("be.visible"); > > //check default values >- cy.get('select[name="matcher"] option:selected').should( >- "have.value", >- "" >- ); >- cy.get('select[name="overlay_action"] option:selected').should( >+ cy.get("select#matcher option:selected").should("have.value", ""); >+ cy.get("select#overlay_action option:selected").should( > "have.value", > "replace" > ); >- cy.get('select[name="nomatch_action"] option:selected').should( >+ cy.get("select#nomatch_action option:selected").should( > "have.value", > "create_new" > ); >- cy.get('select[name="item_action"] option:selected').should( >+ cy.get("select#item_action option:selected").should( > "have.value", > "always_add" > ); > >- cy.get('select[name="format"]').select("MARCXML", { force: true }); >- cy.get("#format").should("have.value", "MARCXML"); >+ cy.get('select[name="format"]').should("have.value", "MARCXML"); > > //select some new options > cy.get("#matcher").select("3", { force: true }); >- cy.get("#overlay_action").select("create_new", { force: true }); >- cy.get("#nomatch_action").select("ignore", { force: true }); >- cy.get("#item_action").select("ignore", { force: true }); >+ cy.get("#matcher") >+ .select("3", { force: true }) >+ .should("have.value", "3"); >+ cy.get("#overlay_action") >+ .select("create_new", { force: true }) >+ .should("have.value", "create_new"); >+ cy.get("#nomatch_action") >+ .select("ignore", { force: true }) >+ .should("have.value", "ignore"); >+ cy.get("#item_action") >+ .select("ignore", { force: true }) >+ .should("have.value", "ignore"); > >- //remove focus >- //cy.get('#item_action').blur(); >- cy.screenshot("after_selection"); >- >- // Now verify all values >- cy.get("#matcher").should("have.value", "3"); >- cy.get("#overlay_action").should("have.value", "create_new"); >- cy.get("#nomatch_action").should("have.value", "ignore"); >- cy.get("#item_action").should("have.value", "ignore"); >- >- cy.screenshot("right_before_submission"); > cy.get("#mainformsubmit").click(); > > cy.get("#job_callback").should("exist"); > >- //wait for View batch link to load with the batch ID >- cy.wait(5000); >- >- cy.screenshot("after_waiting"); > cy.contains("View batch").click(); > >- cy.wait(2000); > // Now verify all values are retained > cy.get("#new_matcher_id").should("have.value", "3"); > cy.get("#overlay_action").should("have.value", "create_new"); >diff --git a/t/cypress/integration/t/api-client.ts b/t/cypress/integration/t/api-client.ts >new file mode 100644 >index 0000000000..693328a61e >--- /dev/null >+++ b/t/cypress/integration/t/api-client.ts >@@ -0,0 +1,28 @@ >+const { APIClient } = require("./../../plugins/dist/api-client.cjs.js"); >+ >+describe("Using APIClient", () => { >+ let client = APIClient.default; >+ it("should 404 for non-existent biblio", () => { >+ try { >+ client.koha.get({ >+ endpoint: "/api/v1/public/biblios/99999", >+ return_response: true, >+ }); >+ } catch (error) { >+ expect(error.response.status).to.equal(404); >+ } >+ }); >+}); >+ >+describe("Using the api-client plugin", () => { >+ it("should 404 for non-existent biblio", () => { >+ try { >+ cy.task("apiGet", { >+ endpoint: "/api/v1/public/biblios/99999", >+ return_response: true, >+ }); >+ } catch (error) { >+ expect(error.response.status).to.equal(404); >+ } >+ }); >+}); >diff --git a/t/cypress/integration/t/commands.ts b/t/cypress/integration/t/commands.ts >new file mode 100644 >index 0000000000..1761941db1 >--- /dev/null >+++ b/t/cypress/integration/t/commands.ts >@@ -0,0 +1,27 @@ >+describe("visit", () => { >+ it("should visit staff", () => { >+ cy.visit("/"); >+ cy.title().should("eq", "Log in to Koha ⺠Koha"); >+ }); >+}); >+ >+describe("login", () => { >+ it("should log in at the staff interface", () => { >+ cy.login(); >+ cy.title().should("eq", "Koha staff interface"); >+ }); >+}); >+ >+describe("visitOpac", () => { >+ it("should visit OPAC", () => { >+ cy.visitOpac("/"); >+ cy.title().should("eq", "Koha online catalog"); >+ }); >+}); >+ >+describe("loginOpac", () => { >+ it("should log in at the OPAC interface", () => { >+ cy.loginOpac(); >+ cy.title().should("eq", "Your summary ⺠Koha online catalog"); >+ }); >+}); >diff --git a/t/cypress/integration/t/db.ts b/t/cypress/integration/t/db.ts >new file mode 100644 >index 0000000000..5715c8870f >--- /dev/null >+++ b/t/cypress/integration/t/db.ts >@@ -0,0 +1,15 @@ >+describe("DB tests", () => { >+ it("should be able to SELECT", () => { >+ cy.task("query", { sql: "SELECT count(*) FROM borrowers" }).then( >+ rows => { >+ expect(typeof rows.length).to.be.equal("number"); >+ } >+ ); >+ cy.task("query", { >+ sql: "SELECT count(*) FROM borrowers WHERE `surname` = ?", >+ values: ["john"], >+ }).then(rows => { >+ expect(typeof rows.length).to.be.equal("number"); >+ }); >+ }); >+}); >diff --git a/t/cypress/integration/t/insertData.ts b/t/cypress/integration/t/insertData.ts >new file mode 100644 >index 0000000000..aff5e3b6e2 >--- /dev/null >+++ b/t/cypress/integration/t/insertData.ts >@@ -0,0 +1,278 @@ >+const { query } = require("./../../plugins/db.js"); >+const { getBasicAuthHeader } = require("./../../plugins/auth.js"); >+ >+describe("insertData", () => { >+ let tablesToCheck = [ >+ "borrowers", >+ "branches", >+ "items", >+ "biblio", >+ "reserves", >+ "issues", >+ ]; >+ beforeEach(() => { >+ const counts = {}; >+ >+ const queries = tablesToCheck.map(table => { >+ return cy >+ .task("query", { >+ sql: `SELECT COUNT(*) as count FROM ${table}`, >+ }) >+ .then(result => { >+ counts[table] = result[0].count; >+ }); >+ }); >+ >+ cy.wrap(Promise.all(queries)).then(() => { >+ cy.wrap(counts).as("initialCounts"); >+ }); >+ }); >+ >+ describe("deleteSampleObjects", () => { >+ it("should delete everything from Object", () => { >+ cy.task("insertSampleBiblio", { item_count: 2 }).then(objects => { >+ cy.task("deleteSampleObjects", objects); >+ }); >+ }); >+ it("should delete everything from Array", () => { >+ cy.task("insertSampleBiblio", { item_count: 2 }).then(objects => { >+ cy.task("deleteSampleObjects", [ >+ { biblio: objects.biblio }, >+ { item_type: objects.item_type }, >+ { item: objects.items[0] }, >+ { item: objects.items[1] }, >+ { library: objects.libraries[0] }, >+ ]); >+ }); >+ }); >+ }); >+ >+ describe("insertSampleBiblio", () => { >+ it("should generate library and item type", () => { >+ cy.task("insertSampleBiblio", { item_count: 3 }).then(objects => { >+ expect(typeof objects.biblio.biblio_id).to.be.equal("number"); >+ expect(typeof objects.biblio.title).to.be.equal("string"); >+ expect(typeof objects.biblio.author).to.be.equal("string"); >+ >+ const biblio_id = objects.biblio.biblio_id; >+ >+ cy.task("query", { >+ sql: "SELECT COUNT(*) as count FROM biblio WHERE biblionumber=?", >+ values: [biblio_id], >+ }).then(result => { >+ expect(result[0].count).to.be.equal(1); >+ }); >+ >+ cy.task("query", { >+ sql: "SELECT COUNT(*) as count FROM items WHERE biblionumber=?", >+ values: [biblio_id], >+ }).then(result => { >+ expect(result[0].count).to.be.equal(3); >+ }); >+ >+ cy.task("query", { >+ sql: "SELECT DISTINCT(itype) as count FROM items WHERE biblionumber=?", >+ values: [biblio_id], >+ }).then(result => { >+ expect(result.length).to.be.equal(1); >+ }); >+ >+ cy.task("query", { >+ sql: "SELECT COUNT(*) as count FROM branches WHERE branchcode=?", >+ values: [objects.libraries[0].library_id], >+ }).then(result => { >+ expect(result[0].count).to.be.equal(1); >+ }); >+ >+ cy.task("query", { >+ sql: "SELECT COUNT(*) as count FROM itemtypes WHERE itemtype=?", >+ values: [objects.item_type.item_type_id], >+ }).then(result => { >+ expect(result[0].count).to.be.equal(1); >+ }); >+ >+ cy.task("deleteSampleObjects", objects); >+ >+ cy.task("query", { >+ sql: "SELECT COUNT(*) as count FROM biblio WHERE biblionumber=?", >+ values: [biblio_id], >+ }).then(result => { >+ expect(result[0].count).to.be.equal(0); >+ }); >+ >+ cy.task("query", { >+ sql: "SELECT COUNT(*) as count FROM items WHERE biblionumber=?", >+ values: [biblio_id], >+ }).then(result => { >+ expect(result[0].count).to.be.equal(0); >+ }); >+ >+ cy.task("query", { >+ sql: "SELECT COUNT(*) as count FROM branches WHERE branchcode=?", >+ values: [objects.libraries[0].library_id], >+ }).then(result => { >+ expect(result[0].count).to.be.equal(0); >+ }); >+ >+ cy.task("query", { >+ sql: "SELECT COUNT(*) as count FROM itemtypes WHERE itemtype=?", >+ values: [objects.item_type.item_type_id], >+ }).then(result => { >+ expect(result[0].count).to.be.equal(0); >+ }); >+ }); >+ }); >+ >+ it("insertSampleBiblio - options.different_libraries", () => { >+ cy.task("insertSampleBiblio", { >+ item_count: 3, >+ options: { different_libraries: true }, >+ }).then(objects => { >+ expect(objects.libraries.length).to.be.equal(3); >+ let libraries = objects.libraries >+ .map(library => library.library_id) >+ .sort(); >+ let itemsLibraries = objects.items >+ .map(item => item.home_library_id) >+ .sort(); >+ expect(libraries).deep.to.equal(itemsLibraries); >+ >+ cy.task("deleteSampleObjects", objects); >+ }); >+ }); >+ }); >+ >+ describe("insertSampleHold", () => { >+ it("insertSampleHold - item/biblio", () => { >+ cy.task("insertSampleBiblio", { item_count: 2 }).then( >+ objects_biblio => { >+ cy.task("insertSampleHold", { >+ item: objects_biblio.items[0], >+ }).then(objects_hold_1 => { >+ cy.task("insertSampleHold", { >+ biblio: objects_biblio.biblio, >+ library_id: objects_biblio.items[0].home_library_id, >+ }).then(objects_hold_2 => { >+ cy.task("apiGet", { >+ // No /holds/:hold_id (yet) >+ // No q={} (yet) >+ endpoint: `/api/v1/holds?hold_id=${objects_hold_1.hold.hold_id}`, >+ headers: { >+ "Content-Type": "application/json", >+ }, >+ }).then(holds => { >+ expect(holds.length).to.be.equal(1); >+ expect(holds[0].biblio_id).to.be.equal( >+ objects_biblio.biblio.biblio_id >+ ); >+ expect(holds[0].item_id).to.be.equal( >+ objects_biblio.items[0].item_id >+ ); >+ }); >+ cy.task("apiGet", { >+ // No /holds/:hold_id (yet) >+ // No q={} (yet) >+ endpoint: `/api/v1/holds?hold_id=${objects_hold_2.hold.hold_id}`, >+ headers: { >+ "Content-Type": "application/json", >+ }, >+ }).then(holds => { >+ expect(holds.length).to.be.equal(1); >+ expect(holds[0].biblio_id).to.be.equal( >+ objects_biblio.biblio.biblio_id >+ ); >+ expect(holds[0].item_id).to.be.equal(null); >+ }); >+ >+ cy.task("deleteSampleObjects", [ >+ objects_biblio, >+ objects_hold_1, >+ objects_hold_2, >+ ]); >+ }); >+ }); >+ } >+ ); >+ }); >+ >+ // How to properly test for Error? >+ it.skip("insertSampleHold - missing library_id", () => { >+ cy.task("insertSampleBiblio", { item_count: 2 }).then( >+ objects_biblio => { >+ cy.task("insertSampleHold", { >+ biblio: objects_biblio.biblio, >+ }).then( >+ () => { >+ throw new Error("Task should have failed"); >+ }, >+ err => { >+ expect(err.message).to.include( >+ "Could not generate sample hold without library_id or item" >+ ); >+ } >+ ); >+ } >+ ); >+ }); >+ }); >+ >+ describe("insertSampleCheckout", () => { >+ it("insertSampleCheckout - without parameter", () => { >+ cy.task("insertSampleCheckout").then(objects_checkout => { >+ cy.task("apiGet", { >+ endpoint: `/api/v1/checkouts/${objects_checkout.checkout.checkout_id}`, >+ }).then(checkout => { >+ expect(checkout.item_id).to.be.equal( >+ objects_checkout.items[0].item_id >+ ); >+ }); >+ >+ cy.task("deleteSampleObjects", objects_checkout); >+ }); >+ }); >+ >+ it("insertSampleCheckout - pass an already generated patron", () => { >+ cy.task("insertSamplePatron").then(objects_patron => { >+ cy.task("insertSampleCheckout", { >+ patron: objects_patron.patron, >+ }).then(objects_checkout => { >+ expect(objects_checkout.patron).to.not.exist; >+ cy.task("apiGet", { >+ endpoint: `/api/v1/checkouts/${objects_checkout.checkout.checkout_id}`, >+ }).then(checkout => { >+ expect(checkout.item_id).to.be.equal( >+ objects_checkout.items[0].item_id >+ ); >+ expect(checkout.patron_id).to.be.equal( >+ objects_patron.patron.patron_id >+ ); >+ }); >+ cy.task("deleteSampleObjects", [ >+ objects_checkout, >+ objects_patron, >+ ]); >+ }); >+ }); >+ }); >+ }); >+ >+ afterEach(function () { >+ cy.get("@initialCounts").then(initialCounts => { >+ const queries = tablesToCheck.map(table => { >+ return cy >+ .task("query", { >+ sql: `SELECT COUNT(*) as count FROM ${table}`, >+ }) >+ .then(result => { >+ const finalCount = result[0].count; >+ expect( >+ finalCount, >+ `Row count for ${table} should match` >+ ).to.eq(initialCounts[table]); >+ }); >+ }); >+ >+ return Promise.all(queries); >+ }); >+ }); >+}); >diff --git a/t/cypress/integration/t/mockData.ts b/t/cypress/integration/t/mockData.ts >index 8c63fb75f7..33b20279c6 100644 >--- a/t/cypress/integration/t/mockData.ts >+++ b/t/cypress/integration/t/mockData.ts >@@ -20,6 +20,16 @@ describe("Generate Random Patron", () => { > ); > }); > }); >+ >+ it("should not overwrite _id if passed", () => { >+ const home_library_id = "LIB4TEST"; >+ cy.task("buildSampleObject", { >+ object: "item", >+ values: { home_library_id }, >+ }).then(mockItem => { >+ expect(mockItem.home_library_id).to.equal(home_library_id); >+ }); >+ }); > }); > > describe("Generate Random Patrons", () => { >@@ -74,4 +84,14 @@ describe("Generate objects", () => { > ); > }); > }); >+ >+ it("should not overwrite _id if passed", () => { >+ const home_library_id = "LIB4TEST"; >+ cy.task("buildSampleObject", { >+ object: "item", >+ values: { home_library_id }, >+ }).then(mockItem => { >+ expect(mockItem.home_library_id).to.equal(home_library_id); >+ }); >+ }); > }); >diff --git a/t/cypress/plugins/api-client.js b/t/cypress/plugins/api-client.js >new file mode 100644 >index 0000000000..750b5d1a3d >--- /dev/null >+++ b/t/cypress/plugins/api-client.js >@@ -0,0 +1,177 @@ >+/** >+ * Koha API Client for Cypress Testing >+ * >+ * This module provides a wrapper around the Koha API client for use in Cypress tests. >+ * It handles authentication, request preparation, and provides convenient methods >+ * for making API calls during test execution. >+ * >+ * @module api-client >+ */ >+ >+const { APIClient } = require("./dist/api-client.cjs.js"); >+ >+const client = APIClient.default.koha; >+ >+/** >+ * Prepares request parameters for API calls by extracting and organizing headers and URL. >+ * >+ * @function prepareRequest >+ * @param {Object} params - Request parameters >+ * @param {string} params.baseUrl - Base URL for the API >+ * @param {string} params.endpoint - API endpoint path >+ * @param {string} [params.authHeader] - Authorization header value >+ * @param {Object} [params.headers={}] - Additional headers to include >+ * @param {...*} params.rest - Other parameters to pass through >+ * @returns {Object} Prepared request object >+ * @returns {string} returns.url - Complete URL for the request >+ * @returns {Object} returns.headers - Combined headers object >+ * @returns {Object} returns.rest - Pass-through parameters >+ * @private >+ */ >+const prepareRequest = params => { >+ const { baseUrl, endpoint, authHeader, headers = {}, ...rest } = params; >+ const url = baseUrl + endpoint; >+ const finalHeaders = { >+ ...headers, >+ ...(authHeader ? { Authorization: authHeader } : {}), >+ }; >+ return { url, headers: finalHeaders, rest }; >+}; >+ >+/** >+ * Performs a GET request to the Koha API. >+ * >+ * @function apiGet >+ * @param {Object} params - Request parameters >+ * @param {string} params.baseUrl - Base URL for the API >+ * @param {string} params.endpoint - API endpoint path >+ * @param {string} [params.authHeader] - Authorization header value >+ * @param {Object} [params.headers={}] - Additional headers to include >+ * @param {...*} params.rest - Additional parameters for the request >+ * @returns {Promise<*>} API response data >+ * @example >+ * // Get a list of patrons >+ * const patrons = await apiGet({ >+ * baseUrl: 'http://localhost:8081', >+ * endpoint: '/api/v1/patrons', >+ * authHeader: 'Basic dGVzdDp0ZXN0' >+ * }); >+ * >+ * @example >+ * // Get a specific patron with query parameters >+ * const patron = await apiGet({ >+ * baseUrl: 'http://localhost:8081', >+ * endpoint: '/api/v1/patrons?q={"patron_id":123}', >+ * authHeader: 'Basic dGVzdDp0ZXN0' >+ * }); >+ */ >+const apiGet = params => { >+ const { url, headers, rest } = prepareRequest(params); >+ return client.get({ >+ endpoint: url, >+ headers, >+ ...rest, >+ }); >+}; >+ >+/** >+ * Performs a POST request to the Koha API. >+ * >+ * @function apiPost >+ * @param {Object} params - Request parameters >+ * @param {string} params.baseUrl - Base URL for the API >+ * @param {string} params.endpoint - API endpoint path >+ * @param {string} [params.authHeader] - Authorization header value >+ * @param {Object} [params.headers={}] - Additional headers to include >+ * @param {Object} [params.body] - Request body data >+ * @param {...*} params.rest - Additional parameters for the request >+ * @returns {Promise<*>} API response data >+ * @example >+ * // Create a new patron >+ * const newPatron = await apiPost({ >+ * baseUrl: 'http://localhost:8081', >+ * endpoint: '/api/v1/patrons', >+ * authHeader: 'Basic dGVzdDp0ZXN0', >+ * body: { >+ * firstname: 'John', >+ * surname: 'Doe', >+ * library_id: 'CPL', >+ * category_id: 'PT' >+ * } >+ * }); >+ */ >+const apiPost = params => { >+ const { url, headers, rest } = prepareRequest(params); >+ return client.post({ >+ endpoint: url, >+ headers, >+ ...rest, >+ }); >+}; >+ >+/** >+ * Performs a PUT request to the Koha API. >+ * >+ * @function apiPut >+ * @param {Object} params - Request parameters >+ * @param {string} params.baseUrl - Base URL for the API >+ * @param {string} params.endpoint - API endpoint path >+ * @param {string} [params.authHeader] - Authorization header value >+ * @param {Object} [params.headers={}] - Additional headers to include >+ * @param {Object} [params.body] - Request body data >+ * @param {...*} params.rest - Additional parameters for the request >+ * @returns {Promise<*>} API response data >+ * @example >+ * // Update a patron >+ * const updatedPatron = await apiPut({ >+ * baseUrl: 'http://localhost:8081', >+ * endpoint: '/api/v1/patrons/123', >+ * authHeader: 'Basic dGVzdDp0ZXN0', >+ * body: { >+ * email: 'newemail@example.com' >+ * } >+ * }); >+ */ >+const apiPut = params => { >+ const { url, headers, rest } = prepareRequest(params); >+ return client.put({ >+ endpoint: url, >+ headers, >+ ...rest, >+ }); >+}; >+ >+/** >+ * Performs a DELETE request to the Koha API. >+ * >+ * @function apiDelete >+ * @param {Object} params - Request parameters >+ * @param {string} params.baseUrl - Base URL for the API >+ * @param {string} params.endpoint - API endpoint path >+ * @param {string} [params.authHeader] - Authorization header value >+ * @param {Object} [params.headers={}] - Additional headers to include >+ * @param {...*} params.rest - Additional parameters for the request >+ * @returns {Promise<*>} API response data >+ * @example >+ * // Delete a patron >+ * await apiDelete({ >+ * baseUrl: 'http://localhost:8081', >+ * endpoint: '/api/v1/patrons/123', >+ * authHeader: 'Basic dGVzdDp0ZXN0' >+ * }); >+ */ >+const apiDelete = params => { >+ const { url, headers, rest } = prepareRequest(params); >+ return client.delete({ >+ endpoint: url, >+ headers, >+ ...rest, >+ }); >+}; >+ >+module.exports = { >+ apiGet, >+ apiPost, >+ apiPut, >+ apiDelete, >+}; >diff --git a/t/cypress/plugins/auth.js b/t/cypress/plugins/auth.js >new file mode 100644 >index 0000000000..1f01366ee5 >--- /dev/null >+++ b/t/cypress/plugins/auth.js >@@ -0,0 +1,40 @@ >+/** >+ * Authentication utilities for Cypress testing >+ * >+ * This module provides authentication helper functions for use in Cypress tests >+ * when making API calls that require authentication. >+ * >+ * @module auth >+ */ >+ >+const { Buffer } = require("buffer"); >+ >+/** >+ * Generates a Basic Authentication header from username and password. >+ * >+ * @function getBasicAuthHeader >+ * @param {string} username - Username for authentication >+ * @param {string} password - Password for authentication >+ * @returns {string} Basic authentication header value in format "Basic <base64>" >+ * @example >+ * // Generate auth header for API calls >+ * const authHeader = getBasicAuthHeader('koha', 'koha'); >+ * // Returns: "Basic a29oYTprb2hh" >+ * >+ * // Use with API client >+ * const response = await apiGet({ >+ * baseUrl: 'http://localhost:8081', >+ * endpoint: '/api/v1/patrons', >+ * authHeader: getBasicAuthHeader('koha', 'koha') >+ * }); >+ */ >+const getBasicAuthHeader = (username, password) => { >+ const credentials = Buffer.from(`${username}:${password}`).toString( >+ "base64" >+ ); >+ return `Basic ${credentials}`; >+}; >+ >+module.exports = { >+ getBasicAuthHeader, >+}; >diff --git a/t/cypress/plugins/db.js b/t/cypress/plugins/db.js >new file mode 100644 >index 0000000000..92f5234d46 >--- /dev/null >+++ b/t/cypress/plugins/db.js >@@ -0,0 +1,66 @@ >+/** >+ * Database Query Utilities for Cypress Testing >+ * >+ * This module provides direct database access for Cypress tests when API >+ * endpoints are not available or when direct database operations are needed >+ * for test setup and cleanup. >+ * >+ * @module db >+ */ >+ >+const mysql = require("mysql2/promise"); >+ >+/** >+ * Database connection configuration >+ * >+ * @todo Replace hardcoded credentials with environment variables >+ * @type {Object} >+ */ >+const connectionConfig = { >+ host: "db", >+ user: "koha_kohadev", >+ password: "password", >+ database: "koha_kohadev", >+}; >+ >+/** >+ * Executes a SQL query with optional parameters. >+ * >+ * @async >+ * @function query >+ * @param {string} sql - SQL query string with optional parameter placeholders (?) >+ * @param {Array} [params=[]] - Array of parameter values for the query >+ * @returns {Promise<Array>} Query results as an array of rows >+ * @throws {Error} When database connection or query execution fails >+ * @description This function: >+ * - Creates a new database connection for each query >+ * - Uses parameterized queries to prevent SQL injection >+ * - Automatically closes the connection after execution >+ * - Returns the raw result rows from the database >+ * >+ * @example >+ * // Simple SELECT query >+ * const patrons = await query('SELECT * FROM borrowers LIMIT 10'); >+ * >+ * @example >+ * // Parameterized query for safety >+ * const patron = await query( >+ * 'SELECT * FROM borrowers WHERE borrowernumber = ?', >+ * [123] >+ * ); >+ * >+ * @example >+ * // DELETE query with multiple parameters >+ * await query( >+ * 'DELETE FROM issues WHERE issue_id IN (?, ?, ?)', >+ * [1, 2, 3] >+ * ); >+ */ >+async function query(sql, params = []) { >+ const connection = await mysql.createConnection(connectionConfig); >+ const [rows] = await connection.execute(sql, params); >+ await connection.end(); >+ return rows; >+} >+ >+module.exports = { query }; >diff --git a/t/cypress/plugins/index.js b/t/cypress/plugins/index.js >index 837e6d747c..5970890dc0 100644 >--- a/t/cypress/plugins/index.js >+++ b/t/cypress/plugins/index.js >@@ -1,21 +1,122 @@ >-const { startDevServer } = require("@cypress/webpack-dev-server"); >+/** >+ * Cypress Plugin Configuration >+ * >+ * This is the main Cypress plugin configuration file that registers all >+ * testing utilities as Cypress tasks. It provides a bridge between Cypress >+ * tests and the various utility modules for data generation, API access, >+ * and database operations. >+ * >+ * @module cypress-plugins >+ */ > >-const mysql = require("cypress-mysql"); >+const { startDevServer } = require("@cypress/webpack-dev-server"); > > const { buildSampleObject, buildSampleObjects } = require("./mockData.js"); > >+const { >+ insertSampleBiblio, >+ insertSampleHold, >+ insertSampleCheckout, >+ insertSamplePatron, >+ insertObject, >+ deleteSampleObjects, >+} = require("./insertData.js"); >+ >+const { getBasicAuthHeader } = require("./auth.js"); >+ >+const { query } = require("./db.js"); >+ >+const { apiGet, apiPost, apiPut, apiDelete } = require("./api-client.js"); >+ >+/** >+ * Cypress plugin configuration function. >+ * >+ * @function >+ * @param {Function} on - Cypress plugin registration function >+ * @param {Object} config - Cypress configuration object >+ * @param {string} config.baseUrl - Base URL for the application under test >+ * @param {Object} config.env - Environment variables from cypress.config.js >+ * @param {string} config.env.apiUsername - Username for API authentication >+ * @param {string} config.env.apiPassword - Password for API authentication >+ * @returns {Object} Modified Cypress configuration >+ * @description This function: >+ * - Registers all testing utilities as Cypress tasks >+ * - Sets up authentication headers for API calls >+ * - Configures the development server for component testing >+ * - Provides automatic parameter injection for common arguments >+ * >+ * Available Cypress tasks: >+ * - Data Generation: buildSampleObject, buildSampleObjects >+ * - Data Insertion: insertSampleBiblio, insertSampleHold, insertSampleCheckout, insertSamplePatron >+ * - Data Cleanup: deleteSampleObjects >+ * - API Access: apiGet, apiPost, apiPut, apiDelete >+ * - Database Access: query >+ * - Authentication: getBasicAuthHeader >+ * >+ * @example >+ * // Usage in Cypress tests >+ * cy.task('insertSampleBiblio', { item_count: 2 }).then(result => { >+ * // Test with the created biblio >+ * }); >+ * >+ * @example >+ * // API call through task >+ * cy.task('apiGet', { endpoint: '/api/v1/patrons' }).then(patrons => { >+ * // Work with patron data >+ * }); >+ */ > module.exports = (on, config) => { >+ const baseUrl = config.baseUrl; >+ const authHeader = getBasicAuthHeader( >+ config.env.apiUsername, >+ config.env.apiPassword >+ ); >+ > on("dev-server:start", options => > startDevServer({ > options, > }) > ); > >- mysql.configurePlugin(on); >- > on("task", { >+ getBasicAuthHeader() { >+ return getBasicAuthHeader( >+ config.env.apiUsername, >+ config.env.apiPassword >+ ); >+ }, > buildSampleObject, > buildSampleObjects, >+ insertSampleBiblio(args) { >+ return insertSampleBiblio({ ...args, baseUrl, authHeader }); >+ }, >+ insertSampleHold(args) { >+ return insertSampleHold({ ...args, baseUrl, authHeader }); >+ }, >+ insertSampleCheckout(args) { >+ return insertSampleCheckout({ ...args, baseUrl, authHeader }); >+ }, >+ insertSamplePatron(args) { >+ return insertSamplePatron({ ...args, baseUrl, authHeader }); >+ }, >+ insertObject(args) { >+ return insertObject({ ...args, baseUrl, authHeader }); >+ }, >+ deleteSampleObjects, >+ query, >+ >+ apiGet(args) { >+ return apiGet({ ...args, baseUrl, authHeader }); >+ }, >+ apiPost(args) { >+ return apiPost({ ...args, baseUrl, authHeader }); >+ }, >+ apiPut(args) { >+ return apiPut({ ...args, baseUrl, authHeader }); >+ }, >+ apiDelete(args) { >+ return apiDelete({ ...args, baseUrl, authHeader }); >+ }, > }); > return config; > }; >diff --git a/t/cypress/plugins/insertData.js b/t/cypress/plugins/insertData.js >new file mode 100644 >index 0000000000..3aa062611a >--- /dev/null >+++ b/t/cypress/plugins/insertData.js >@@ -0,0 +1,825 @@ >+/** >+ * Koha Cypress Testing Data Insertion Utilities >+ * >+ * This module provides functions to create and manage test data for Cypress tests. >+ * It handles creating complete bibliographic records, patrons, holds, checkouts, >+ * and other Koha objects with proper relationships and dependencies. >+ * >+ * @module insertData >+ */ >+ >+const { buildSampleObject, buildSampleObjects } = require("./mockData.js"); >+const { query } = require("./db.js"); >+ >+const { apiGet, apiPost } = require("./api-client.js"); >+ >+/** >+ * Creates a complete bibliographic record with associated items and libraries. >+ * >+ * @async >+ * @function insertSampleBiblio >+ * @param {Object} params - Configuration parameters >+ * @param {number} params.item_count - Number of items to create for this biblio >+ * @param {Object} [params.options] - Additional options >+ * @param {boolean} [params.options.different_libraries] - If true, creates different libraries for each item >+ * @param {string} params.baseUrl - Base URL for API calls >+ * @param {string} params.authHeader - Authorization header for API calls >+ * @returns {Promise<Object>} Created biblio with items, libraries, and item_type >+ * @returns {Object} returns.biblio - The created bibliographic record >+ * @returns {Array<Object>} returns.items - Array of created item records >+ * @returns {Array<Object>} returns.libraries - Array of created library records >+ * @returns {Object} returns.item_type - The created item type record >+ * @example >+ * // Create a biblio with 3 items using the same library >+ * const result = await insertSampleBiblio({ >+ * item_count: 3, >+ * baseUrl: 'http://localhost:8081', >+ * authHeader: 'Basic dGVzdDp0ZXN0' >+ * }); >+ * >+ * @example >+ * // Create a biblio with 2 items using different libraries >+ * const result = await insertSampleBiblio({ >+ * item_count: 2, >+ * options: { different_libraries: true }, >+ * baseUrl: 'http://localhost:8081', >+ * authHeader: 'Basic dGVzdDp0ZXN0' >+ * }); >+ */ >+const insertSampleBiblio = async ({ >+ item_count, >+ options, >+ baseUrl, >+ authHeader, >+}) => { >+ const generatedItemType = await buildSampleObject({ object: "item_type" }); >+ const item_type = await insertObject({ >+ type: "item_type", >+ object: generatedItemType, >+ baseUrl, >+ authHeader, >+ }); >+ >+ let title = "Some boring read"; >+ let author = "Some boring author"; >+ let biblio = { >+ leader: " nam a22 7a 4500", >+ fields: [ >+ { "005": "20250120101920.0" }, >+ { >+ 245: { >+ ind1: "", >+ ind2: "", >+ subfields: [{ a: title }], >+ }, >+ }, >+ { >+ 100: { >+ ind1: "", >+ ind2: "", >+ subfields: [{ c: author }], >+ }, >+ }, >+ { >+ 942: { >+ ind1: "", >+ ind2: "", >+ subfields: [{ c: item_type.item_type_id }], >+ }, >+ }, >+ ], >+ }; >+ let result = await apiPost({ >+ endpoint: "/api/v1/biblios", >+ headers: { >+ "Content-Type": "application/marc-in-json", >+ "x-confirm-not-duplicate": 1, >+ }, >+ body: biblio, >+ baseUrl, >+ authHeader, >+ }); >+ const biblio_id = result.id; >+ // We do not have a route to get a biblio as it is stored in DB >+ // We might need to refine that in the future >+ biblio = { >+ biblio_id, >+ title, >+ author, >+ }; >+ >+ let items = buildSampleObjects({ >+ object: "item", >+ count: item_count, >+ values: { >+ biblio_id, >+ lost_status: 0, >+ withdrawn: 0, >+ damaged_status: 0, >+ not_for_loan_status: 0, >+ restricted_status: 0, >+ new_status: null, >+ issues: 0, >+ checked_out_date: null, >+ item_type_id: item_type.item_type_id, >+ }, >+ }); >+ items = items.map( >+ ({ >+ item_id, >+ checkout, >+ transfer, >+ lost_date, >+ withdrawn_date, >+ damaged_date, >+ course_item, >+ _strings, >+ biblio, >+ bundle_host, >+ item_group_item, >+ recall, >+ return_claim, >+ return_claims, >+ serial_item, >+ first_hold, >+ checkouts_count, >+ renewals_count, >+ holds_count, >+ bundle_items_lost_count, >+ analytics_count, >+ effective_not_for_loan_status, >+ effective_item_type_id, >+ home_library, >+ holding_library, >+ bundle_items_not_lost_count, >+ item_type, >+ _status, >+ effective_bookable, >+ in_bundle, >+ cover_image_ids, >+ localuse, >+ ...rest >+ }) => rest >+ ); >+ let createdItems = []; >+ let libraries = []; >+ let commonLibrary; >+ if (!options || !options.different_libraries) { >+ const generatedLibrary = await buildSampleObject({ object: "library" }); >+ commonLibrary = await insertObject({ >+ type: "library", >+ object: generatedLibrary, >+ baseUrl, >+ authHeader, >+ }); >+ libraries.push(commonLibrary); >+ } >+ for (const item of items) { >+ if (options?.different_libraries) { >+ const generatedLibrary = await buildSampleObject({ >+ object: "library", >+ }); >+ const library = await insertObject({ >+ type: "library", >+ object: generatedLibrary, >+ baseUrl, >+ authHeader, >+ }); >+ libraries.push(library); >+ item.home_library_id = library.library_id; >+ item.holding_library_id = library.library_id; >+ } else { >+ item.home_library_id = commonLibrary.library_id; >+ item.holding_library_id = commonLibrary.library_id; >+ } >+ >+ await apiPost({ >+ endpoint: `/api/v1/biblios/${biblio_id}/items`, >+ body: item, >+ baseUrl, >+ authHeader, >+ }).then(i => createdItems.push(i)); >+ } >+ return { biblio, items: createdItems, libraries, item_type }; >+}; >+ >+/** >+ * Creates a hold request for a bibliographic record or item. >+ * >+ * @async >+ * @function insertSampleHold >+ * @param {Object} params - Configuration parameters >+ * @param {Object} [params.item] - Item to place hold on (optional if biblio provided) >+ * @param {Object} [params.biblio] - Biblio to place hold on (optional if item provided) >+ * @param {string} [params.library_id] - Library ID for pickup location (defaults to item's home library) >+ * @param {string} params.baseUrl - Base URL for API calls >+ * @param {string} params.authHeader - Authorization header for API calls >+ * @returns {Promise<Object>} Created hold with associated patron and patron_category >+ * @returns {Object} returns.hold - The created hold record >+ * @returns {Object} returns.patron - The patron who placed the hold >+ * @returns {Object} returns.patron_category - The patron's category >+ * @throws {Error} When neither library_id nor item is provided >+ * @example >+ * // Create a hold on a specific item >+ * const holdResult = await insertSampleHold({ >+ * item: { item_id: 123, home_library_id: 'CPL' }, >+ * baseUrl: 'http://localhost:8081', >+ * authHeader: 'Basic dGVzdDp0ZXN0' >+ * }); >+ * >+ * @example >+ * // Create a biblio-level hold >+ * const holdResult = await insertSampleHold({ >+ * biblio: { biblio_id: 456 }, >+ * library_id: 'CPL', >+ * baseUrl: 'http://localhost:8081', >+ * authHeader: 'Basic dGVzdDp0ZXN0' >+ * }); >+ */ >+const insertSampleHold = async ({ >+ item, >+ biblio, >+ library_id, >+ baseUrl, >+ authHeader, >+}) => { >+ library_id ||= item?.home_library_id; >+ >+ if (!library_id) { >+ throw new Error( >+ "Could not generate sample hold without library_id or item" >+ ); >+ } >+ >+ const { patron, patron_category } = await insertSamplePatron({ >+ library: { library_id }, >+ baseUrl, >+ authHeader, >+ }); >+ >+ const generatedHold = buildSampleObject({ >+ object: "hold", >+ values: { >+ patron_id: patron.patron_id, >+ biblio_id: item?.biblio_id || biblio.biblio_id, >+ pickup_library_id: library_id, >+ item_id: item?.item_id || null, >+ }, >+ }); >+ const hold = await insertObject({ >+ type: "hold", >+ object: generatedHold, >+ baseUrl, >+ authHeader, >+ }); >+ return { hold, patron, patron_category }; >+}; >+ >+/** >+ * Creates a checkout record with associated biblio, item, and optional patron. >+ * >+ * @async >+ * @function insertSampleCheckout >+ * @param {Object} params - Configuration parameters >+ * @param {Object} [params.patron] - Existing patron to check out to (creates new if not provided) >+ * @param {string} params.baseUrl - Base URL for API calls >+ * @param {string} params.authHeader - Authorization header for API calls >+ * @returns {Promise<Object>} Created checkout with all associated records >+ * @returns {Object} returns.biblio - The bibliographic record >+ * @returns {Array<Object>} returns.items - Array of item records >+ * @returns {Array<Object>} returns.libraries - Array of library records >+ * @returns {Object} returns.item_type - The item type record >+ * @returns {Object} returns.checkout - The checkout record >+ * @returns {Object} [returns.patron] - The patron record (if generated) >+ * @returns {Object} [returns.patron_category] - The patron category (if generated) >+ * @example >+ * // Create a checkout with a new patron >+ * const checkoutResult = await insertSampleCheckout({ >+ * baseUrl: 'http://localhost:8081', >+ * authHeader: 'Basic dGVzdDp0ZXN0' >+ * }); >+ * >+ * @example >+ * // Create a checkout for an existing patron >+ * const checkoutResult = await insertSampleCheckout({ >+ * patron: { patron_id: 123 }, >+ * baseUrl: 'http://localhost:8081', >+ * authHeader: 'Basic dGVzdDp0ZXN0' >+ * }); >+ */ >+const insertSampleCheckout = async ({ patron, baseUrl, authHeader }) => { >+ const { biblio, items, libraries, item_type } = await insertSampleBiblio({ >+ item_count: 1, >+ baseUrl, >+ authHeader, >+ }); >+ >+ let generatedPatron; >+ let patronCategory; >+ if (!patron) { >+ generatedPatron = true; >+ const patron_objects = await insertSamplePatron({ >+ library: { library_id: libraries[0].library_id }, >+ baseUrl, >+ authHeader, >+ }); >+ generatedCategory = patron_objects.category; >+ patron = patron_objects.patron; >+ } >+ >+ const generatedCheckout = buildSampleObject({ >+ object: "checkout", >+ values: { >+ patron_id: patron.patron_id, >+ item_id: items[0].item_id, >+ }, >+ }); >+ delete generatedCheckout.external_id; >+ const checkout = await insertObject({ >+ type: "checkout", >+ object: generatedCheckout, >+ baseUrl, >+ authHeader, >+ }); >+ return { >+ biblio, >+ items, >+ libraries, >+ item_type, >+ checkout, >+ ...(generatedPatron >+ ? { >+ patron, >+ patron_category: generatedCategory, >+ } >+ : {}), >+ }; >+}; >+ >+/** >+ * Creates a patron record with associated library and category. >+ * >+ * @async >+ * @function insertSamplePatron >+ * @param {Object} params - Configuration parameters >+ * @param {Object} [params.library] - Library to assign patron to (creates new if not provided) >+ * @param {Object} [params.patron_category] - Patron category to assign (creates new if not provided) >+ * @param {string} params.baseUrl - Base URL for API calls >+ * @param {string} params.authHeader - Authorization header for API calls >+ * @returns {Promise<Object>} Created patron with associated records >+ * @returns {Object} returns.patron - The created patron record >+ * @returns {Object} [returns.library] - The library record (if generated) >+ * @returns {Object} [returns.patron_category] - The patron category record (if generated) >+ * @example >+ * // Create a patron with new library and category >+ * const patronResult = await insertSamplePatron({ >+ * baseUrl: 'http://localhost:8081', >+ * authHeader: 'Basic dGVzdDp0ZXN0' >+ * }); >+ * >+ * @example >+ * // Create a patron for an existing library >+ * const patronResult = await insertSamplePatron({ >+ * library: { library_id: 'CPL' }, >+ * baseUrl: 'http://localhost:8081', >+ * authHeader: 'Basic dGVzdDp0ZXN0' >+ * }); >+ */ >+const insertSamplePatron = async ({ >+ library, >+ patron_category, >+ baseUrl, >+ authHeader, >+}) => { >+ let generatedLibrary; >+ let generatedCategory; >+ if (!library) { >+ generatedLibrary = await buildSampleObject({ object: "library" }); >+ library = await insertLibrary({ >+ library: generatedLibrary, >+ baseUrl, >+ authHeader, >+ }); >+ } >+ if (!patron_category) { >+ generatedCategory = await buildSampleObject({ >+ object: "patron_category", >+ }); >+ query({ >+ sql: "INSERT INTO categories(categorycode, description) VALUES (?, ?)", >+ values: [ >+ generatedCategory.patron_category_id, >+ `description for ${generatedCategory.patron_category_id}`, >+ ], >+ }); >+ // FIXME We need /patron_categories/:patron_category_id >+ await apiGet({ >+ endpoint: `/api/v1/patron_categories?q={"me.patron_category_id":"${generatedCategory.patron_category_id}"}`, >+ baseUrl, >+ authHeader, >+ }).then(categories => (patron_category = categories[0])); >+ } >+ >+ let generatedPatron = await buildSampleObject({ >+ object: "patron", >+ values: { >+ library_id: library.library_id, >+ category_id: patron_category.patron_category_id, >+ incorrect_address: null, >+ patron_card_lost: null, >+ }, >+ }); >+ >+ let { >+ patron_id, >+ _strings, >+ anonymized, >+ restricted, >+ expired, >+ extended_attributes, >+ checkouts_count, >+ overdues_count, >+ account_balance, >+ lang, >+ login_attempts, >+ sms_provider_id, >+ ...patron >+ } = generatedPatron; >+ delete patron.library; >+ >+ patron = await apiPost({ >+ endpoint: `/api/v1/patrons`, >+ body: patron, >+ baseUrl, >+ authHeader, >+ }); >+ >+ return { >+ patron, >+ ...(generatedLibrary ? { library } : {}), >+ ...(generatedCategory ? { patron_category } : {}), >+ }; >+}; >+ >+/** >+ * Deletes test objects from the database in the correct order to respect foreign key constraints. >+ * >+ * @async >+ * @function deleteSampleObjects >+ * @param {Object|Array<Object>} allObjects - Object(s) to delete, can be single object or array >+ * @returns {Promise<boolean>} True if deletion was successful >+ * @description This function handles cleanup of test data by: >+ * - Accepting single objects or arrays of objects >+ * - Grouping objects by type (holds, checkouts, patrons, items, etc.) >+ * - Deleting in dependency order to avoid foreign key violations >+ * - Supporting all major Koha object types >+ * @example >+ * // Delete a single test result >+ * await deleteSampleObjects(checkoutResult); >+ * >+ * @example >+ * // Delete multiple test results >+ * await deleteSampleObjects([biblioResult, holdResult, checkoutResult]); >+ * >+ * @example >+ * // Delete after creating test data >+ * const biblio = await insertSampleBiblio({ item_count: 2, baseUrl, authHeader }); >+ * const hold = await insertSampleHold({ item: biblio.items[0], baseUrl, authHeader }); >+ * // ... run tests ... >+ * await deleteSampleObjects([biblio, hold]); >+ */ >+const deleteSampleObjects = async allObjects => { >+ if (!Array.isArray(allObjects)) { >+ allObjects = [allObjects]; >+ } >+ >+ const pluralMap = { >+ hold: "holds", >+ checkout: "checkouts", >+ old_checkout: "old_checkouts", >+ basket: "baskets", >+ vendor: "vendors", >+ patron: "patrons", >+ item: "items", >+ biblio: "biblios", >+ library: "libraries", >+ item_type: "item_types", >+ }; >+ // Merge by type >+ const mergedObjects = {}; >+ for (const objects of allObjects) { >+ for (const [type, value] of Object.entries(objects)) { >+ let plural = pluralMap?.[type] || type; >+ if (!mergedObjects[plural]) { >+ mergedObjects[plural] = []; >+ } >+ >+ if (Array.isArray(value)) { >+ mergedObjects[plural].push(...value); >+ } else { >+ mergedObjects[plural].push(value); >+ } >+ } >+ } >+ >+ const deletionOrder = [ >+ "holds", >+ "checkouts", >+ "old_checkouts", >+ "baskets", >+ "vendors", >+ "patrons", >+ "items", >+ "biblios", >+ "libraries", >+ "item_types", >+ ]; >+ >+ for (const type of deletionOrder) { >+ if (!mergedObjects[type] || mergedObjects[type].length === 0) { >+ continue; >+ } >+ >+ const objects = mergedObjects[type]; >+ let ids = []; >+ switch (type) { >+ case "biblios": >+ ids = objects.map(i => i.biblio_id); >+ await query({ >+ sql: `DELETE FROM biblio WHERE biblionumber IN (${ids.map(() => "?").join(",")})`, >+ values: ids, >+ }); >+ break; >+ case "items": >+ ids = objects.map(i => i.item_id); >+ await query({ >+ sql: `DELETE FROM items WHERE itemnumber IN (${ids.map(() => "?").join(",")})`, >+ values: ids, >+ }); >+ break; >+ case "libraries": >+ ids = objects.map(i => i.library_id); >+ await query({ >+ sql: `DELETE FROM branches WHERE branchcode IN (${ids.map(() => "?").join(",")})`, >+ values: ids, >+ }); >+ break; >+ case "holds": >+ ids = objects.map(i => i.hold_id); >+ await query({ >+ sql: `DELETE FROM reserves WHERE reserve_id IN (${ids.map(() => "?").join(",")})`, >+ values: ids, >+ }); >+ break; >+ case "checkouts": >+ ids = objects.map(i => i.checkout_id); >+ await query({ >+ sql: `DELETE FROM issues WHERE issue_id IN (${ids.map(() => "?").join(",")})`, >+ values: ids, >+ }); >+ break; >+ case "old_checkouts": >+ ids = objects.map(i => i.checkout_id); >+ await query({ >+ sql: `DELETE FROM old_issues WHERE issue_id IN (${ids.map(() => "?").join(",")})`, >+ values: ids, >+ }); >+ break; >+ case "item_types": >+ ids = objects.map(i => i.item_type_id); >+ await query({ >+ sql: `DELETE FROM itemtypes WHERE itemtype IN (${ids.map(() => "?").join(",")})`, >+ values: ids, >+ }); >+ break; >+ case "patrons": >+ ids = objects.map(i => i.patron_id); >+ await query({ >+ sql: `DELETE FROM borrowers WHERE borrowernumber IN (${ids.map(() => "?").join(",")})`, >+ values: ids, >+ }); >+ break; >+ case "baskets": >+ ids = objects.map(i => i.basket_id); >+ await query({ >+ sql: `DELETE FROM aqbasket WHERE basketno IN (${ids.map(() => "?").join(",")})`, >+ values: ids, >+ }); >+ break; >+ case "vendors": >+ ids = objects.map(i => i.id); >+ await query({ >+ sql: `DELETE FROM aqbooksellers WHERE id IN (${ids.map(() => "?").join(",")})`, >+ values: ids, >+ }); >+ break; >+ default: >+ throw Error( >+ `Not implemented yet: cannot deleted object '${type}'` >+ ); >+ } >+ } >+ return true; >+}; >+ >+/** >+ * Creates a library record via API, filtering out unsupported fields. >+ * >+ * @async >+ * @function insertLibrary >+ * @param {Object} params - Configuration parameters >+ * @param {Object} params.library - Library object to insert >+ * @param {string} params.baseUrl - Base URL for API calls >+ * @param {string} params.authHeader - Authorization header for API calls >+ * @returns {Promise<Object>} Created library record >+ * @private >+ * @description This is a helper function that removes fields not supported by the API >+ * before creating the library record. >+ */ >+const insertLibrary = async ({ library, baseUrl, authHeader }) => { >+ const { >+ pickup_items, >+ smtp_server, >+ cash_registers, >+ desks, >+ library_hours, >+ needs_override, >+ ...new_library >+ } = library; >+ return apiPost({ >+ endpoint: "/api/v1/libraries", >+ body: new_library, >+ baseUrl, >+ authHeader, >+ }); >+}; >+ >+/** >+ * Generic function to insert various types of Koha objects. >+ * >+ * @async >+ * @function insertObject >+ * @param {Object} params - Configuration parameters >+ * @param {string} params.type - Type of object to insert ('library', 'item_type', 'hold', 'checkout', 'vendor', 'basket') >+ * @param {Object} params.object - Object data to insert >+ * @param {string} params.baseUrl - Base URL for API calls >+ * @param {string} params.authHeader - Authorization header for API calls >+ * @returns {Promise<Object|boolean>} Created object or true if successful >+ * @throws {Error} When object type is not supported >+ * @private >+ * @description This is a generic helper function that handles the specifics of creating >+ * different types of Koha objects. Each object type may require different field filtering, >+ * API endpoints, or database operations. >+ * >+ * Supported object types: >+ * - library: Creates library via API >+ * - item_type: Creates item type via database query >+ * - hold: Creates hold via API >+ * - checkout: Creates checkout via API with confirmation token support >+ * - vendor: Creates vendor via API >+ * - basket: Creates basket via database query >+ */ >+const insertObject = async ({ type, object, baseUrl, authHeader }) => { >+ if (type == "library") { >+ const keysToKeep = ["library_id", "name"]; >+ const library = Object.fromEntries( >+ Object.entries(object).filter(([key]) => keysToKeep.includes(key)) >+ ); >+ return apiPost({ >+ endpoint: "/api/v1/libraries", >+ body: library, >+ baseUrl, >+ authHeader, >+ }); >+ } else if (type == "item_type") { >+ const keysToKeep = ["item_type_id", "description"]; >+ const item_type = Object.fromEntries( >+ Object.entries(object).filter(([key]) => keysToKeep.includes(key)) >+ ); >+ return query({ >+ sql: "INSERT INTO itemtypes(itemtype, description) VALUES (?, ?)", >+ values: [item_type.item_type_id, item_type.description], >+ }) >+ .then(result => { >+ // FIXME We need /item_types/:item_type_id >+ return apiGet({ >+ endpoint: `/api/v1/item_types?q={"item_type_id":"${item_type.item_type_id}"}`, >+ baseUrl, >+ authHeader, >+ }); >+ }) >+ .then(item_types => item_types[0]); >+ } else if (type == "hold") { >+ const { >+ hold_id, >+ deleted_biblio_id, >+ item_group_id, >+ desk_id, >+ cancellation_date, >+ cancellation_reason, >+ notes, >+ priority, >+ status, >+ timestamp, >+ waiting_date, >+ expiration_date, >+ lowest_priority, >+ suspended, >+ suspended_until, >+ non_priority, >+ item_type, >+ item_level, >+ cancellation_requested, >+ biblio, >+ deleted_biblio, >+ item, >+ pickup_library, >+ hold_date, >+ ...hold >+ } = object; >+ >+ return apiPost({ >+ endpoint: `/api/v1/holds`, >+ body: hold, >+ baseUrl, >+ authHeader, >+ }); >+ } else if (type == "checkout") { >+ const { issuer, patron, ...checkout } = object; >+ >+ let endpoint = "/api/v1/checkouts"; >+ // Force the checkout - we might need a parameter to control this behaviour later >+ await apiGet({ >+ endpoint: `/api/v1/checkouts/availability?item_id=${object.item_id}&patron_id=${object.patron_id}`, >+ baseUrl, >+ authHeader, >+ }).then(result => { >+ if (result.confirmation_token) { >+ endpoint += `?confirmation=${result.confirmation_token}`; >+ } >+ }); >+ >+ return apiPost({ >+ endpoint, >+ body: checkout, >+ baseUrl, >+ authHeader, >+ }); >+ } else if (type == "vendor") { >+ const { >+ id, >+ baskets_count, >+ invoices_count, >+ subscriptions_count, >+ external_id, >+ aliases, >+ baskets, >+ contacts, >+ contracts, >+ interfaces, >+ invoices, >+ ...vendor >+ } = object; >+ >+ let endpoint = "/api/v1/acquisitions/vendors"; >+ >+ return apiPost({ >+ endpoint, >+ body: vendor, >+ baseUrl, >+ authHeader, >+ }); >+ } else if (type == "basket") { >+ const keysToKeep = ["name", "vendor_id", "close_date"]; >+ const basket = Object.fromEntries( >+ Object.entries(object).filter(([key]) => keysToKeep.includes(key)) >+ ); >+ return query({ >+ sql: "INSERT INTO aqbasket(basketname, booksellerid, closedate) VALUES (?, ?, ?)", >+ values: [basket.name, basket.vendor_id, basket.close_date], >+ }) >+ .then(result => { >+ const basket_id = result.insertId; >+ // FIXME We need /acquisitions/baskets/:basket_id >+ return apiGet({ >+ endpoint: `/api/v1/acquisitions/baskets?q={"basket_id":"${basket_id}"}`, >+ baseUrl, >+ authHeader, >+ }); >+ }) >+ .then(baskets => baskets[0]); >+ } else { >+ throw Error(`Unsupported object type '${type}' to insert`); >+ } >+ >+ return true; >+}; >+ >+module.exports = { >+ insertSampleBiblio, >+ insertSampleHold, >+ insertSampleCheckout, >+ insertSamplePatron, >+ insertObject, >+ deleteSampleObjects, >+}; >diff --git a/t/cypress/plugins/mockData.js b/t/cypress/plugins/mockData.js >index 0f9c292df8..c7de7e052e 100644 >--- a/t/cypress/plugins/mockData.js >+++ b/t/cypress/plugins/mockData.js >@@ -1,21 +1,77 @@ >+/** >+ * Mock Data Generation for Cypress Testing >+ * >+ * This module provides functions to generate realistic test data for Koha objects >+ * based on OpenAPI schema definitions. It uses Faker.js to generate random data >+ * that conforms to the API specifications. >+ * >+ * @module mockData >+ */ >+ > const { faker } = require("@faker-js/faker"); > const { readYamlFile } = require("./../plugins/readYamlFile.js"); >+const { query } = require("./db.js"); > const fs = require("fs"); > >+/** >+ * Cache to store generated ID values to prevent duplicates >+ * @type {Set<string>} >+ */ >+const generatedDataCache = new Set(); >+ >+/** >+ * Generates mock data for a specific data type based on OpenAPI schema properties. >+ * >+ * @function generateMockData >+ * @param {string} type - The data type (string, integer, boolean, array, number, date, date-time) >+ * @param {Object} properties - OpenAPI schema properties for the field >+ * @param {Array} [properties.enum] - Enumerated values to choose from >+ * @param {number} [properties.maxLength] - Maximum length for strings >+ * @param {number} [properties.minLength] - Minimum length for strings >+ * @returns {*} Generated mock data appropriate for the type >+ * @private >+ * @example >+ * // Generate a string with max length 50 >+ * const name = generateMockData('string', { maxLength: 50 }); >+ * >+ * // Generate from enum values >+ * const status = generateMockData('string', { enum: ['active', 'inactive'] }); >+ */ > const generateMockData = (type, properties) => { >+ if (properties.hasOwnProperty("enum")) { >+ let values = properties.enum; >+ return values[Math.floor(Math.random() * values.length)]; >+ } >+ > switch (type) { > case "string": > if (properties?.maxLength) { >- return faker.string.alpha({ >+ // The propability to have a string with length=1 is the same as length=10 >+ // We have very limited pool of possible values for length=1 which will result in a "Duplicate ID" error from the server >+ // Setting minLength to 3 to prevent this kind of failures >+ let minLength = >+ properties.minLength === 1 || >+ properties.minLength === undefined >+ ? 3 >+ : properties.minLength; >+ >+ if ( >+ properties.maxLength !== undefined && >+ properties.maxLength < minLength >+ ) { >+ minLength = properties.maxLength; >+ } >+ return (value = faker.string.alpha({ > length: { >- min: properties.minLength || 1, >+ min: minLength, > max: properties.maxLength, > }, >- }); >+ })); > } >- return faker.lorem.words(3); >+ return (value = faker.lorem.words(3)); > case "integer": >- return faker.number.int(); >+ // Do not return more than int(11); >+ return faker.number.int(2 ** 31 - 1); > case "boolean": > return faker.datatype.boolean(); > case "array": >@@ -31,6 +87,26 @@ const generateMockData = (type, properties) => { > } > }; > >+/** >+ * Generates mock data for an entire object based on OpenAPI schema properties. >+ * >+ * @function generateDataFromSchema >+ * @param {Object} properties - OpenAPI schema properties object >+ * @param {Object} [values={}] - Override values for specific fields >+ * @returns {Object} Generated mock object with all required fields >+ * @private >+ * @description This function: >+ * - Iterates through all properties in the schema >+ * - Generates appropriate mock data for each field >+ * - Handles object relationships (libraries, items, etc.) >+ * - Ensures unique values for ID fields >+ * - Applies any override values provided >+ * >+ * Special handling for object relationships: >+ * - home_library/holding_library -> generates library object >+ * - item_type -> generates item_type object >+ * - Automatically sets corresponding _id fields >+ */ > const generateDataFromSchema = (properties, values = {}) => { > const mockData = {}; > const ids = {}; >@@ -56,25 +132,68 @@ const generateDataFromSchema = (properties, values = {}) => { > data = buildSampleObject({ object: "library" }); > fk_name = "library_id"; > break; >+ case "pickup_library": >+ data = buildSampleObject({ object: "library" }); >+ fk_name = "pickup_library_id"; >+ break; >+ case "library": >+ data = buildSampleObject({ object: "library" }); >+ fk_name = "library_id"; >+ break; > case "item_type": > data = buildSampleObject({ object: "item_type" }); > fk_name = "item_type_id"; > break; >+ case "item": >+ data = buildSampleObject({ object: "item" }); >+ fk_name = "item_id"; >+ break; > default: >- data = generateMockData(type, value); >+ try { >+ data = generateMockData(type, value); >+ } catch (e) { >+ throw new Error( >+ `Failed to generate data for (${key}): ${e}` >+ ); >+ } > } > if (typeof data === "object") { > ids[key] = data[fk_name]; > } > } else { >- data = generateMockData(type, value); >+ try { >+ if (key.match(/_id$/)) { >+ let attempts = 0; >+ >+ do { >+ data = generateMockData(type, value); >+ attempts++; >+ if (attempts > 10) { >+ throw new Error( >+ "Could not generate unique string after 10 attempts" >+ ); >+ } >+ } while (generatedDataCache.has(data)); >+ >+ generatedDataCache.add(data); >+ } else { >+ data = generateMockData(type, value); >+ } >+ } catch (e) { >+ throw new Error( >+ `Failed to generate data for ${key} (${type}): ${e}` >+ ); >+ } > } > mockData[key] = data; > } > }); > > Object.keys(ids).forEach(k => { >- if (mockData.hasOwnProperty(k + "_id")) { >+ if ( >+ mockData.hasOwnProperty(k + "_id") && >+ !values.hasOwnProperty(k + "_id") >+ ) { > mockData[k + "_id"] = ids[k]; > } > }); >@@ -82,6 +201,37 @@ const generateDataFromSchema = (properties, values = {}) => { > return mockData; > }; > >+/** >+ * Builds an array of sample objects based on OpenAPI schema definitions. >+ * >+ * @function buildSampleObjects >+ * @param {Object} params - Configuration parameters >+ * @param {string} params.object - Object type to generate (must match YAML file name) >+ * @param {Object} [params.values] - Override values for specific fields >+ * @param {number} [params.count=1] - Number of objects to generate >+ * @returns {Array<Object>} Array of generated objects >+ * @throws {Error} When object type is not supported or generation fails >+ * @description This function: >+ * - Reads the OpenAPI schema from api/v1/swagger/definitions/{object}.yaml >+ * - Generates the specified number of objects >+ * - Applies any override values to all generated objects >+ * - Ensures all objects conform to the API schema >+ * >+ * @example >+ * // Generate 3 patron objects >+ * const patrons = buildSampleObjects({ >+ * object: 'patron', >+ * count: 3 >+ * }); >+ * >+ * @example >+ * // Generate 2 items with specific library >+ * const items = buildSampleObjects({ >+ * object: 'item', >+ * values: { library_id: 'CPL' }, >+ * count: 2 >+ * }); >+ */ > const buildSampleObjects = ({ object, values, count = 1 }) => { > const yamlPath = `api/v1/swagger/definitions/${object}.yaml`; > if (!fs.existsSync(yamlPath)) { >@@ -90,11 +240,54 @@ const buildSampleObjects = ({ object, values, count = 1 }) => { > ); > } > const schema = readYamlFile(yamlPath); >- return Array.from({ length: count }, () => >- generateDataFromSchema(schema.properties, values) >- ); >+ let generatedObject; >+ try { >+ generatedObject = Array.from({ length: count }, () => >+ generateDataFromSchema(schema.properties, values) >+ ); >+ } catch (e) { >+ throw new Error(`Failed to generate data for object '${object}': ${e}`); >+ } >+ return generatedObject; > }; > >+/** >+ * Builds a single sample object based on OpenAPI schema definitions. >+ * >+ * @function buildSampleObject >+ * @param {Object} params - Configuration parameters >+ * @param {string} params.object - Object type to generate (must match YAML file name) >+ * @param {Object} [params.values={}] - Override values for specific fields >+ * @returns {Object} Generated object conforming to API schema >+ * @throws {Error} When object type is not supported or generation fails >+ * @description This is a convenience function that generates a single object >+ * by calling buildSampleObjects with count=1 and returning the first result. >+ * >+ * Supported object types include: >+ * - patron: Library patron/borrower >+ * - item: Bibliographic item >+ * - biblio: Bibliographic record >+ * - library: Library/branch >+ * - hold: Hold/reservation request >+ * - checkout: Circulation checkout >+ * - vendor: Acquisitions vendor >+ * - basket: Acquisitions basket >+ * - And others as defined in api/v1/swagger/definitions/ >+ * >+ * @example >+ * // Generate a single patron >+ * const patron = buildSampleObject({ object: 'patron' }); >+ * >+ * @example >+ * // Generate an item with specific values >+ * const item = buildSampleObject({ >+ * object: 'item', >+ * values: { >+ * barcode: '12345678', >+ * home_library_id: 'CPL' >+ * } >+ * }); >+ */ > const buildSampleObject = ({ object, values = {} }) => { > return buildSampleObjects({ object, values })[0]; > }; >diff --git a/t/cypress/plugins/readYamlFile.js b/t/cypress/plugins/readYamlFile.js >index 369650c44d..321904c875 100644 >--- a/t/cypress/plugins/readYamlFile.js >+++ b/t/cypress/plugins/readYamlFile.js >@@ -1,7 +1,38 @@ >+/** >+ * YAML File Reading Utilities for Cypress Testing >+ * >+ * This module provides utilities for reading and parsing YAML files, >+ * primarily used for loading OpenAPI schema definitions during test >+ * data generation. >+ * >+ * @module readYamlFile >+ */ >+ > const path = require("path"); > const fs = require("fs"); > const yaml = require("yaml"); > >+/** >+ * Reads and parses a YAML file. >+ * >+ * @function readYamlFile >+ * @param {string} filePath - Path to the YAML file (relative or absolute) >+ * @returns {Object} Parsed YAML content as a JavaScript object >+ * @throws {Error} When file doesn't exist or YAML parsing fails >+ * @description This function: >+ * - Resolves the file path to an absolute path >+ * - Checks if the file exists before attempting to read >+ * - Reads the file content as UTF-8 text >+ * - Parses the YAML content into a JavaScript object >+ * >+ * @example >+ * // Read an OpenAPI schema definition >+ * const patronSchema = readYamlFile('api/v1/swagger/definitions/patron.yaml'); >+ * >+ * @example >+ * // Read a configuration file >+ * const config = readYamlFile('./config/test-config.yaml'); >+ */ > const readYamlFile = filePath => { > const absolutePath = path.resolve(filePath); > if (!fs.existsSync(absolutePath)) { >diff --git a/t/cypress/support/e2e.js b/t/cypress/support/e2e.js >index e420889c3b..bcffe74d50 100644 >--- a/t/cypress/support/e2e.js >+++ b/t/cypress/support/e2e.js >@@ -32,6 +32,10 @@ function get_fallback_login_value(param) { > : Cypress.env(env_var); > } > >+Cypress.Commands.add("visitOpac", path => { >+ cy.visit(Cypress.env("opacBaseUrl") + path); >+}); >+ > Cypress.Commands.add("login", (username, password) => { > var user = > typeof username === "undefined" >@@ -47,6 +51,21 @@ Cypress.Commands.add("login", (username, password) => { > cy.get("#submit-button").click(); > }); > >+Cypress.Commands.add("loginOpac", (username, password) => { >+ var user = >+ typeof username === "undefined" >+ ? get_fallback_login_value("username") >+ : username; >+ var pass = >+ typeof password === "undefined" >+ ? get_fallback_login_value("password") >+ : password; >+ cy.visitOpac("/cgi-bin/koha/opac-main.pl?logout.x=1"); >+ cy.get("#userid").type(user); >+ cy.get("#password").type(pass); >+ cy.get("#auth .action").contains("Log in").click(); >+}); >+ > Cypress.Commands.add("left_menu_active_item_is", label => { > cy.get(".sidebar_menu a.current:not(.disabled)") > .should("have.length", 1) >@@ -1874,9 +1893,6 @@ cy.getVendor = () => { > }; > }; > >-const mysql = require("cypress-mysql"); >-mysql.addCommands(); >- > Cypress.Commands.add("set_syspref", (variable, value) => { > cy.window().then(win => { > const client = win.APIClient.sysprefs; >@@ -1919,3 +1935,15 @@ Cypress.Commands.add("mock_table_settings", (settings, table_settings_var) => { > cy.wrap(table_settings.columns).as("columns"); > }); > }); >+ >+before(() => { >+ cy.task("query", { >+ sql: "SELECT value FROM systempreferences WHERE variable='RESTBasicAuth'", >+ }).then(value => { >+ if (value[0].value !== "1") { >+ throw new Error( >+ "Cypress tests tests require 'RESTBasicAuth'. Skipping suite." >+ ); >+ } >+ }); >+}); >diff --git a/t/db_dependent/Auth.t b/t/db_dependent/Auth.t >index a2c0a77da7..502838b91c 100755 >--- a/t/db_dependent/Auth.t >+++ b/t/db_dependent/Auth.t >@@ -7,7 +7,8 @@ use CGI qw ( -utf8 ); > use Test::MockObject; > use Test::MockModule; > use List::MoreUtils qw/all any none/; >-use Test::More tests => 23; >+use Test::More tests => 24; >+use Test::NoWarnings; > use Test::Warn; > use t::lib::Mocks; > use t::lib::TestBuilder; >diff --git a/t/db_dependent/Auth_with_shibboleth.t b/t/db_dependent/Auth_with_shibboleth.t >index db6ce0b1cb..b7cfdbbfb2 100755 >--- a/t/db_dependent/Auth_with_shibboleth.t >+++ b/t/db_dependent/Auth_with_shibboleth.t >@@ -20,9 +20,10 @@ > use Modern::Perl; > use utf8; > >-use Test::More tests => 6; >+use Test::More tests => 7; > use Test::MockModule; > use Test::Warn; >+use Test::NoWarnings; > use CGI qw(-utf8 ); > use File::Temp qw(tempdir); > >@@ -67,17 +68,18 @@ $context->mock( 'interface', sub { return $interface; } ); > # Mock Letters: GetPreparedLetter, EnqueueLetter and SendQueuedMessages > # We want to test the params > my $mocked_letters = Test::MockModule->new('C4::Letters'); >+my $sub_called = {}; > $mocked_letters->mock( > 'GetPreparedLetter', > sub { >- warn "GetPreparedLetter called"; >+ $sub_called->{GetPreparedLetter}++; > return 1; > } > ); > $mocked_letters->mock( > 'EnqueueLetter', > sub { >- warn "EnqueueLetter called"; >+ $sub_called->{EnqueueLetter}++; > > # return a 'message_id' > return 42; >@@ -87,7 +89,7 @@ $mocked_letters->mock( > 'SendQueuedMessages', > sub { > my $params = shift; >- warn "SendQueuedMessages called with message_id: $params->{message_id}"; >+ $sub_called->{SendQueuedMessages}->{ $params->{message_id} }++; > return 1; > } > ); >@@ -172,7 +174,7 @@ subtest "get_login_shib tests" => sub { > > subtest "checkpw_shib tests" => sub { > >- plan tests => 54; >+ plan tests => 56; > > # Test borrower data > my $test_borrowers = [ >@@ -251,18 +253,13 @@ subtest "checkpw_shib tests" => sub { > $ENV{'emailpro'} = 'me@myemail.com'; > $ENV{branchcode} = $library->branchcode; # needed since T::D::C does no longer hides the FK constraint > >- warnings_are { >- ( $retval, $retcard, $retuserid, $retpatron ) = checkpw_shib($shib_login); >- } >- [ >- 'GetPreparedLetter called', >- 'EnqueueLetter called', >- 'SendQueuedMessages called with message_id: 42' >- ], >- "WELCOME notice Prepared, Enqueued and Send"; >- is( $retval, "1", "user authenticated" ); >- is( $retuserid, "test4321", "expected userid returned" ); >- is( ref($retpatron), 'Koha::Patron', "expected Koha::Patron object returned" ); >+ ( $retval, $retcard, $retuserid, $retpatron ) = checkpw_shib($shib_login); >+ is( $sub_called->{GetPreparedLetter}, 1, 'GetPreparedLetter called' ); >+ is( $sub_called->{EnqueueLetter}, 1, 'EnqueueLetter called' ); >+ is( $sub_called->{SendQueuedMessages}->{42}, 1, 'SendQueuedMessages called with message_id: 42' ); >+ is( $retval, "1", "user authenticated" ); >+ is( $retuserid, "test4321", "expected userid returned" ); >+ is( ref($retpatron), 'Koha::Patron', "expected Koha::Patron object returned" ); > $logger->debug_is( "koha borrower field to match: userid", "borrower match field debug info" ) > ->debug_is( "shibboleth attribute to match: uid", "shib match attribute debug info" )->clear(); > >diff --git a/t/db_dependent/AuthorisedValues.t b/t/db_dependent/AuthorisedValues.t >index cf07bfc001..559cd2ec74 100755 >--- a/t/db_dependent/AuthorisedValues.t >+++ b/t/db_dependent/AuthorisedValues.t >@@ -194,8 +194,8 @@ subtest 'search_by_*_field + find_by_koha_field + get_description + authorised_v > kohafield => 'items.restricted' > } > )->store; >- Koha::MarcSubfieldStructure->new( { tagfield => '003', frameworkcode => '', authorised_value => 'CONTROL_TEST', } ) >- ->store; >+ Koha::MarcSubfieldStructure->new( >+ { tagfield => '003', tagsubfield => '@', frameworkcode => '', authorised_value => 'CONTROL_TEST', } )->store; > Koha::AuthorisedValue->new( { category => 'TEST', authorised_value => 'location_1', lib => 'location_1' } )->store; > Koha::AuthorisedValue->new( { category => 'TEST', authorised_value => 'location_2', lib => 'location_2' } )->store; > Koha::AuthorisedValue->new( { category => 'TEST', authorised_value => 'location_3', lib => 'location_3' } )->store; >diff --git a/t/db_dependent/Biblio.t b/t/db_dependent/Biblio.t >index 1f08417b93..e34c00e6da 100755 >--- a/t/db_dependent/Biblio.t >+++ b/t/db_dependent/Biblio.t >@@ -17,7 +17,8 @@ > > use Modern::Perl; > >-use Test::More tests => 25; >+use Test::More tests => 26; >+use Test::NoWarnings; > use Test::MockModule; > use Test::Warn; > use List::MoreUtils qw( uniq ); >@@ -293,11 +294,12 @@ subtest "Test caching of authority types in LinkBibHeadingsToAuthorities" => sub > } > ); > my $authorities_type = Test::MockModule->new('Koha::Authority::Types'); >+ my $found_auth_type = {}; > $authorities_type->mock( > 'find', > sub { > my ( $self, $params ) = @_; >- warn "Finding auth type $params"; >+ $found_auth_type->{$params}++; > return $authorities_type->original("find")->( $self, $params ); > } > ); >@@ -305,17 +307,13 @@ subtest "Test caching of authority types in LinkBibHeadingsToAuthorities" => sub > my $field1 = MARC::Field->new( 655, ' ', ' ', 'a' => 'Magical realism' ); > my $field2 = MARC::Field->new( 655, ' ', ' ', 'a' => 'Magical falsism' ); > $marc_record->append_fields( ( $field1, $field2 ) ); >- my ( $num_changed, $results ); >- warning_like { ( $num_changed, $results ) = LinkBibHeadingsToAuthorities( $linker, $marc_record, "", undef ) } >- qr/Finding auth type GENRE\/FORM/, >- "Type fetched only once"; >+ my ( $num_changed, $results ) = LinkBibHeadingsToAuthorities( $linker, $marc_record, "", undef ); >+ is_deeply( $found_auth_type, { "GENRE/FORM" => 1 }, "Type fetched only once" ); > my $gf_type = $cache->get_from_cache("LinkBibHeadingsToAuthorities:AuthorityType:GENRE/FORM"); > ok( $gf_type, "GENRE/FORM type is found in cache" ); > >- warning_like { ( $num_changed, $results ) = LinkBibHeadingsToAuthorities( $linker, $marc_record, "", undef ) } >- undef, >- "Type not fetched a second time"; >- >+ ( $num_changed, $results ) = LinkBibHeadingsToAuthorities( $linker, $marc_record, "", undef ); >+ is_deeply( $found_auth_type, { "GENRE/FORM" => 1 }, "Type not fetched a second time" ); > }; > > # Mocking variables >diff --git a/t/db_dependent/Circulation/OfflineCirculation.t b/t/db_dependent/Circulation/OfflineCirculation.t >index ac74899d7b..8d9afffd2d 100755 >--- a/t/db_dependent/Circulation/OfflineCirculation.t >+++ b/t/db_dependent/Circulation/OfflineCirculation.t >@@ -18,7 +18,7 @@ > use Modern::Perl; > > use Test::NoWarnings; >-use Test::More tests => 3; >+use Test::More tests => 4; > use Test::MockModule; > use Test::Warn; > >@@ -85,18 +85,21 @@ subtest "Bug 34529: Offline circulation should be able to accept userid as well > } > ); > >+ my ( $message, $checkout ) = ProcessOfflineIssue( >+ { >+ cardnumber => $borrower1->{cardnumber}, >+ barcode => $item1->barcode >+ } >+ ); >+ > is( >- ProcessOfflineIssue( >- { >- cardnumber => $borrower1->{cardnumber}, >- barcode => $item1->barcode >- } >- ), >- "Success.", >+ $message, "Success.", > "ProcessOfflineIssue succeeds with cardnumber" > ); >+ >+ ( $message, $checkout ) = ProcessOfflineIssue( { cardnumber => $borrower1->{userid}, barcode => $item2->barcode } ); > is( >- ProcessOfflineIssue( { cardnumber => $borrower1->{userid}, barcode => $item2->barcode } ), >+ $message, > "Success.", > "ProcessOfflineIssue succeeds with user id" > ); >@@ -192,9 +195,48 @@ subtest "Bug 30114 - Koha offline circulation will always cancel the next hold w > > my $op = GetOfflineOperation( $offline_rs->next->id ); > >- my $ret = ProcessOfflineOperation($op); >+ my ($ret) = ProcessOfflineOperation($op); > > is( Koha::Holds->search( { biblionumber => $biblionumber } )->count, 2, "Still found two holds for the record" ); > }; > >+subtest "Bug 32934: ProcessOfflineIssue returns checkout object for SIP no block due date" => sub { >+ plan tests => 4; >+ >+ $branch = $builder->build( { source => 'Branch' } )->{branchcode}; >+ $manager_id = $builder->build( { source => 'Borrower' } )->{borrowernumber}; >+ >+ my $borrower = $builder->build( >+ { >+ source => 'Borrower', >+ value => { branchcode => $branch } >+ } >+ ); >+ >+ my $biblio = $builder->build_sample_biblio; >+ my $item = $builder->build_sample_item( >+ { >+ biblionumber => $biblio->id, >+ library => $branch, >+ } >+ ); >+ >+ my $due_date = dt_from_string->add( days => 7 )->ymd; >+ >+ # Test ProcessOfflineIssue returns both message and checkout object >+ my ( $message, $checkout ) = ProcessOfflineIssue( >+ { >+ cardnumber => $borrower->{cardnumber}, >+ barcode => $item->barcode, >+ due_date => $due_date, >+ timestamp => dt_from_string >+ } >+ ); >+ >+ is( $message, "Success.", "ProcessOfflineIssue returns success message" ); >+ isa_ok( $checkout, 'Koha::Checkout', "ProcessOfflineIssue returns checkout object" ); >+ is( $checkout->borrowernumber, $borrower->{borrowernumber}, "Checkout has correct borrower" ); >+ is( dt_from_string( $checkout->date_due )->ymd, $due_date, "Checkout respects specified due_date" ); >+}; >+ > $schema->storage->txn_rollback; >diff --git a/t/db_dependent/Circulation_holdsqueue.t b/t/db_dependent/Circulation_holdsqueue.t >index 5674fa11aa..33e2a2f791 100755 >--- a/t/db_dependent/Circulation_holdsqueue.t >+++ b/t/db_dependent/Circulation_holdsqueue.t >@@ -17,7 +17,8 @@ > > use Modern::Perl; > >-use Test::More tests => 1; >+use Test::More tests => 2; >+use Test::NoWarnings; > use Test::MockModule; > > use C4::Circulation qw( AddIssue AddReturn ); >diff --git a/t/db_dependent/Holds.t b/t/db_dependent/Holds.t >index 5157b1121e..65b735676f 100755 >--- a/t/db_dependent/Holds.t >+++ b/t/db_dependent/Holds.t >@@ -7,7 +7,8 @@ use t::lib::TestBuilder; > > use C4::Context; > >-use Test::More tests => 72; >+use Test::More tests => 73; >+use Test::NoWarnings; > use Test::Exception; > > use MARC::Record; >@@ -1653,7 +1654,7 @@ subtest 'non priority holds' => sub { > itemnumber => $item->itemnumber, > branchcode => $item->homebranch > } >- )->store; >+ )->store->get_from_storage; > > my $hid = AddReserve( > { >diff --git a/t/db_dependent/HoldsQueue/TransportCostOptimizations.t b/t/db_dependent/HoldsQueue/TransportCostOptimizations.t >index b11b3ba073..933ab22e3f 100755 >--- a/t/db_dependent/HoldsQueue/TransportCostOptimizations.t >+++ b/t/db_dependent/HoldsQueue/TransportCostOptimizations.t >@@ -9,7 +9,9 @@ > > use Modern::Perl; > >-use Test::More tests => 117; >+use Test::More tests => 120; >+use Test::NoWarnings; >+use Test::Warn; > use Data::Dumper; > > use C4::Context; >@@ -214,23 +216,32 @@ sub test_allocation { > > $schema->txn_rollback; > } >- >-test_allocation( >- "trivial case", >- [], >- [], >- [], >- [], >- 0 >+warning_like( >+ sub { >+ test_allocation( >+ "trivial case", >+ [], >+ [], >+ [], >+ [], >+ 0 >+ ); >+ }, >+ qr{UseTransportCostMatrix set to yes, but matrix not populated} > ); > >-test_allocation( >- "unit case", >- [ [0] ], >- [1], >- [1], >- [ [ 0, 0 ] ], >- 0 >+warning_like( >+ sub { >+ test_allocation( >+ "unit case", >+ [ [0] ], >+ [1], >+ [1], >+ [ [ 0, 0 ] ], >+ 0 >+ ); >+ }, >+ qr{UseTransportCostMatrix set to yes, but matrix not populated} > ); > > test_allocation( >diff --git a/t/db_dependent/Items/AutomaticItemModificationByAge.t b/t/db_dependent/Items/AutomaticItemModificationByAge.t >index 79dff9425b..1978257599 100755 >--- a/t/db_dependent/Items/AutomaticItemModificationByAge.t >+++ b/t/db_dependent/Items/AutomaticItemModificationByAge.t >@@ -2,7 +2,7 @@ > > use Modern::Perl; > use Test::NoWarnings; >-use Test::More tests => 21; >+use Test::More tests => 23; > use MARC::Record; > use MARC::Field; > use DateTime; >@@ -391,6 +391,110 @@ C4::Items::ToggleNewStatus( { rules => \@rules } ); > $modified_item = Koha::Items->find($itemnumber); > is( $modified_item->new_status, 'new_updated_value_biblio', q|ToggleNewStatus: conditions on biblio| ); > >+# Test for error handling in ToggleNewStatus with an on-loan item >+subtest "ToggleNewStatus onloan error handling" => sub { >+ plan tests => 3; >+ >+ # Create a new test item >+ my $test_item2 = $builder->build_object( { class => 'Koha::Items' } ); >+ my $patron2 = $builder->build_object( { class => 'Koha::Patrons' } ); >+ >+ # Check out the item to create the condition for an error >+ $test_item2->checkout( $patron2->borrowernumber ); >+ ok( $test_item2->onloan, "Item is checked out" ); >+ >+ # Create rules to try to modify the withdrawn status >+ my @withdrawal_rules = ( >+ { >+ conditions => [ >+ { >+ field => "items.itemnumber", >+ value => $test_item2->itemnumber >+ } >+ ], >+ substitutions => [ >+ { >+ field => "items.withdrawn", >+ value => 1 >+ } >+ ] >+ } >+ ); >+ >+ # Run ToggleNewStatus with the rules and catch errors in the report >+ my $error_report = C4::Items::ToggleNewStatus( >+ { >+ rules => \@withdrawal_rules, >+ report_only => 0 >+ } >+ ); >+ >+ # Verify report structure >+ ok( exists $error_report->{ $test_item2->itemnumber }, "Error item appears in report" ); >+ >+ is( $test_item2->withdrawn, 0, 'Item should not be withdrawn' ); >+ >+}; >+ >+subtest "ToggleNewStatus in-transit error handling" => sub { >+ plan tests => 3; >+ >+ # Create a new test item >+ my $test_item3 = $builder->build_object( { class => 'Koha::Items' } ); >+ >+ # Create a transfer to put item in-transit >+ my $from_library = $builder->build_object( { class => 'Koha::Libraries' } ); >+ my $to_library = $builder->build_object( { class => 'Koha::Libraries' } ); >+ >+ my $transfer = Koha::Item::Transfer->new( >+ { >+ itemnumber => $test_item3->itemnumber, >+ frombranch => $from_library->branchcode, >+ tobranch => $to_library->branchcode, >+ datesent => dt_from_string(), >+ } >+ )->store; >+ >+ # Mark transfer as in transit >+ $transfer->datearrived(undef); >+ $transfer->store; >+ >+ # Verify item is in transit >+ my $item_transfer = $test_item3->get_transfer; >+ ok( $item_transfer && !$item_transfer->datearrived, "Item is in transit" ); >+ >+ # Create rules to try to modify the withdrawn status >+ my @withdrawal_rules = ( >+ { >+ conditions => [ >+ { >+ field => "items.itemnumber", >+ value => $test_item3->itemnumber >+ } >+ ], >+ substitutions => [ >+ { >+ field => "items.withdrawn", >+ value => 1 >+ } >+ ] >+ } >+ ); >+ >+ # Run ToggleNewStatus with the rules and catch errors in the report >+ my $error_report = C4::Items::ToggleNewStatus( >+ { >+ rules => \@withdrawal_rules, >+ report_only => 0 >+ } >+ ); >+ >+ # Verify report structure >+ ok( exists $error_report->{ $test_item3->itemnumber }, "Error item appears in report" ); >+ >+ is( $test_item3->withdrawn, 0, 'Item should not be withdrawn' ); >+}; >+ > # Run twice > t::lib::Mocks::mock_preference( 'CataloguingLog', 1 ); > my $actions_nb = $schema->resultset('ActionLog')->count(); >diff --git a/t/db_dependent/Koha.t b/t/db_dependent/Koha.t >index 986a8b354c..a73738c940 100755 >--- a/t/db_dependent/Koha.t >+++ b/t/db_dependent/Koha.t >@@ -11,6 +11,7 @@ use Test::Warn; > use Test::Deep; > > use t::lib::TestBuilder; >+use t::lib::Mocks; > > use C4::Context; > use Koha::Database; >@@ -33,7 +34,7 @@ my $dbh = C4::Context->dbh; > our $itype_1 = $builder->build( { source => 'Itemtype' } ); > > subtest 'Authorized Values Tests' => sub { >- plan tests => 4; >+ plan tests => 5; > > my $data = { > category => 'CATEGORY', >@@ -166,6 +167,9 @@ subtest 'Authorized Values Tests' => sub { > > warning_is { GetAuthorisedValues() } [], 'No warning when no parameter passed to GetAuthorisedValues'; > >+ C4::Context->set_userenv(); >+ warning_is { GetAuthorisedValues("BUG10656") } [], 'No warning when userenv is anonymous'; >+ > }; > > subtest 'isbn tests' => sub { >diff --git a/t/db_dependent/Koha/Biblio.t b/t/db_dependent/Koha/Biblio.t >index 446180240b..f0d32ee300 100755 >--- a/t/db_dependent/Koha/Biblio.t >+++ b/t/db_dependent/Koha/Biblio.t >@@ -1016,46 +1016,99 @@ subtest 'get_volumes_query' => sub { > }; > > subtest 'generate_marc_host_field' => sub { >- plan tests => 24; >+ plan tests => 36; > > $schema->storage->txn_begin; > >+ # Set up MARC21 tests > t::lib::Mocks::mock_preference( 'marcflavour', 'MARC21' ); > >- my $biblio = $builder->build_sample_biblio(); >- my $record = $biblio->metadata->record; >+ # 1. Complete MARC21 record test >+ my $record = MARC::Record->new(); >+ $record->leader('00000nam a22000007a 4500'); > $record->append_fields( >- MARC::Field->new( '001', '1234' ), >- MARC::Field->new( '003', 'FIRST' ), >- MARC::Field->new( '240', '', '', a => 'A uniform title' ), >- MARC::Field->new( '260', '', '', a => 'Publication 260' ), >- MARC::Field->new( '250', '', '', a => 'Edition a', b => 'Edition b' ), >- MARC::Field->new( '022', '', '', a => '0317-8471' ), >+ MARC::Field->new( '001', '12345' ), >+ MARC::Field->new( '003', 'NB' ), >+ MARC::Field->new( '020', '', '', 'a' => '978-3-16-148410-0' ), >+ MARC::Field->new( '022', '', '', 'a' => '1234-5678' ), >+ MARC::Field->new( '100', '1', '', 'a' => 'Smith, John', 'e' => 'author', '9' => 'xyz', '4' => 'aut' ), >+ MARC::Field->new( '245', '1', '0', 'a' => 'The Title', 'b' => 'Subtitle', 'c' => 'John Smith' ), >+ MARC::Field->new( '250', '', '', 'a' => '2nd edition', 'b' => 'revised' ), >+ MARC::Field->new( '260', '', '', 'a' => 'New York', 'b' => 'Publisher', 'c' => '2023' ), >+ MARC::Field->new( '830', '', '', 'a' => 'Series Title', 'v' => 'vol. 2', 'x' => '2345-6789' ) > ); >- C4::Biblio::ModBiblio( $record, $biblio->biblionumber ); >- $biblio = Koha::Biblios->find( $biblio->biblionumber ); >+ my ($biblio_id) = AddBiblio( $record, qw{} ); >+ my $biblio = Koha::Biblios->find($biblio_id); > >- t::lib::Mocks::mock_preference( 'UseControlNumber', '0' ); >+ # Test MARC21 with UseControlNumber off >+ t::lib::Mocks::mock_preference( 'UseControlNumber', 0 ); > my $link = $biblio->generate_marc_host_field(); > >- is( ref($link), 'MARC::Field', "->generate_marc_host_field returns a MARC::Field object" ); >- is( $link->tag, '773', "MARC::Field->tag returns '773' when marcflavour is 'MARC21" ); >- is( $link->subfield('a'), 'Some boring author', 'MARC::Field->subfield(a) returns content from 100ab' ); >- is( $link->subfield('b'), 'Edition a Edition b', 'MARC::Field->subfield(b) returns content from 250ab' ); >- is( $link->subfield('d'), 'Publication 260', 'MARC::Field->subfield(c) returns content from 260abc' ); >- is( $link->subfield('s'), 'A uniform title', 'MARC::Field->subfield(s) returns content from 240a' ); >- is( $link->subfield('t'), 'Some boring read', 'MARC::Field->subfield(s) returns content from 245ab' ); >- is( $link->subfield('x'), '0317-8471', 'MARC::Field->subfield(s) returns content from 022a' ); >- is( $link->subfield('z'), undef, 'MARC::Field->subfield(s) returns undef when 020a is empty' ); >- is( $link->subfield('w'), undef, 'MARC::Field->subfield(w) returns undef when "UseControlNumber" is disabled' ); >+ # Test standard MARC21 field >+ is( ref($link), 'MARC::Field', 'Returns a MARC::Field object' ); >+ is( $link->tag(), '773', 'Field tag is 773 for MARC21' ); >+ is( $link->indicator(1), '0', 'First indicator is 0' ); >+ is( $link->indicator(2), ' ', 'Second indicator is blank' ); > >+ # Check all subfields >+ is( $link->subfield('7'), 'p1am', 'Subfield 7 correctly formed' ); >+ is( $link->subfield('a'), 'Smith, John', 'Subfield a contains author from 100a' ); >+ is( >+ $link->subfield('t'), 'The Title Subtitle', >+ 'Subfield t contains title without trailing punctuation from 245ab' >+ ); >+ is( $link->subfield('b'), '2nd edition revised', 'Subfield b contains edition info from 250ab' ); >+ is( $link->subfield('d'), 'New York Publisher 2023', 'Subfield d contains publication info from 260abc' ); >+ is( $link->subfield('k'), 'Series Title, ISSN 2345-6789 ; vol. 2', 'Subfield k contains series info from 830' ); >+ is( $link->subfield('x'), '1234-5678', 'Subfield x contains ISSN from 022a' ); >+ is( $link->subfield('z'), '978-3-16-148410-0', 'Subfield z contains ISBN from 020a' ); >+ is( $link->subfield('w'), undef, 'Subfield w is undefined when UseControlNumber is disabled' ); >+ >+ # Test with UseControlNumber enabled > t::lib::Mocks::mock_preference( 'UseControlNumber', '1' ); > $link = $biblio->generate_marc_host_field(); > is( >- $link->subfield('w'), '(FIRST)1234', >- 'MARC::Field->subfield(w) returns content from 003 and 001 when "UseControlNumber" is enabled' >+ $link->subfield('w'), '(NB)12345', >+ 'Subfield w contains control number with source when UseControlNumber is enabled' > ); > >+ # 245 punctuation handling tests >+ # Trailing slash >+ $record->field('245')->update( a => 'A title /', b => '', c => '', 'ind2' => '0' ); >+ ($biblio_id) = AddBiblio( $record, qw{} ); >+ $biblio = Koha::Biblios->find($biblio_id); >+ $link = $biblio->generate_marc_host_field(); >+ is( $link->subfield('t'), 'A title', "Trailing slash is removed from 245a" ); >+ >+ # Trailing period >+ $record->field('245')->update( a => 'Another title.', 'ind2' => '0' ); >+ ($biblio_id) = AddBiblio( $record, qw{} ); >+ $biblio = Koha::Biblios->find($biblio_id); >+ $link = $biblio->generate_marc_host_field(); >+ is( $link->subfield('t'), 'Another title', "Trailing period is removed from 245a" ); >+ >+ # Offset from indicator 2 = 4 >+ $record->field('245')->update( a => 'The offset title', 'ind2' => '4' ); >+ ($biblio_id) = AddBiblio( $record, qw{} ); >+ $biblio = Koha::Biblios->find($biblio_id); >+ $link = $biblio->generate_marc_host_field(); >+ is( $link->subfield('t'), 'Offset title', "Title offset applied from indicator 2" ); >+ >+ # Capitalization after offset >+ $record->field('245')->update( a => 'the capital test', 'ind2' => '0' ); >+ ($biblio_id) = AddBiblio( $record, qw{} ); >+ $biblio = Koha::Biblios->find($biblio_id); >+ $link = $biblio->generate_marc_host_field(); >+ is( $link->subfield('t'), 'The capital test', "Title is capitalized after indicator offset" ); >+ >+ # 240 uniform title tests >+ $record->append_fields( MARC::Field->new( '240', '1', '0', 'a' => 'Bible. English', 'l' => 'English' ) ); >+ ($biblio_id) = AddBiblio( $record, qw{} ); >+ $biblio = Koha::Biblios->find($biblio_id); >+ $link = $biblio->generate_marc_host_field(); >+ is( $link->subfield('s'), 'Bible. English', "Subfield s contains uniform title from 240a" ); >+ >+ # 260/264 handling tests > $record->append_fields( > MARC::Field->new( '264', '', '', a => 'Publication 264' ), > ); >@@ -1080,36 +1133,96 @@ subtest 'generate_marc_host_field' => sub { > 'MARC::Field->subfield(d) returns content from 264 with indicator 1 = 3 in preference to 264 without' > ); > >- # UNIMARC tests >- t::lib::Mocks::mock_preference( 'marcflavour', 'UNIMARC' ); >+ # 2. Test MARC21 with corporate author (110) >+ my $record_corporate = MARC::Record->new(); >+ $record_corporate->leader('00000nam a22000007a 4500'); >+ $record_corporate->append_fields( >+ MARC::Field->new( '110', '2', '', 'a' => 'Corporate Author', 'e' => 'sponsor', '9' => 'xyz', '4' => 'spn' ), >+ MARC::Field->new( '245', '1', '0', 'a' => 'The Title' ) >+ ); >+ ($biblio_id) = AddBiblio( $record_corporate, qw{} ); >+ $biblio = Koha::Biblios->find($biblio_id); > >- $biblio = $builder->build_sample_biblio(); >- $record = $biblio->metadata->record; >- $record->append_fields( >- MARC::Field->new( '001', '1234' ), >- MARC::Field->new( '700', '', '', a => 'A nice author' ), >- MARC::Field->new( '210', '', '', a => 'A publication', d => 'A date' ), >- MARC::Field->new( '205', '', '', a => "Fun things" ), >- MARC::Field->new( '856', '', '', u => 'http://myurl.com/' ), >- MARC::Field->new( '011', '', '', a => '0317-8471' ), >- MARC::Field->new( '545', '', '', a => 'Invisible on OPAC' ), >+ $link = $biblio->generate_marc_host_field(); >+ is( $link->subfield('7'), 'c2am', 'Subfield 7 correctly formed for corporate author' ); >+ is( $link->subfield('a'), 'Corporate Author', 'Subfield a contains corporate author' ); >+ >+ # 3. Test MARC21 with meeting name (111) >+ my $record_meeting = MARC::Record->new(); >+ $record_meeting->leader('00000nam a22000007a 4500'); >+ $record_meeting->append_fields( >+ MARC::Field->new( '111', '2', '', 'a' => 'Conference Name', 'j' => 'relator', '9' => 'xyz', '4' => 'spn' ), >+ MARC::Field->new( '245', '1', '0', 'a' => 'The Title' ) > ); >- C4::Biblio::ModBiblio( $record, $biblio->biblionumber ); >+ ($biblio_id) = AddBiblio( $record_meeting, qw{} ); >+ $biblio = Koha::Biblios->find($biblio_id); >+ >+ $link = $biblio->generate_marc_host_field(); >+ is( $link->subfield('7'), 'm2am', 'Subfield 7 correctly formed for meeting name' ); >+ >+ # 4. Test MARC21 with minimal record >+ my $record_minimal = MARC::Record->new(); >+ $record_minimal->leader('00000nam a22000007a 4500'); >+ $record_minimal->append_fields( MARC::Field->new( '245', '0', '0', 'a' => 'Title Only' ) ); >+ ($biblio_id) = AddBiblio( $record_minimal, qw{} ); >+ $biblio = Koha::Biblios->find($biblio_id); >+ >+ $link = $biblio->generate_marc_host_field(); >+ is( $link->subfield('7'), 'nnam', 'Subfield 7 correctly formed with no main entry' ); >+ >+ # 5. Test UNIMARC >+ t::lib::Mocks::mock_preference( 'marcflavour', 'UNIMARC' ); >+ $biblio = $builder->build_sample_biblio(); >+ my $record_unimarc = MARC::Record->new(); >+ $record_unimarc->append_fields( >+ MARC::Field->new( '001', '54321' ), >+ MARC::Field->new( '010', '', '', 'a' => '978-0-12-345678-9' ), >+ MARC::Field->new( '011', '', '', 'a' => '2345-6789' ), >+ MARC::Field->new( '200', '', '', 'a' => 'UNIMARC Title' ), >+ MARC::Field->new( '205', '', '', 'a' => 'Third edition' ), >+ MARC::Field->new( '210', '', '', 'a' => 'Paris', 'd' => '2023' ), >+ MARC::Field->new( '700', '', '', 'a' => 'Doe', 'b' => 'Jane' ), >+ MARC::Field->new( '856', '', '', 'u' => 'http://example.com' ) >+ ); >+ ($biblio_id) = AddBiblio( $record_unimarc, qw{} ); >+ $biblio = Koha::Biblios->find($biblio_id); >+ >+ $link = $biblio->generate_marc_host_field(); >+ >+ is( ref($link), 'MARC::Field', 'Returns a MARC::Field object for UNIMARC' ); >+ is( $link->tag(), '461', 'Field tag is 461 for UNIMARC' ); >+ is( $link->indicator(1), '0', 'First indicator is 0 for UNIMARC' ); >+ is( $link->indicator(2), ' ', 'Second indicator is blank for UNIMARC' ); >+ >+ # Check UNIMARC subfields >+ is( $link->subfield('a'), 'Doe Jane', 'Subfield a contains author for UNIMARC' ); >+ is( $link->subfield('t'), 'UNIMARC Title', 'Subfield t contains title for UNIMARC' ); >+ is( $link->subfield('c'), 'Paris', 'Subfield c contains place of publication for UNIMARC' ); >+ is( $link->subfield('d'), '2023', 'Subfield d contains date of publication for UNIMARC' ); >+ is( $link->subfield('0'), '54321', 'Subfield 0 contains control number for UNIMARC' ); >+ >+ # 6. Test UNIMARC with different author types >+ my $record_unimarc_corporate = MARC::Record->new(); >+ $record_unimarc_corporate->append_fields( >+ MARC::Field->new( '710', '', '', 'a' => 'Corporate', 'b' => 'Department' ), >+ MARC::Field->new( '200', '', '', 'a' => 'Title' ) >+ ); >+ C4::Biblio::ModBiblio( $record_unimarc_corporate, $biblio->biblionumber ); > $biblio = Koha::Biblios->find( $biblio->biblionumber ); > > $link = $biblio->generate_marc_host_field(); >+ is( $link->subfield('a'), 'Corporate Department', 'Subfield a contains corporate author for UNIMARC' ); >+ >+ my $record_unimarc_family = MARC::Record->new(); >+ $record_unimarc_family->append_fields( >+ MARC::Field->new( '720', '', '', 'a' => 'Family', 'b' => 'Name' ), >+ MARC::Field->new( '200', '', '', 'a' => 'Title' ) >+ ); >+ C4::Biblio::ModBiblio( $record_unimarc_family, $biblio->biblionumber ); >+ $biblio = Koha::Biblios->find( $biblio->biblionumber ); > >- is( ref($link), 'MARC::Field', "->generate_marc_host_field returns a MARC::Field object" ); >- is( $link->tag, '461', "MARC::Field->tag returns '461' when marcflavour is 'UNIMARC" ); >- is( $link->subfield('a'), 'A nice author', 'MARC::Field->subfield(a) returns content from 700ab' ); >- is( $link->subfield('c'), 'A publication', 'MARC::Field->subfield(b) returns content from 210a' ); >- is( $link->subfield('d'), 'A date', 'MARC::Field->subfield(c) returns content from 210d' ); >- is( $link->subfield('e'), 'Fun things', 'MARC::Field->subfield(s) returns content from 205' ); >- is( $link->subfield('t'), 'Some boring read', 'MARC::Field->subfield(s) returns content from 200a' ); >- is( $link->subfield('u'), 'http://myurl.com/', 'MARC::Field->subfield(s) returns content from 856u' ); >- is( $link->subfield('x'), '0317-8471', 'MARC::Field->subfield(s) returns content from 011a' ); >- is( $link->subfield('y'), undef, 'MARC::Field->subfield(w) returns undef if 010a is empty' ); >- is( $link->subfield('0'), '1234', 'MARC::Field->subfield(0) returns content from 001' ); >+ $link = $biblio->generate_marc_host_field(); >+ is( $link->subfield('a'), 'Family Name', 'Subfield a contains family name for UNIMARC' ); > > $schema->storage->txn_rollback; > t::lib::Mocks::mock_preference( 'marcflavour', 'MARC21' ); >diff --git a/t/db_dependent/Koha/Booking.t b/t/db_dependent/Koha/Booking.t >index f1fba22d66..a63d37b120 100755 >--- a/t/db_dependent/Koha/Booking.t >+++ b/t/db_dependent/Koha/Booking.t >@@ -20,7 +20,10 @@ > use Modern::Perl; > use utf8; > >-use Test::More tests => 2; >+use Test::More tests => 3; >+use Test::NoWarnings; >+ >+use Test::Warn; > > use Test::Exception; > >@@ -369,7 +372,7 @@ subtest 'store() tests' => sub { > }; > > subtest 'confirmation notice trigger' => sub { >- plan tests => 2; >+ plan tests => 4; > > # FIXME: This is a bandaid solution to prevent test failures when running > # the Koha_Main_My8 job because notices are not added at upgrade time. >@@ -412,7 +415,16 @@ subtest 'store() tests' => sub { > )->count; > > # Reuse previous booking to produce a clash >- eval { $booking = Koha::Booking->new( $booking->unblessed )->store }; >+ warning_like( >+ sub { >+ throws_ok { >+ Koha::Booking->new( $booking->unblessed )->store >+ } >+ 'Koha::Exceptions::Object::DuplicateID', >+ 'Exception is thrown correctly'; >+ }, >+ qr{Duplicate entry '(.*?)' for key '(.*\.?)PRIMARY'} >+ ); > > my $post_notices_count = Koha::Notice::Messages->search( > { >@@ -612,12 +624,17 @@ subtest 'store() tests' => sub { > }; > > subtest 'status change exception' => sub { >- plan tests => 2; >+ plan tests => 3; > > $booking->discard_changes; > my $status = $booking->status; >- throws_ok { $booking->update( { status => 'blah' } ) } 'Koha::Exceptions::Object::BadValue', >- 'Throws exception when passed booking status would fail enum constraint'; >+ warning_like( >+ sub { >+ throws_ok { $booking->update( { status => 'blah' } ) } 'Koha::Exceptions::Object::BadValue', >+ 'Throws exception when passed booking status would fail enum constraint'; >+ }, >+ qr{Data truncated for column 'status'} >+ ); > > # Status unchanged > $booking->discard_changes; >diff --git a/t/db_dependent/Koha/Hold.t b/t/db_dependent/Koha/Hold.t >index 4b84be5744..55fe078227 100755 >--- a/t/db_dependent/Koha/Hold.t >+++ b/t/db_dependent/Koha/Hold.t >@@ -346,25 +346,28 @@ subtest 'fill() tests' => sub { > > subtest 'holds_queue update tests' => sub { > >- plan tests => 1; >+ plan tests => 2; > > my $biblio = $builder->build_sample_biblio; > >- my $mock = Test::MockModule->new('Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue'); >+ # The check of the pref is in the Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue >+ # so we mock the base enqueue method here to see if it is called >+ my $mock = Test::MockModule->new('Koha::BackgroundJob'); > $mock->mock( > 'enqueue', > sub { > my ( $self, $args ) = @_; > is_deeply( >- $args->{biblio_ids}, >+ $args->{job_args}->{biblio_ids}, > [ $biblio->id ], >- '->fill triggers a holds queue update for the related biblio' >+ 'when pref enabled the previous action triggers a holds queue update for the related biblio' > ); > } > ); > > t::lib::Mocks::mock_preference( 'RealTimeHoldsQueue', 1 ); > >+ # Filling a hold when pref enabled should trigger a test > $builder->build_object( > { > class => 'Koha::Holds', >@@ -376,7 +379,7 @@ subtest 'fill() tests' => sub { > > t::lib::Mocks::mock_preference( 'RealTimeHoldsQueue', 0 ); > >- # this call shouldn't add a new test >+ # Filling a hold when pref disabled should not trigger a test > $builder->build_object( > { > class => 'Koha::Holds', >@@ -385,6 +388,42 @@ subtest 'fill() tests' => sub { > } > } > )->fill; >+ >+ my $library_1 = $builder->build_object( >+ { >+ class => 'Koha::Libraries', >+ } >+ )->store; >+ my $library_2 = $builder->build_object( >+ { >+ class => 'Koha::Libraries', >+ } >+ )->store; >+ >+ my $hold = $builder->build_object( >+ { >+ class => 'Koha::Holds', >+ value => { >+ biblionumber => $biblio->id, >+ branchcode => $library_1->branchcode, >+ } >+ } >+ )->store; >+ >+ # Pref is off, no test triggered >+ # Updating a hold location when pref disabled should not trigger a test >+ $hold->branchcode( $library_2->branchcode )->store; >+ >+ t::lib::Mocks::mock_preference( 'RealTimeHoldsQueue', 1 ); >+ >+ # Updating a hold location when pref enabled should trigger a test >+ >+ # Pref is on, test triggered >+ $hold->branchcode( $library_1->branchcode )->store; >+ >+ # Update with no change to pickup location should not trigger a test >+ $hold->branchcode( $library_1->branchcode )->store; >+ > }; > > $schema->storage->txn_rollback; >diff --git a/t/db_dependent/Koha/Installer.t b/t/db_dependent/Koha/Installer.t >index c9091d2bde..f127bf0452 100755 >--- a/t/db_dependent/Koha/Installer.t >+++ b/t/db_dependent/Koha/Installer.t >@@ -18,7 +18,8 @@ > # along with Koha; if not, see <http://www.gnu.org/licenses>. > > use Modern::Perl; >-use Test::More tests => 4; >+use Test::More tests => 5; >+use Test::NoWarnings; > > use Test::MockModule; > use File::Temp qw/tempdir/; >diff --git a/t/db_dependent/Koha/Item.t b/t/db_dependent/Koha/Item.t >index 1ff4f8a9bd..c505066d88 100755 >--- a/t/db_dependent/Koha/Item.t >+++ b/t/db_dependent/Koha/Item.t >@@ -21,7 +21,7 @@ use Modern::Perl; > use utf8; > > use Test::NoWarnings; >-use Test::More tests => 40; >+use Test::More tests => 41; > use Test::Exception; > use Test::MockModule; > use Test::Warn; >@@ -188,6 +188,57 @@ subtest '_status() tests' => sub { > } > > t::lib::Mocks::mock_preference( 'UseRecalls', 0 ); >+ $schema->storage->txn_rollback; >+}; >+ >+subtest 'store PreventWithdrawingItemsStatus' => sub { >+ plan tests => 2; >+ $schema->storage->txn_begin; >+ >+ t::lib::Mocks::mock_preference( 'PreventWithdrawingItemsStatus', 'intransit,checkedout' ); >+ my $library_1 = $builder->build( { source => 'Branch' } ); >+ my $library_2 = $builder->build( { source => 'Branch' } ); >+ >+ my $patron = $builder->build_object( { class => 'Koha::Patrons' } ); >+ t::lib::Mocks::mock_userenv( { branchcode => $patron->branchcode } ); >+ >+ my $item = $builder->build_sample_item( >+ { >+ withdrawn => 0, >+ } >+ ); >+ >+ #Check item out >+ C4::Circulation::AddIssue( $patron, $item->barcode ); >+ >+ throws_ok { $item->withdrawn('1')->store } >+ 'Koha::Exceptions::Item::Transfer::OnLoan', >+ 'Exception thrown when trying to withdraw checked-out item'; >+ >+ my $item_2 = $builder->build_sample_item( >+ { >+ withdrawn => 0, >+ } >+ ); >+ >+ #set in_transit >+ my $transfer_1 = $builder->build_object( >+ { >+ class => 'Koha::Item::Transfers', >+ value => { >+ itemnumber => $item_2->itemnumber, >+ frombranch => $library_1->{branchcode}, >+ tobranch => $library_2->{branchcode}, >+ datesent => '1999-12-31', >+ } >+ } >+ ); >+ >+ throws_ok { $item_2->withdrawn('1')->store } >+ 'Koha::Exceptions::Item::Transfer::InTransit', >+ 'Exception thrown when trying to withdraw item in transit'; >+ >+ t::lib::Mocks::mock_preference( 'PreventWithdrawingItemsStatus', '' ); > > $schema->storage->txn_rollback; > }; >@@ -197,7 +248,6 @@ subtest 'z3950_status' => sub { > > $schema->storage->txn_begin; > t::lib::Mocks::mock_preference( 'z3950Status', '' ); >- > my $itemtype = $builder->build_object( { class => "Koha::ItemTypes" } ); > my $item = $builder->build_sample_item( > { >diff --git a/t/db_dependent/Koha/Items/BatchUpdate.t b/t/db_dependent/Koha/Items/BatchUpdate.t >index 18f0304cf5..e3895459e5 100755 >--- a/t/db_dependent/Koha/Items/BatchUpdate.t >+++ b/t/db_dependent/Koha/Items/BatchUpdate.t >@@ -284,7 +284,7 @@ subtest 'mark_items_returned' => sub { > }; > > subtest 'report' => sub { >- plan tests => 5; >+ plan tests => 7; > > my $item_1 = $builder->build_sample_item( { biblionumber => $biblio->biblionumber } ); > my $item_2 = $builder->build_sample_item( { biblionumber => $biblio->biblionumber } ); >@@ -297,7 +297,8 @@ subtest 'report' => sub { > $report, > { > modified_itemnumbers => [ $item_1->itemnumber, $item_2->itemnumber ], >- modified_fields => 2 >+ modified_fields => 2, >+ errors => [] > } > ); > >@@ -308,7 +309,8 @@ subtest 'report' => sub { > $report, > { > modified_itemnumbers => [ $item_1->itemnumber, $item_2->itemnumber ], >- modified_fields => 2 >+ modified_fields => 2, >+ errors => [] > } > ); > >@@ -320,7 +322,8 @@ subtest 'report' => sub { > $report, > { > modified_itemnumbers => [ $item_1->itemnumber ], >- modified_fields => 1 >+ modified_fields => 1, >+ errors => [] > } > ); > >@@ -331,7 +334,8 @@ subtest 'report' => sub { > $report, > { > modified_itemnumbers => [ $item_1->itemnumber, $item_2->itemnumber ], >- modified_fields => 4 >+ modified_fields => 4, >+ errors => [] > } > ); > >@@ -349,10 +353,61 @@ subtest 'report' => sub { > $report, > { > modified_itemnumbers => [ $item_1->itemnumber, $item_2->itemnumber ], >- modified_fields => 7 >+ modified_fields => 7, >+ errors => [] >+ } >+ ); >+ t::lib::Mocks::mock_preference( 'PreventWithDrawingItemsStatus', 'intransit,checkedout' ); >+ >+ $item_2->get_from_storage->update( { onloan => '2025-01-01' } ); >+ >+ local $SIG{__WARN__} = sub { }; >+ my ($report2) = $items->batch_update( { new_values => { withdrawn => 1 } } ); >+ >+ $items->reset; >+ >+ is_deeply( >+ $report2, >+ { >+ modified_itemnumbers => [ $item_1->itemnumber ], >+ modified_fields => 1, >+ errors => [ >+ { error => "Exception 'Koha::Exceptions::Item::Transfer::OnLoan' thrown 'onloan_cannot_withdraw'\n" } >+ ] > } > ); > >+ my $item_3 = $builder->build_sample_item( { biblionumber => $biblio->biblionumber } ); >+ my $item_4 = $builder->build_sample_item( { biblionumber => $biblio->biblionumber } ); >+ >+ Koha::Item::Transfer->new( >+ { >+ itemnumber => $item_3->itemnumber, >+ frombranch => $item_3->homebranch, >+ tobranch => $builder->build( { source => 'Branch' } )->{branchcode}, >+ datesent => '1999-01-01', >+ } >+ )->store; >+ >+ my $items2 = Koha::Items->search( { itemnumber => [ $item_3->itemnumber, $item_4->itemnumber ] } ); >+ >+ local $SIG{__WARN__} = sub { }; >+ my ($report3) = $items2->batch_update( { new_values => { withdrawn => 1 } } ); >+ >+ $items2->reset; >+ is_deeply( >+ $report3, >+ { >+ modified_itemnumbers => [ $item_4->itemnumber ], >+ modified_fields => 1, >+ errors => [ >+ { >+ error => >+ "Exception 'Koha::Exceptions::Item::Transfer::InTransit' thrown 'intransit_cannot_withdraw'\n" >+ } >+ ] >+ } >+ ); > }; > > Koha::Caches->get_instance->clear_from_cache("MarcStructure-1-"); >diff --git a/t/db_dependent/Koha/Old/Hold.t b/t/db_dependent/Koha/Old/Hold.t >index 1204766b72..70b3368840 100755 >--- a/t/db_dependent/Koha/Old/Hold.t >+++ b/t/db_dependent/Koha/Old/Hold.t >@@ -17,7 +17,8 @@ > > use Modern::Perl; > >-use Test::More tests => 3; >+use Test::More tests => 4; >+use Test::NoWarnings; > use Test::Exception; > > use Koha::Database; >@@ -103,7 +104,7 @@ subtest 'biblio() tests' => sub { > my $hold_2 = $builder->build_object( > { > class => 'Koha::Old::Holds', >- value => { biblionumber => '' } >+ value => { biblionumber => undef } > } > ); > >diff --git a/t/db_dependent/Koha/Patron.t b/t/db_dependent/Koha/Patron.t >index e908706c8b..7cba994728 100755 >--- a/t/db_dependent/Koha/Patron.t >+++ b/t/db_dependent/Koha/Patron.t >@@ -19,7 +19,8 @@ > > use Modern::Perl; > >-use Test::More tests => 39; >+use Test::More tests => 42; >+use Test::NoWarnings; > use Test::Exception; > use Test::Warn; > use Time::Fake; >@@ -29,7 +30,8 @@ use Koha::Database; > use Koha::DateUtils qw(dt_from_string); > use Koha::ArticleRequests; > use Koha::Patrons; >-use Koha::List::Patron qw(AddPatronList AddPatronsToList); >+use Koha::List::Patron qw(AddPatronList AddPatronsToList); >+use Koha::Patron::Debarments qw(AddDebarment); > use Koha::Patron::Relationships; > use C4::Circulation qw( AddIssue AddReturn ); > >@@ -738,6 +740,8 @@ subtest 'to_api() tests' => sub { > } > ); > >+ t::lib::Mocks::mock_userenv( { patron => $consumer } ); >+ > my $restricted = $patron->to_api( { user => $consumer } )->{restricted}; > ok( defined $restricted, 'restricted is defined' ); > ok( !$restricted, 'debarred is undef, restricted evaluates to false' ); >@@ -2868,3 +2872,249 @@ subtest 'ill_requests() tests' => sub { > > $schema->storage->txn_rollback; > }; >+ >+subtest 'can_place_holds() tests' => sub { >+ >+ plan tests => 7; >+ >+ subtest "'expired' tests" => sub { >+ >+ plan tests => 4; >+ >+ $schema->storage->txn_begin; >+ >+ t::lib::Mocks::mock_preference( 'BlockExpiredPatronOpacActions', 'hold' ); >+ >+ my $patron = $builder->build_object( >+ { >+ class => 'Koha::Patrons', >+ value => { dateexpiry => \'DATE_ADD(NOW(), INTERVAL -1 DAY)' } >+ } >+ ); >+ >+ my $result = $patron->can_place_holds(); >+ ok( !$result, 'expired, cannot place holds' ); >+ >+ my $messages = $result->messages(); >+ is( $messages->[0]->type, 'error' ); >+ is( $messages->[0]->message, 'expired' ); >+ >+ ok( $patron->can_place_holds( { overrides => { expired => 1 } } ), "Override works for 'expired'" ); >+ >+ $schema->storage->txn_rollback; >+ }; >+ >+ subtest "'debt_limit' tests" => sub { >+ >+ plan tests => 7; >+ >+ $schema->storage->txn_begin; >+ >+ # Add a patron, making sure it is not (yet) expired >+ my $patron = $builder->build_object( { class => 'Koha::Patrons', } ); >+ $patron->account->add_debit( { amount => 10, interface => 'opac', type => 'ACCOUNT' } ); >+ >+ t::lib::Mocks::mock_preference( 'maxoutstanding', undef ); >+ >+ ok( $patron->can_place_holds(), "No 'maxoutstanding', can place holds" ); >+ >+ t::lib::Mocks::mock_preference( 'maxoutstanding', '5' ); >+ >+ my $result = $patron->can_place_holds(); >+ ok( !$result, 'debt, cannot place holds' ); >+ >+ my $messages = $result->messages(); >+ is( $messages->[0]->type, 'error' ); >+ is( $messages->[0]->message, 'debt_limit' ); >+ is( $messages->[0]->payload->{total_outstanding}, 10 ); >+ is( $messages->[0]->payload->{max_outstanding}, 5 ); >+ >+ ok( $patron->can_place_holds( { overrides => { debt_limit => 1 } } ), "Override works for 'debt_limit'" ); >+ >+ $schema->storage->txn_rollback; >+ }; >+ >+ subtest "'bad_address' tests" => sub { >+ >+ plan tests => 4; >+ >+ $schema->storage->txn_begin; >+ >+ # Add a patron, making sure it is not (yet) expired >+ my $patron = $builder->build_object( >+ { >+ class => 'Koha::Patrons', >+ value => { >+ gonenoaddress => 1, >+ } >+ } >+ ); >+ >+ my $result = $patron->can_place_holds(); >+ ok( !$result, 'flagged for bad address, cannot place holds' ); >+ >+ my $messages = $result->messages(); >+ is( $messages->[0]->type, 'error' ); >+ is( $messages->[0]->message, 'bad_address' ); >+ >+ ok( $patron->can_place_holds( { overrides => { bad_address => 1 } } ), "Override works for 'bad_address'" ); >+ >+ $schema->storage->txn_rollback; >+ }; >+ >+ subtest "'card_lost' tests" => sub { >+ >+ plan tests => 4; >+ >+ $schema->storage->txn_begin; >+ >+ # Add a patron, making sure it is not (yet) expired >+ my $patron = $builder->build_object( >+ { >+ class => 'Koha::Patrons', >+ value => { >+ lost => 1, >+ } >+ } >+ ); >+ >+ my $result = $patron->can_place_holds(); >+ ok( !$result, 'flagged for lost card, cannot place holds' ); >+ >+ my $messages = $result->messages(); >+ is( $messages->[0]->type, 'error' ); >+ is( $messages->[0]->message, 'card_lost' ); >+ >+ ok( $patron->can_place_holds( { overrides => { card_lost => 1 } } ), "Override works for 'card_lost'" ); >+ >+ $schema->storage->txn_rollback; >+ }; >+ >+ subtest "'restricted' tests" => sub { >+ >+ plan tests => 4; >+ >+ $schema->storage->txn_begin; >+ >+ # Add a patron, making sure it is not (yet) expired >+ my $patron = $builder->build_object( { class => 'Koha::Patrons' } ); >+ AddDebarment( { borrowernumber => $patron->borrowernumber } ); >+ $patron->discard_changes(); >+ >+ my $result = $patron->can_place_holds(); >+ ok( !$result, 'restricted, cannot place holds' ); >+ >+ my $messages = $result->messages(); >+ is( $messages->[0]->type, 'error' ); >+ is( $messages->[0]->message, 'restricted' ); >+ >+ ok( $patron->can_place_holds( { overrides => { restricted => 1 } } ), "Override works for 'restricted'" ); >+ >+ $schema->storage->txn_rollback; >+ }; >+ >+ subtest "'hold_limit' tests" => sub { >+ >+ plan tests => 5; >+ >+ $schema->storage->txn_begin; >+ >+ # Add a patron, making sure it is not (yet) expired >+ my $patron = $builder->build_object( { class => 'Koha::Patrons' } ); >+ >+ # Add a hold >+ $builder->build_object( { class => 'Koha::Holds', value => { borrowernumber => $patron->borrowernumber } } ); >+ >+ t::lib::Mocks::mock_preference( 'maxreserves', undef ); >+ >+ ok( $patron->can_place_holds(), "No 'maxreserves', can place holds" ); >+ >+ t::lib::Mocks::mock_preference( 'maxreserves', 1 ); >+ >+ my $result = $patron->can_place_holds(); >+ ok( !$result, 'hold limit reached, cannot place holds' ); >+ >+ my $messages = $result->messages(); >+ is( $messages->[0]->type, 'error' ); >+ is( $messages->[0]->message, 'hold_limit' ); >+ >+ ok( $patron->can_place_holds( { overrides => { hold_limit => 1 } } ), "Override works for 'hold_limit'" ); >+ >+ $schema->storage->txn_rollback; >+ }; >+ >+ subtest "'no_short_circuit' tests" => sub { >+ >+ plan tests => 9; >+ >+ $schema->storage->txn_begin; >+ >+ # Create a patron with multiple issues >+ my $patron = $builder->build_object( >+ { >+ class => 'Koha::Patrons', >+ value => { >+ dateexpiry => \'DATE_ADD(NOW(), INTERVAL -1 DAY)', # expired >+ gonenoaddress => 1, # bad address >+ lost => 1, # card lost >+ } >+ } >+ ); >+ >+ # Add debt >+ $patron->account->add_debit( { amount => 10, interface => 'opac', type => 'ACCOUNT' } ); >+ >+ # Add restriction >+ AddDebarment( { borrowernumber => $patron->borrowernumber } ); >+ $patron->discard_changes(); >+ >+ # Mock preferences >+ t::lib::Mocks::mock_preference( 'BlockExpiredPatronOpacActions', 'hold' ); >+ t::lib::Mocks::mock_preference( 'maxoutstanding', '5' ); >+ >+ # Test short-circuit behavior (default) >+ my $result = $patron->can_place_holds(); >+ ok( !$result, 'patron cannot place holds' ); >+ my $messages = $result->messages(); >+ is( scalar @$messages, 1, 'short-circuit: only one error message returned' ); >+ >+ # Test no_short_circuit behavior >+ $result = $patron->can_place_holds( { no_short_circuit => 1 } ); >+ ok( !$result, 'patron still cannot place holds with no_short_circuit' ); >+ $messages = $result->messages(); >+ is( scalar @$messages, 5, 'no_short_circuit: all error messages collected' ); >+ >+ # Verify we got all expected error types >+ my %message_types = map { $_->message => 1 } @$messages; >+ ok( $message_types{expired}, "'expired' error included" ); >+ ok( $message_types{debt_limit}, "'debt_limit' error included" ); >+ ok( $message_types{bad_address}, "'bad_address' error included" ); >+ ok( $message_types{card_lost}, "'card_lost' error included" ); >+ ok( $message_types{restricted}, "'restricted' error included" ); >+ >+ $schema->storage->txn_rollback; >+ }; >+}; >+ >+subtest 'is_anonymous' => sub { >+ plan tests => 3; >+ >+ $schema->storage->txn_begin; >+ >+ t::lib::Mocks::mock_preference( 'AnonymousPatron', '' ); >+ >+ my $patron = $builder->build_object( { class => 'Koha::Patrons' } ); >+ >+ is( $patron->is_anonymous, 0, 'is_anonymous returns 0 if pref is empty' ); >+ >+ t::lib::Mocks::mock_preference( 'AnonymousPatron', $patron->borrowernumber ); >+ >+ is( $patron->is_anonymous, 1, q{is_anonymous returns 1 if pref is equal to patron's id} ); >+ >+ t::lib::Mocks::mock_preference( 'AnonymousPatron', $patron->borrowernumber + 1 ); >+ >+ is( $patron->is_anonymous, 0, q{is_anonymous returns 0 if pref is not equal to patron's id} ); >+ >+ $schema->storage->txn_rollback; >+ >+}; >diff --git a/t/db_dependent/Koha/Patron/Modifications.t b/t/db_dependent/Koha/Patron/Modifications.t >index f430fa452d..147fc04dc8 100755 >--- a/t/db_dependent/Koha/Patron/Modifications.t >+++ b/t/db_dependent/Koha/Patron/Modifications.t >@@ -19,7 +19,8 @@ use Modern::Perl; > > use utf8; > >-use Test::More tests => 7; >+use Test::More tests => 8; >+use Test::NoWarnings; > use Test::Exception; > > use t::lib::TestBuilder; >@@ -49,9 +50,12 @@ subtest 'new() tests' => sub { > > Koha::Patron::Modifications->search->delete; > >+ my $patron = $builder->build_object( { class => 'Koha::Patrons' } ); >+ > # Create new pending modification > Koha::Patron::Modification->new( > { >+ borrowernumber => $patron->borrowernumber, > verification_token => '1234567890', > changed_fields => 'surname,firstname', > surname => 'Hall', >@@ -443,9 +447,10 @@ subtest 'dateofbirth tests' => sub { > { class => 'Koha::Patrons', value => { dateofbirth => '1980-01-01', surname => 'a_surname' } } ); > my $patron_modification = Koha::Patron::Modification->new( > { >- changed_fields => 'borrowernumber,dateofbirth', >- borrowernumber => $patron->borrowernumber, >- dateofbirth => undef >+ verification_token => '1234567890', >+ changed_fields => 'borrowernumber,dateofbirth', >+ borrowernumber => $patron->borrowernumber, >+ dateofbirth => undef > } > )->store; > $patron_modification->approve; >@@ -459,9 +464,10 @@ subtest 'dateofbirth tests' => sub { > # Adding a dateofbirth > $patron_modification = Koha::Patron::Modification->new( > { >- changed_fields => 'borrowernumber,dateofbirth', >- borrowernumber => $patron->borrowernumber, >- dateofbirth => '1980-02-02' >+ verification_token => '1234567890', >+ changed_fields => 'borrowernumber,dateofbirth', >+ borrowernumber => $patron->borrowernumber, >+ dateofbirth => '1980-02-02' > } > )->store; > $patron_modification->approve; >@@ -476,10 +482,11 @@ subtest 'dateofbirth tests' => sub { > # Modifying a dateofbirth > $patron_modification = Koha::Patron::Modification->new( > { >- changed_fields => 'borrowernumber,dateofbirth', >- borrowernumber => $patron->borrowernumber, >- dateofbirth => '1980-03-03', >- surname => undef >+ verification_token => '1234567890', >+ changed_fields => 'borrowernumber,dateofbirth', >+ borrowernumber => $patron->borrowernumber, >+ dateofbirth => '1980-03-03', >+ surname => undef > } > )->store; > $patron_modification->approve; >@@ -494,10 +501,11 @@ subtest 'dateofbirth tests' => sub { > # Modifying something else > $patron_modification = Koha::Patron::Modification->new( > { >- changed_fields => 'borrowernumber,surname', >- borrowernumber => $patron->borrowernumber, >- surname => 'another_surname', >- dateofbirth => undef >+ verification_token => '1234567890', >+ changed_fields => 'borrowernumber,surname', >+ borrowernumber => $patron->borrowernumber, >+ surname => 'another_surname', >+ dateofbirth => undef > } > )->store; > $patron_modification->approve; >diff --git a/t/db_dependent/Koha/Patrons.t b/t/db_dependent/Koha/Patrons.t >index ed7ad7a351..54595fecc6 100755 >--- a/t/db_dependent/Koha/Patrons.t >+++ b/t/db_dependent/Koha/Patrons.t >@@ -2675,10 +2675,10 @@ subtest 'lock' => sub { > }; > > subtest 'anonymize' => sub { >- plan tests => 10; >+ plan tests => 11; > >- my $patron1 = $builder->build_object( { class => 'Koha::Patrons' } ); >- my $patron2 = $builder->build_object( { class => 'Koha::Patrons' } ); >+ my $patron1 = $builder->build_object( { class => 'Koha::Patrons' } )->store; >+ my $patron2 = $builder->build_object( { class => 'Koha::Patrons' } )->store; > > # First try patron with issues > my $issue = $builder->build_object( >@@ -2696,16 +2696,17 @@ subtest 'anonymize' => sub { > is( $patron1->firstname, undef, 'First name cleared' ); > isnt( $patron1->surname, $surname, 'Surname changed' ); > ok( $patron1->surname =~ /^\w{10}$/, 'Mandatory surname randomized' ); >- is( $patron1->branchcode, $branchcode, 'Branch code skipped' ); >- is( $patron1->email, undef, 'Email was mandatory, must be cleared' ); >+ is( $patron1->branchcode, $branchcode, 'Branch code skipped' ); >+ is( $patron1->email, undef, 'Email was mandatory, must be cleared' ); >+ is( $patron1->checkprevcheckout, 'inherit', 'Enum checkprevcheckout is reset to the default value' ); > > # Test wrapper in Koha::Patrons >- $patron1->surname($surname)->store; # restore >+ $patron1->surname($surname)->store; # restore > my $rs = Koha::Patrons->search( { borrowernumber => [ $patron1->borrowernumber, $patron2->borrowernumber ] } ) > ->anonymize; >- $patron1->discard_changes; # refresh >+ $patron1->discard_changes; # refresh > isnt( $patron1->surname, $surname, 'Surname patron1 changed again' ); >- $patron2->discard_changes; # refresh >+ $patron2->discard_changes; # refresh > is( $patron2->firstname, undef, 'First name patron2 cleared' ); > }; > >diff --git a/t/db_dependent/Koha/Patrons/Import.t b/t/db_dependent/Koha/Patrons/Import.t >index 8949574a79..64e5f95a06 100755 >--- a/t/db_dependent/Koha/Patrons/Import.t >+++ b/t/db_dependent/Koha/Patrons/Import.t >@@ -18,7 +18,8 @@ > # along with Koha; if not, see <http://www.gnu.org/licenses>. > > use Modern::Perl; >-use Test::More tests => 180; >+use Test::More tests => 181; >+use Test::NoWarnings; > use Test::Warn; > use Test::Exception; > use Encode qw( encode_utf8 ); >diff --git a/t/db_dependent/Koha/Plugins/Patron.t b/t/db_dependent/Koha/Plugins/Patron.t >index ebe4531b08..da5382b64d 100755 >--- a/t/db_dependent/Koha/Plugins/Patron.t >+++ b/t/db_dependent/Koha/Plugins/Patron.t >@@ -16,8 +16,10 @@ > > use Modern::Perl; > >-use Test::More tests => 5; >+use Test::More tests => 6; >+use Test::NoWarnings; > use Test::Exception; >+use Test::MockModule; > > use File::Basename; > >@@ -51,6 +53,10 @@ subtest 'check_password hook tests' => sub { > my $plugins = Koha::Plugins->new; > $plugins->InstallPlugins; > >+ # Mock patron_barcode_transform: we don't want to call it as it generates warnings >+ my $plugin_mock = Test::MockModule->new("Koha::Plugin::Test"); >+ $plugin_mock->mock( "patron_barcode_transform", sub { } ); >+ > # Test Plugin enforces a 4 digit numeric pin for passwords > my $plugin = Koha::Plugin::Test->new->enable; > >diff --git a/t/db_dependent/Koha/Reports.t b/t/db_dependent/Koha/Reports.t >index 205b1df60a..9778357d87 100755 >--- a/t/db_dependent/Koha/Reports.t >+++ b/t/db_dependent/Koha/Reports.t >@@ -18,7 +18,7 @@ > use Modern::Perl; > > use Test::NoWarnings; >-use Test::More tests => 9; >+use Test::More tests => 10; > > use Koha::Report; > use Koha::Reports; >@@ -185,4 +185,40 @@ subtest '_might_add_limit' => sub { > ); > }; > >+subtest 'reports_branches are added and removed from report_branches table' => sub { >+ plan tests => 4; >+ >+ my $updated_nb_of_reports = Koha::Reports->search->count; >+ my $report = Koha::Report->new( >+ { >+ report_name => 'report_name_for_test_1', >+ savedsql => 'SELECT * FROM items WHERE itemnumber IN <<Test|list>>', >+ } >+ )->store; >+ >+ my $id = $report->id; >+ my $library1 = $builder->build_object( { class => 'Koha::Libraries' } ); >+ my $library2 = $builder->build_object( { class => 'Koha::Libraries' } ); >+ my $library3 = $builder->build_object( { class => 'Koha::Libraries' } ); >+ my @branches = ( $library1->branchcode, $library2->branchcode, $library3->branchcode ); >+ >+ $report->replace_library_limits( \@branches ); >+ >+ my @branches_loop = $report->get_library_limits->as_list; >+ is( scalar @branches_loop, 3, '3 branches added to report_branches table' ); >+ >+ $report->replace_library_limits( [ $library1->branchcode, $library2->branchcode ] ); >+ >+ @branches_loop = $report->get_library_limits->as_list; >+ is( scalar @branches_loop, 2, '1 branch removed from report_branches table' ); >+ >+ $report->delete; >+ is( Koha::Reports->search->count, $updated_nb_of_reports, 'Report deleted, count is back to original' ); >+ is( >+ $schema->resultset('ReportsBranch')->search( { report_id => $id } )->count, >+ 0, >+ 'No branches left in reports_branches table after report deletion' >+ ); >+}; >+ > $schema->storage->txn_rollback; >diff --git a/t/db_dependent/Koha/SearchEngine/Elasticsearch.t b/t/db_dependent/Koha/SearchEngine/Elasticsearch.t >index 2e4b3402ec..66b267fc34 100755 >--- a/t/db_dependent/Koha/SearchEngine/Elasticsearch.t >+++ b/t/db_dependent/Koha/SearchEngine/Elasticsearch.t >@@ -18,7 +18,9 @@ > use Modern::Perl; > use Encode; > >-use Test::More tests => 8; >+use Test::More tests => 9; >+use Test::NoWarnings; >+use Test::Warn; > use Test::Exception; > > use t::lib::Mocks; >@@ -209,7 +211,7 @@ subtest 'get_elasticsearch_mappings() tests' => sub { > > subtest 'Koha::SearchEngine::Elasticsearch::marc_records_to_documents () tests' => sub { > >- plan tests => 70; >+ plan tests => 71; > > t::lib::Mocks::mock_preference( 'marcflavour', 'MARC21' ); > t::lib::Mocks::mock_preference( 'ElasticsearchMARCFormat', 'base64ISO2709' ); >@@ -851,7 +853,10 @@ subtest 'Koha::SearchEngine::Elasticsearch::marc_records_to_documents () tests' > MARC::Field->new( '999', '', '', c => '1234567' ), > ); > >- $docs = $see->marc_records_to_documents( [$marc_record_with_large_field] ); >+ warning_is { >+ $docs = $see->marc_records_to_documents( [$marc_record_with_large_field] ); >+ } >+ "Warnings encountered while roundtripping a MARC record to/from USMARC. Failing over to MARCXML."; > > subtest '_process_mappings() split tests' => sub { > >@@ -1225,21 +1230,18 @@ subtest 'marc_records_to_documents should set the "available" field' => sub { > # sort_fields will call this and use the actual db values unless we call it first > $see->get_elasticsearch_mappings(); > >- my $marc_record_1 = MARC::Record->new(); >- $marc_record_1->leader(' cam 22 a 4500'); >- $marc_record_1->append_fields( >- MARC::Field->new( '245', '', '', a => 'Title' ), >- ); >- my ($biblionumber) = C4::Biblio::AddBiblio( $marc_record_1, '' ); >+ my $builder = t::lib::TestBuilder->new; >+ my $biblio = $builder->build_sample_biblio; >+ my $marc_record_1 = $biblio->metadata->record; > > my $docs = $see->marc_records_to_documents( [$marc_record_1] ); > is_deeply( $docs->[0]->{available}, \0, 'a biblio without items is not available' ); > >- my $item = Koha::Item->new( >+ my $item = $builder->build_sample_item( > { >- biblionumber => $biblionumber, >+ biblionumber => $biblio->biblionumber, > } >- )->store(); >+ ); > > $docs = $see->marc_records_to_documents( [$marc_record_1] ); > is_deeply( $docs->[0]->{available}, \1, 'a biblio with one item that has no particular status is available' ); >@@ -1264,11 +1266,11 @@ subtest 'marc_records_to_documents should set the "available" field' => sub { > $docs = $see->marc_records_to_documents( [$marc_record_1] ); > is_deeply( $docs->[0]->{available}, \1, 'a biblio with one item that is damaged is available' ); > >- my $item2 = Koha::Item->new( >+ my $item2 = $builder->build_sample_item( > { >- biblionumber => $biblionumber, >+ biblionumber => $biblio->biblionumber, > } >- )->store(); >+ ); > $docs = $see->marc_records_to_documents( [$marc_record_1] ); > is_deeply( > $docs->[0]->{available}, \1, >diff --git a/t/db_dependent/Koha/SearchEngine/Elasticsearch/ExportConfig.t b/t/db_dependent/Koha/SearchEngine/Elasticsearch/ExportConfig.t >index 9fc6f7b57c..f14e253b68 100755 >--- a/t/db_dependent/Koha/SearchEngine/Elasticsearch/ExportConfig.t >+++ b/t/db_dependent/Koha/SearchEngine/Elasticsearch/ExportConfig.t >@@ -17,7 +17,8 @@ > > use Modern::Perl; > >-use Test::More tests => 19; >+use Test::More tests => 20; >+use Test::NoWarnings; > > use Koha::Database; > use Koha::SearchFields; >@@ -59,7 +60,8 @@ $search_field->add_to_search_marc_maps( > { > facet => 0, > suggestible => 0, >- sort => 0 >+ sort => 0, >+ filter => '', > } > ); > >@@ -76,7 +78,8 @@ $search_field->add_to_search_marc_maps( > { > facet => 0, > suggestible => 0, >- sort => 0 >+ sort => 0, >+ filter => '', > } > ); > >@@ -93,7 +96,8 @@ $search_field->add_to_search_marc_maps( > { > facet => 0, > suggestible => 1, >- sort => 0 >+ sort => 0, >+ filter => '', > } > ); > >diff --git a/t/db_dependent/Koha/SearchEngine/Elasticsearch/Search.t b/t/db_dependent/Koha/SearchEngine/Elasticsearch/Search.t >index 5e71ba5f31..d5b8790abe 100755 >--- a/t/db_dependent/Koha/SearchEngine/Elasticsearch/Search.t >+++ b/t/db_dependent/Koha/SearchEngine/Elasticsearch/Search.t >@@ -17,7 +17,8 @@ > > use Modern::Perl; > >-use Test::More tests => 14; >+use Test::More tests => 15; >+use Test::NoWarnings; > use t::lib::Mocks; > use t::lib::TestBuilder; > >diff --git a/t/db_dependent/Letters.t b/t/db_dependent/Letters.t >index 4edf7c80f5..5cdf9b289d 100755 >--- a/t/db_dependent/Letters.t >+++ b/t/db_dependent/Letters.t >@@ -19,7 +19,8 @@ > > use Modern::Perl; > use File::Basename qw(dirname); >-use Test::More tests => 104; >+use Test::More tests => 105; >+use Test::NoWarnings; > > use Test::MockModule; > use Test::Warn; >diff --git a/t/db_dependent/Overdues.t b/t/db_dependent/Overdues.t >index 979f378a25..f17ee28d18 100755 >--- a/t/db_dependent/Overdues.t >+++ b/t/db_dependent/Overdues.t >@@ -1,7 +1,8 @@ > #!/usr/bin/perl; > > use Modern::Perl; >-use Test::More tests => 18; >+use Test::More tests => 19; >+use Test::NoWarnings; > use Test::Warn; > > use C4::Context; >@@ -549,10 +550,9 @@ subtest 'UpdateFine tests' => sub { > { order_by => { '-asc' => 'accountlines_id' } } > ); > is( $fines->count, 4, "New amount should be 0 so no fine added" ); >- ok( >- C4::Circulation::AddReturn( $item_1->barcode, $item_1->homebranch, 1 ), >- "Returning the item and forgiving fines succeeds" >- ); >+ t::lib::Mocks::mock_userenv( { patron => $patron_1 } ); >+ my @r = C4::Circulation::AddReturn( $item_1->barcode, $item_1->homebranch, 1 ); >+ is( $r[1]->{WasReturned}, 1, "Returning the item and forgiving fines succeeds" ); > > t::lib::Mocks::mock_preference( 'MaxFine', 0 ); > >diff --git a/t/db_dependent/Reserves.t b/t/db_dependent/Reserves.t >index a922452815..cced1feee7 100755 >--- a/t/db_dependent/Reserves.t >+++ b/t/db_dependent/Reserves.t >@@ -17,7 +17,8 @@ > > use Modern::Perl; > >-use Test::More tests => 70; >+use Test::More tests => 71; >+use Test::NoWarnings; > use Test::MockModule; > use Test::Warn; > >@@ -413,7 +414,14 @@ is( > $which_highest->{reserve_id}, $reserve_id, > 'CheckReserves returns higher priority future reserve with sufficient lookahead' > ); >-ModReserve( { reserve_id => $now_reserve_id, rank => 'del', cancellation_reason => 'test reserve' } ); >+ >+{ >+ # Prevent warning 'No reserves HOLD_CANCELLATION letter transported by email' >+ my $mock_letters = Test::MockModule->new('C4::Letters'); >+ $mock_letters->mock( 'GetPreparedLetter', sub { return } ); >+ >+ ModReserve( { reserve_id => $now_reserve_id, rank => 'del', cancellation_reason => 'test reserve' } ); >+} > > # End of tests for bug 9761 (ConfirmFutureHolds) > >diff --git a/t/db_dependent/Reserves/CancelExpiredReserves.t b/t/db_dependent/Reserves/CancelExpiredReserves.t >index b9713ebd14..2cc84788ed 100755 >--- a/t/db_dependent/Reserves/CancelExpiredReserves.t >+++ b/t/db_dependent/Reserves/CancelExpiredReserves.t >@@ -1,7 +1,9 @@ > #!/usr/bin/perl > > use Modern::Perl; >-use Test::More tests => 4; >+use Test::More tests => 5; >+use Test::NoWarnings; >+use Test::MockModule; > > use t::lib::Mocks; > use t::lib::TestBuilder; >@@ -240,7 +242,13 @@ subtest 'Test handling of cancellation reason if passed' => sub { > ); > my $reserve_id = $reserve->{reserve_id}; > my $count = Koha::Holds->search->count; >- CancelExpiredReserves("EXPIRED"); >+ { >+ # Prevent warning 'No reserves HOLD_CANCELLATION letter transported by email' >+ my $mock_letters = Test::MockModule->new('C4::Letters'); >+ $mock_letters->mock( 'GetPreparedLetter', sub { return } ); >+ >+ CancelExpiredReserves("EXPIRED"); >+ } > is( Koha::Holds->search->count, $count - 1, "Hold is cancelled when reason is passed" ); > my $old_reserve = Koha::Old::Holds->find($reserve_id); > is( $old_reserve->cancellation_reason, 'EXPIRED', "Hold cancellation_reason was set correctly" ); >diff --git a/t/db_dependent/SIP/Transaction.t b/t/db_dependent/SIP/Transaction.t >index a07033def5..b121b9c7c4 100755 >--- a/t/db_dependent/SIP/Transaction.t >+++ b/t/db_dependent/SIP/Transaction.t >@@ -983,7 +983,7 @@ subtest do_checkout_with_patron_blocked => sub { > }; > > subtest do_checkout_with_noblock => sub { >- plan tests => 1; >+ plan tests => 3; > > my $mockILS = Test::MockObject->new; > my $server = { ils => $mockILS }; >@@ -1033,6 +1033,25 @@ subtest do_checkout_with_noblock => sub { > $patron->checkouts->count, > 1, 'No Block checkout was performed for debarred patron' > ); >+ >+ # Test Bug 32934: SIP checkout with no_block_due_date should honor the specified due date >+ $sip_patron = C4::SIP::ILS::Patron->new( $patron->cardnumber ); >+ my $sip_item_2 = C4::SIP::ILS::Item->new( $item->barcode ); >+ my $transaction = C4::SIP::ILS::Transaction::Checkout->new(); >+ >+ $transaction->patron($sip_patron); >+ $transaction->item($sip_item_2); >+ >+ # Set no_block_due_date to test the fix (YYYYMMDDZZZZHHMMSS) >+ my $expected_due_date = '20250115 000000'; >+ my $checkout_result = $transaction->do_checkout( undef, $expected_due_date ); >+ >+ my $checkout_obj = $patron->checkouts->find( { itemnumber => $item->itemnumber } ); >+ isnt( $checkout_obj, undef, 'Checkout object exists after no block checkout' ); >+ like( >+ dt_from_string( $checkout_obj->date_due )->ymd(''), qr/^20250115/, >+ 'No block due date is honored in SIP checkout' >+ ); > }; > > subtest do_checkout_with_holds => sub { >diff --git a/t/db_dependent/Search.t b/t/db_dependent/Search.t >index 9249bca11d..beaf6cb43a 100755 >--- a/t/db_dependent/Search.t >+++ b/t/db_dependent/Search.t >@@ -170,6 +170,8 @@ $contextmodule->mock( > return q{}; > } elsif ( $pref eq 'COinSinOPACResults' ) { > return q{}; >+ } elsif ( $pref eq 'FacetSortingLocale' ) { >+ return 'default'; > } else { > warn > "The syspref $pref was requested but I don't know what to say; this indicates that the test requires updating" >diff --git a/t/db_dependent/Search_FacetSorting.t b/t/db_dependent/Search_FacetSorting.t >new file mode 100755 >index 0000000000..a65abc9171 >--- /dev/null >+++ b/t/db_dependent/Search_FacetSorting.t >@@ -0,0 +1,119 @@ >+#!/usr/bin/perl >+ >+# This file is part of Koha. >+# >+# Koha 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 3 of the License, or >+# (at your option) any later version. >+# >+# Koha 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 Koha; if not, see <http://www.gnu.org/licenses>. >+ >+use Modern::Perl; >+ >+use Test::NoWarnings; >+use Test::More tests => 3; >+use Test::MockModule; >+use t::lib::Mocks; >+use C4::Search; >+ >+use utf8; >+ >+subtest '_sort_facets_zebra with system preference' => sub { >+ plan tests => 3; >+ >+ my $facets = _get_facets(); >+ >+ # Test with explicit locale parameter >+ my $sorted_facets_explicit = C4::Search::_sort_facets_zebra( $facets, 'default' ); >+ my $expected = [ >+ { facet_label_value => 'Ãberg, Erik' }, >+ { facet_label_value => 'ari' }, >+ { facet_label_value => 'Ari' }, >+ { facet_label_value => 'Ãuthor' }, >+ { facet_label_value => 'étienne' }, >+ { facet_label_value => 'fairy' }, >+ { facet_label_value => 'Fairy' }, >+ { facet_label_value => 'harry' }, >+ { facet_label_value => 'Harry' }, >+ { facet_label_value => 'mary' }, >+ { facet_label_value => 'Mary' }, >+ { facet_label_value => 'Å ostakovitÅ¡, Dmitri' }, >+ { facet_label_value => 'Zambidis' }, >+ ]; >+ is_deeply( $sorted_facets_explicit, $expected, "Zebra facets sorted correctly with explicit locale" ); >+ >+ # Test with system preference >+ t::lib::Mocks::mock_preference( 'FacetSortingLocale', 'default' ); >+ my $sorted_facets_syspref = C4::Search::_sort_facets_zebra($facets); >+ is_deeply( $sorted_facets_syspref, $expected, "Zebra facets sorted correctly with system preference" ); >+ >+ # Test fallback behavior >+ t::lib::Mocks::mock_preference( 'FacetSortingLocale', '' ); >+ my $sorted_facets_fallback = C4::Search::_sort_facets_zebra($facets); >+ is( ref($sorted_facets_fallback), 'ARRAY', "Zebra facets sorting fallback works" ); >+}; >+ >+subtest '_sort_facets_zebra with fi_FI locale' => sub { >+ plan tests => 1; >+ my $locale_map = _get_locale_map(); >+SKIP: { >+ skip( "fi_FI.utf8 locale not available on this system", 1 ) unless $locale_map->{"fi_FI.utf8"}; >+ >+ my $facets = _get_facets(); >+ >+ # Test with explicit locale parameter >+ my $sorted_facets_explicit = C4::Search::_sort_facets_zebra( $facets, 'fi_FI' ); >+ my $expected = [ >+ { facet_label_value => 'ari' }, >+ { facet_label_value => 'Ari' }, >+ { facet_label_value => 'étienne' }, >+ { facet_label_value => 'fairy' }, >+ { facet_label_value => 'Fairy' }, >+ { facet_label_value => 'harry' }, >+ { facet_label_value => 'Harry' }, >+ { facet_label_value => 'mary' }, >+ { facet_label_value => 'Mary' }, >+ { facet_label_value => 'Å ostakovitÅ¡, Dmitri' }, >+ { facet_label_value => 'Zambidis' }, >+ { facet_label_value => 'Ãberg, Erik' }, >+ { facet_label_value => 'Ãuthor' }, >+ ]; >+ is_deeply( $sorted_facets_explicit, $expected, "Zebra facets sorted correctly with explicit locale" ); >+ } >+}; >+ >+sub _get_facets { >+ my $facets = [ >+ { facet_label_value => 'Mary' }, >+ { facet_label_value => 'Harry' }, >+ { facet_label_value => 'Fairy' }, >+ { facet_label_value => 'Ari' }, >+ { facet_label_value => 'mary' }, >+ { facet_label_value => 'harry' }, >+ { facet_label_value => 'Ãberg, Erik' }, >+ { facet_label_value => 'Ãuthor' }, >+ { facet_label_value => 'fairy' }, >+ { facet_label_value => 'ari' }, >+ { facet_label_value => 'étienne' }, >+ { facet_label_value => 'Å ostakovitÅ¡, Dmitri' }, >+ { facet_label_value => 'Zambidis' }, >+ ]; >+ return $facets; >+} >+ >+sub _get_locale_map { >+ my $map = {}; >+ my @locales = `locale -a`; >+ foreach my $locale (@locales) { >+ chomp($locale); >+ $map->{$locale} = 1; >+ } >+ return $map; >+} >diff --git a/t/db_dependent/api/v1/biblios.t b/t/db_dependent/api/v1/biblios.t >index b3a457b269..e337c37604 100755 >--- a/t/db_dependent/api/v1/biblios.t >+++ b/t/db_dependent/api/v1/biblios.t >@@ -20,7 +20,8 @@ use Modern::Perl; > use utf8; > use Encode; > >-use Test::More tests => 15; >+use Test::More tests => 16; >+use Test::NoWarnings; > use Test::MockModule; > use Test::Mojo; > use Test::Warn; >@@ -1889,7 +1890,7 @@ subtest 'list() tests' => sub { > > subtest 'add_item() tests' => sub { > >- plan tests => 8; >+ plan tests => 10; > > $schema->storage->txn_begin; > >@@ -1939,17 +1940,20 @@ subtest 'add_item() tests' => sub { > > my $item = $builder->build_sample_item(); > >- $t->post_ok( >- "//$userid:$password@/api/v1/biblios/$biblio_id/items" => json => { >- external_id => $item->barcode, >- } >- )->status_is( 409, 'Duplicate barcode' ); >+ warnings_like { >+ $t->post_ok( >+ "//$userid:$password@/api/v1/biblios/$biblio_id/items" => json => { >+ external_id => $item->barcode, >+ } >+ )->status_is( 409, 'Duplicate barcode' )->json_is( "/error" => "Duplicate barcode." ); >+ } >+ qr{DBD::mysql::st execute failed: Duplicate entry '(.*?)' for key '(.*\.?)itembarcodeidx'}; > > $schema->storage->txn_rollback; > }; > > subtest 'update_item() tests' => sub { >- plan tests => 7; >+ plan tests => 9; > > $schema->storage->txn_begin; > >@@ -1993,11 +1997,14 @@ subtest 'update_item() tests' => sub { > > my $other_item = $builder->build_sample_item(); > >- $t->put_ok( >- "//$userid:$password@/api/v1/biblios/$biblio_id/items/$item_id" => json => { >- external_id => $other_item->barcode, >- } >- )->status_is( 409, 'Barcode not unique' ); >+ warnings_like { >+ $t->put_ok( >+ "//$userid:$password@/api/v1/biblios/$biblio_id/items/$item_id" => json => { >+ external_id => $other_item->barcode, >+ } >+ )->status_is( 409, 'Barcode not unique' )->json_is( "/error" => "Duplicate barcode." ); >+ } >+ qr{DBD::mysql::st execute failed: Duplicate entry '(.*?)' for key '(.*\.?)itembarcodeidx'}; > > $t->put_ok( > "//$userid:$password@/api/v1/biblios/$biblio_id/items/$item_id" => json => { >diff --git a/t/db_dependent/api/v1/holds.t b/t/db_dependent/api/v1/holds.t >index a07374d751..728de95644 100755 >--- a/t/db_dependent/api/v1/holds.t >+++ b/t/db_dependent/api/v1/holds.t >@@ -17,7 +17,8 @@ > > use Modern::Perl; > >-use Test::More tests => 15; >+use Test::More tests => 17; >+use Test::NoWarnings; > use Test::MockModule; > use Test::Mojo; > use t::lib::TestBuilder; >@@ -37,6 +38,7 @@ use Koha::Biblios; > use Koha::Biblioitems; > use Koha::Items; > use Koha::CirculationRules; >+use Koha::Patron::Debarments qw(AddDebarment); > > my $schema = Koha::Database->new->schema; > my $builder = t::lib::TestBuilder->new(); >@@ -198,6 +200,10 @@ subtest "Test endpoints with permission" => sub { > > plan tests => 45; > >+ # Prevent warning 'No reserves HOLD_CANCELLATION letter transported by email' >+ my $mock_letters = Test::MockModule->new('C4::Letters'); >+ $mock_letters->mock( 'GetPreparedLetter', sub { return } ); >+ > $t->get_ok("//$userid_1:$password@/api/v1/holds")->status_is(200)->json_has('/0')->json_has('/1')->json_hasnt('/2'); > > $t->get_ok("//$userid_1:$password@/api/v1/holds?priority=2")->status_is(200) >@@ -697,8 +703,12 @@ subtest 'add() tests (maxreserves behaviour)' => sub { > item_id => $item_3->itemnumber > }; > >- $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $post_data )->status_is(403) >- ->json_is( { error => 'Hold cannot be placed. Reason: tooManyReserves' } ); >+ $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $post_data )->status_is(409)->json_is( >+ { >+ error => 'Hold cannot be placed. Reason: hold_limit', >+ error_code => 'hold_limit' >+ } >+ ); > > t::lib::Mocks::mock_preference( 'maxreserves', 0 ); > >@@ -717,6 +727,248 @@ subtest 'add() tests (maxreserves behaviour)' => sub { > $schema->storage->txn_rollback; > }; > >+subtest 'add() + can_place_holds() tests' => sub { >+ >+ plan tests => 7; >+ >+ $schema->storage->txn_begin; >+ >+ my $password = 'AbcdEFG123'; >+ >+ my $library = $builder->build_object( { class => 'Koha::Libraries', value => { pickup_location => 1 } } ); >+ my $staff = $builder->build_object( { class => 'Koha::Patrons', value => { flags => 1 } } ); >+ $staff->set_password( { password => $password, skip_validation => 1 } ); >+ my $userid = $staff->userid; >+ >+ subtest "'expired' tests" => sub { >+ >+ plan tests => 5; >+ >+ t::lib::Mocks::mock_preference( 'BlockExpiredPatronOpacActions', 'hold' ); >+ >+ my $patron = $builder->build_object( >+ { >+ class => 'Koha::Patrons', >+ value => { dateexpiry => \'DATE_ADD(NOW(), INTERVAL -1 DAY)' } >+ } >+ ); >+ >+ my $item = $builder->build_sample_item( { library => $library->id } ); >+ >+ my $post_data = { >+ patron_id => $patron->id, >+ pickup_library_id => $item->holdingbranch, >+ item_id => $item->id >+ }; >+ >+ $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $post_data ) >+ ->status_is( 409, "Expected error related to 'expired'" ) >+ ->json_is( { error => "Hold cannot be placed. Reason: expired", error_code => 'expired' } ); >+ >+ $t->post_ok( "//$userid:$password@/api/v1/holds" => { 'x-koha-override' => 'expired' } => json => $post_data ) >+ ->status_is( 201, "Override works for 'expired'" ); >+ }; >+ >+ subtest "'debt_limit' tests" => sub { >+ >+ plan tests => 7; >+ >+ # Add a patron, making sure it is not (yet) expired >+ my $patron = $builder->build_object( { class => 'Koha::Patrons', } ); >+ $patron->account->add_debit( { amount => 10, interface => 'opac', type => 'ACCOUNT' } ); >+ >+ my $item = $builder->build_sample_item( { library => $library->id } ); >+ >+ my $post_data = { >+ patron_id => $patron->id, >+ pickup_library_id => $item->holdingbranch, >+ item_id => $item->id >+ }; >+ >+ t::lib::Mocks::mock_preference( 'maxoutstanding', '5' ); >+ >+ $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $post_data ) >+ ->status_is( 409, "Expected error related to 'debt_limit'" ) >+ ->json_is( { error => "Hold cannot be placed. Reason: debt_limit", error_code => 'debt_limit' } ); >+ >+ my $hold_id = >+ $t->post_ok( >+ "//$userid:$password@/api/v1/holds" => { 'x-koha-override' => 'debt_limit' } => json => $post_data ) >+ ->status_is( 201, "Override works for 'debt_limit'" )->tx->res->json->{hold_id}; >+ >+ Koha::Holds->find($hold_id)->delete(); >+ >+ t::lib::Mocks::mock_preference( 'maxoutstanding', undef ); >+ >+ $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $post_data ) >+ ->status_is( 201, "No 'maxoutstanding', can place holds" ); >+ }; >+ >+ subtest "'bad_address' tests" => sub { >+ >+ plan tests => 5; >+ >+ # Add a patron, making sure it is not (yet) expired >+ my $patron = $builder->build_object( >+ { >+ class => 'Koha::Patrons', >+ value => { >+ gonenoaddress => 1, >+ } >+ } >+ ); >+ >+ my $item = $builder->build_sample_item( { library => $library->id } ); >+ >+ my $post_data = { >+ patron_id => $patron->id, >+ pickup_library_id => $item->holdingbranch, >+ item_id => $item->id >+ }; >+ >+ $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $post_data ) >+ ->status_is( 409, "Expected error related to 'bad_address'" ) >+ ->json_is( { error => "Hold cannot be placed. Reason: bad_address", error_code => 'bad_address' } ); >+ >+ $t->post_ok( >+ "//$userid:$password@/api/v1/holds" => { 'x-koha-override' => 'bad_address' } => json => $post_data ) >+ ->status_is( 201, "Override works for 'bad_address'" ); >+ }; >+ >+ subtest "'card_lost' tests" => sub { >+ >+ plan tests => 5; >+ >+ # Add a patron, making sure it is not (yet) expired >+ my $patron = $builder->build_object( >+ { >+ class => 'Koha::Patrons', >+ value => { >+ lost => 1, >+ } >+ } >+ ); >+ >+ my $item = $builder->build_sample_item( { library => $library->id } ); >+ >+ my $post_data = { >+ patron_id => $patron->id, >+ pickup_library_id => $item->holdingbranch, >+ item_id => $item->id >+ }; >+ >+ $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $post_data ) >+ ->status_is( 409, "Expected error related to 'card_lost'" ) >+ ->json_is( { error => "Hold cannot be placed. Reason: card_lost", error_code => 'card_lost' } ); >+ >+ $t->post_ok( "//$userid:$password@/api/v1/holds" => { 'x-koha-override' => 'card_lost' } => json => $post_data ) >+ ->status_is( 201, "Override works for 'card_lost'" ); >+ }; >+ >+ subtest "'restricted' tests" => sub { >+ >+ plan tests => 5; >+ >+ # Add a patron, making sure it is not (yet) expired >+ my $patron = $builder->build_object( { class => 'Koha::Patrons' } ); >+ AddDebarment( { borrowernumber => $patron->borrowernumber } ); >+ $patron->discard_changes(); >+ >+ my $item = $builder->build_sample_item( { library => $library->id } ); >+ >+ my $post_data = { >+ patron_id => $patron->id, >+ pickup_library_id => $item->holdingbranch, >+ item_id => $item->id >+ }; >+ >+ $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $post_data ) >+ ->status_is( 409, "Expected error related to 'restricted'" ) >+ ->json_is( { error => "Hold cannot be placed. Reason: restricted", error_code => 'restricted' } ); >+ >+ $t->post_ok( >+ "//$userid:$password@/api/v1/holds" => { 'x-koha-override' => 'restricted' } => json => $post_data ) >+ ->status_is( 201, "Override works for 'restricted'" ); >+ }; >+ >+ subtest "'hold_limit' tests" => sub { >+ >+ plan tests => 7; >+ >+ # Add a patron, making sure it is not (yet) expired >+ my $patron = $builder->build_object( { class => 'Koha::Patrons' } ); >+ >+ # Add a hold >+ $builder->build_object( { class => 'Koha::Holds', value => { borrowernumber => $patron->borrowernumber } } ); >+ >+ t::lib::Mocks::mock_preference( 'maxreserves', 1 ); >+ >+ my $item = $builder->build_sample_item( { library => $library->id } ); >+ >+ my $post_data = { >+ patron_id => $patron->id, >+ pickup_library_id => $item->holdingbranch, >+ item_id => $item->id >+ }; >+ >+ $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $post_data ) >+ ->status_is( 409, "Expected error related to 'hold_limit'" ) >+ ->json_is( { error => "Hold cannot be placed. Reason: hold_limit", error_code => 'hold_limit' } ); >+ >+ my $hold_id = $t->post_ok( >+ "//$userid:$password@/api/v1/holds" => { 'x-koha-override' => 'hold_limit' } => json => $post_data ) >+ ->status_is( 201, "Override works for 'hold_limit'" )->tx->res->json->{hold_id}; >+ >+ Koha::Holds->find($hold_id)->delete(); >+ >+ t::lib::Mocks::mock_preference( 'maxreserves', undef ); >+ >+ $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $post_data ) >+ ->status_is( 201, "No 'max_reserves', can place holds" ); >+ }; >+ >+ subtest "Multiple blocking conditions tests" => sub { >+ >+ plan tests => 8; >+ >+ t::lib::Mocks::mock_preference( 'BlockExpiredPatronOpacActions', 'hold' ); >+ >+ my $patron = $builder->build_object( >+ { >+ class => 'Koha::Patrons', >+ value => { dateexpiry => \'DATE_ADD(NOW(), INTERVAL -1 DAY)' } >+ } >+ ); >+ >+ t::lib::Mocks::mock_preference( 'maxoutstanding', '5' ); >+ $patron->account->add_debit( { amount => 10, interface => 'opac', type => 'ACCOUNT' } ); >+ >+ my $item = $builder->build_sample_item( { library => $library->id } ); >+ >+ my $post_data = { >+ patron_id => $patron->id, >+ pickup_library_id => $item->holdingbranch, >+ item_id => $item->id >+ }; >+ >+ ## NOTICE: this tests rely on 'expired' being checked before 'debt_limit' in Koha::Patron->can_place_holds >+ >+ # patron meets 'expired' and 'debt_limit' >+ $t->post_ok( "//$userid:$password@/api/v1/holds" => json => $post_data ) >+ ->status_is( 409, "Expected error related to 'expired'" ) >+ ->json_is( { error => "Hold cannot be placed. Reason: expired", error_code => 'expired' } ); >+ >+ # patron meets 'expired' AND 'debt_limit' >+ $t->post_ok( "//$userid:$password@/api/v1/holds" => { 'x-koha-override' => 'expired' } => json => $post_data ) >+ ->status_is( 409, "Expected error related to 'debt_limit'" ) >+ ->json_is( { error => "Hold cannot be placed. Reason: debt_limit", error_code => 'debt_limit' } ); >+ >+ $t->post_ok( >+ "//$userid:$password@/api/v1/holds" => { 'x-koha-override' => 'expired,debt_limit' } => json => $post_data ) >+ ->status_is( 201, "Override works for both 'expired' and 'debt_limit'" ); >+ }; >+}; >+ > subtest 'pickup_locations() tests' => sub { > > plan tests => 15; >diff --git a/t/db_dependent/api/v1/two_factor_auth.t b/t/db_dependent/api/v1/two_factor_auth.t >index 1ddc0cd4b5..ddb9178e3a 100755 >--- a/t/db_dependent/api/v1/two_factor_auth.t >+++ b/t/db_dependent/api/v1/two_factor_auth.t >@@ -17,7 +17,8 @@ > > use Modern::Perl; > >-use Test::More tests => 2; >+use Test::More tests => 3; >+use Test::NoWarnings; > use Test::Mojo; > use Test::MockModule; > >diff --git a/t/db_dependent/selenium/basic_workflow.t b/t/db_dependent/selenium/basic_workflow.t >index b1134aeb54..dc96e9125b 100755 >--- a/t/db_dependent/selenium/basic_workflow.t >+++ b/t/db_dependent/selenium/basic_workflow.t >@@ -32,6 +32,9 @@ > > use Modern::Perl; > >+use Test::More tests => 23; >+use Test::NoWarnings; >+ > use Time::HiRes qw(gettimeofday); > use POSIX qw(strftime); > use C4::Context; >@@ -39,7 +42,6 @@ use C4::Biblio qw( AddBiblio ); > > use Koha::CirculationRules; > >-use Test::More tests => 22; > use MARC::Record; > use MARC::Field; > >@@ -326,6 +328,7 @@ sub cleanup { > sub time_diff { > my $lib = shift; > my $now = gettimeofday; >- warn "CP $lib = " . sprintf( "%.2f", $now - $prev_time ) . "\n"; >+ >+ #warn "CP $lib = " . sprintf( "%.2f", $now - $prev_time ) . "\n"; > $prev_time = $now; > } >diff --git a/t/db_dependent/selenium/opac_ill_requests.t b/t/db_dependent/selenium/opac_ill_requests.t >index 5f022dbe9b..20f1d49b3d 100755 >--- a/t/db_dependent/selenium/opac_ill_requests.t >+++ b/t/db_dependent/selenium/opac_ill_requests.t >@@ -53,14 +53,15 @@ SKIP: { > 'Correctly in search results page' > ); > >+ # Get the second li, the first one is "Make a purchase suggestion" > is( >- $driver->find_element('//div[@class="suggestion"]/ul/li')->get_text, >+ $driver->find_element('//div[@class="suggestion"]/ul/li[2]')->get_text, > 'Make an interlibrary loan request', > 'Placing an ILL request through the OPAC is allowed', > ); > > # Clicking on the search results page link works >- $driver->find_element('//div[@class="suggestion"]/ul/li/a')->click; >+ $driver->find_element('//div[@class="suggestion"]/ul/li[2]/a')->click; > is( > $driver->find_element('(//nav[@id="breadcrumbs"]/ol/li)[last()]')->get_text, > 'New interlibrary loan request', >@@ -94,8 +95,8 @@ SKIP: { > > is( > scalar @{$link_exists}, >- 0, >- 'Search page - Place ILL request link should be absent. ' >+ 2, >+ 'Search page - Place ILL request link should be present. ' > ); > > # Visiting the create request page directly does not work >diff --git a/t/db_dependent/selenium/regressions.t b/t/db_dependent/selenium/regressions.t >index a55d9b2876..790cda2383 100755 >--- a/t/db_dependent/selenium/regressions.t >+++ b/t/db_dependent/selenium/regressions.t >@@ -17,11 +17,13 @@ > > use Modern::Perl; > use utf8; >+use Encode qw(encode_utf8); > > use C4::Context; > > use Test::More; > use Test::MockModule; >+use Test::NoWarnings; > > use C4::Context; > use C4::Biblio qw( AddBiblio ); >@@ -36,7 +38,7 @@ eval { require Selenium::Remote::Driver; }; > if ($@) { > plan skip_all => "Selenium::Remote::Driver is needed for selenium tests."; > } else { >- plan tests => 9; >+ plan tests => 10; > } > > my $s = t::lib::Selenium->new; >@@ -354,7 +356,10 @@ subtest 'Encoding in session variables' => sub { > is( > $driver->find_element('//span[@class="logged-in-branch-name"]')->get_text(), > $branchname, >- sprintf( "logged-in-branch-name set - SessionStorage=%s, branchname=%s", $SessionStorage, $branchname ) >+ sprintf( >+ "logged-in-branch-name set - SessionStorage=%s, branchname=%s", $SessionStorage, >+ encode_utf8($branchname) >+ ) > ); > > $driver->find_element('//input[@id="barcode"]')->send_keys( $item->barcode ); >@@ -372,7 +377,7 @@ subtest 'Encoding in session variables' => sub { > $branchname, > sprintf( > "'Checked out from' column should contain the branchname - SessionStorage=%s, branchname=%s", >- $SessionStorage, $branchname >+ $SessionStorage, encode_utf8($branchname) > ) > ); > >diff --git a/t/db_dependent/www/auth_values_input_www.t b/t/db_dependent/www/auth_values_input_www.t >index 2a93f7aff9..f42c5f44f9 100755 >--- a/t/db_dependent/www/auth_values_input_www.t >+++ b/t/db_dependent/www/auth_values_input_www.t >@@ -19,8 +19,8 @@ use Modern::Perl; > > use utf8; > use Test::More; #See plan tests => \d+ below >+use Test::NoWarnings; > use Test::WWW::Mechanize; >-use XML::Simple; > use JSON; > use File::Basename; > use File::Spec; >@@ -32,11 +32,8 @@ use Koha::AuthorisedValueCategories; > > my $testdir = File::Spec->rel2abs( dirname(__FILE__) ); > >-my $koha_conf = $ENV{KOHA_CONF}; >-my $xml = XMLin($koha_conf); >- >-my $user = $ENV{KOHA_USER} || $xml->{config}->{user}; >-my $password = $ENV{KOHA_PASS} || $xml->{config}->{pass}; >+my $user = $ENV{KOHA_USER} || 'koha'; >+my $password = $ENV{KOHA_PASS} || 'koha'; > my $intranet = $ENV{KOHA_INTRANET_URL}; > > eval { use C4::Context; }; >@@ -45,7 +42,7 @@ if ($@) { > } elsif ( not defined $intranet ) { > plan skip_all => "Tests skip. You must set env. variable KOHA_INTRANET_URL to do tests\n"; > } else { >- plan tests => 30; >+ plan tests => 31; > } > > my $dbh = C4::Context->dbh; >diff --git a/t/db_dependent/www/batch.t b/t/db_dependent/www/batch.t >index 73a41cbb92..8add67d1e3 100755 >--- a/t/db_dependent/www/batch.t >+++ b/t/db_dependent/www/batch.t >@@ -20,8 +20,8 @@ use Modern::Perl; > > use utf8; > use Test::More; #See plan tests => \d+ below >+use Test::NoWarnings; > use Test::WWW::Mechanize; >-use XML::Simple; > use JSON; > use File::Basename; > use File::Spec; >@@ -31,9 +31,6 @@ use Koha::BackgroundJobs; > > my $testdir = File::Spec->rel2abs( dirname(__FILE__) ); > >-my $koha_conf = $ENV{KOHA_CONF}; >-my $xml = XMLin($koha_conf); >- > use C4::Context; > my $marcflavour = C4::Context->preference('marcflavour') || 'MARC21'; > >@@ -42,8 +39,8 @@ my $file = > ? "$testdir/data/unimarcrecord.mrc" > : "$testdir/data/marc21record.mrc"; > >-my $user = $ENV{KOHA_USER} || $xml->{config}->{user}; >-my $password = $ENV{KOHA_PASS} || $xml->{config}->{pass}; >+my $user = $ENV{KOHA_USER} || 'koha'; >+my $password = $ENV{KOHA_PASS} || 'koha'; > my $intranet = $ENV{KOHA_INTRANET_URL}; > > if ( not defined $intranet ) { >@@ -52,7 +49,7 @@ if ( not defined $intranet ) { > . "KOHA_CONF set, you must also set KOHA_USER and KOHA_PASS for " > . "your username and password"; > } else { >- plan tests => 24; >+ plan tests => 25; > } > > $intranet =~ s#/$##; >diff --git a/t/db_dependent/www/history.t b/t/db_dependent/www/history.t >index af83692647..c0501cfb42 100755 >--- a/t/db_dependent/www/history.t >+++ b/t/db_dependent/www/history.t >@@ -18,17 +18,14 @@ > use Modern::Perl; > > use utf8; >-use XML::Simple; > use Encode; > > use Test::More; #See plan tests => \d+ below >+use Test::NoWarnings; > use Test::WWW::Mechanize; > >-my $koha_conf = $ENV{KOHA_CONF}; >-my $xml = XMLin($koha_conf); >- >-my $user = $ENV{KOHA_USER} || $xml->{config}->{user}; >-my $password = $ENV{KOHA_PASS} || $xml->{config}->{pass}; >+my $user = $ENV{KOHA_USER} || 'koha'; >+my $password = $ENV{KOHA_PASS} || 'koha'; > my $intranet = $ENV{KOHA_INTRANET_URL}; > > eval { use C4::Context; }; >@@ -37,7 +34,7 @@ if ($@) { > } elsif ( not defined $intranet ) { > plan skip_all => "Tests skip. You must set env. variable KOHA_INTRANET_URL to do tests\n"; > } else { >- plan tests => 4; >+ plan tests => 5; > } > > $intranet =~ s#/$##; >diff --git a/t/db_dependent/www/search_utf8.t b/t/db_dependent/www/search_utf8.t >index 3295eedc87..3629c576ad 100755 >--- a/t/db_dependent/www/search_utf8.t >+++ b/t/db_dependent/www/search_utf8.t >@@ -19,9 +19,9 @@ use Modern::Perl; > > use utf8; > use Test::More; #See plan tests => \d+ below >+use Test::NoWarnings; > use Test::WWW::Mechanize; > use Data::Dumper; >-use XML::Simple; > use File::Basename qw(dirname ); > use POSIX; > use Encode; >@@ -64,7 +64,7 @@ if ( not $intranet ) { > elsif ( not $opac ) { > plan skip_all => "Tests skip. You must set env. variable KOHA_OPAC_URL to do tests\n"; > } else { >- plan tests => 80; >+ plan tests => 81; > } > > $intranet =~ s#/$##; >@@ -94,8 +94,8 @@ if ( not defined $mock_zebra->{indexer_pid} ) { > our $agent = Test::WWW::Mechanize->new( autocheck => 1 ); > $agent->get_ok( "$intranet/cgi-bin/koha/mainpage.pl", 'connect to intranet' ); > $agent->form_name('loginform'); >-$agent->field( 'login_userid', $ENV{KOHA_PASS} ); >-$agent->field( 'login_password', $ENV{KOHA_USER} ); >+$agent->field( 'login_userid', $ENV{KOHA_PASS} || 'koha' ); >+$agent->field( 'login_password', $ENV{KOHA_USER} || 'koha' ); > $agent->field( 'branch', '' ); > $agent->click_ok( '', 'login to staff interface' ); > >diff --git a/t/dummy.t b/t/dummy.t >deleted file mode 100755 >index 3eb62d4679..0000000000 >--- a/t/dummy.t >+++ /dev/null >@@ -1,4 +0,0 @@ >-# Dummy test until Test::Harness or similar >-# is used by the other tests to check deps. >-use Modern::Perl; >-print "1..1\nok 1\n"; >diff --git a/t/lib/TestBuilder.pm b/t/lib/TestBuilder.pm >index 58ce08c5b8..71a86984f2 100644 >--- a/t/lib/TestBuilder.pm >+++ b/t/lib/TestBuilder.pm >@@ -658,9 +658,10 @@ sub _gen_default_values { > reservefee => 0, > > # Not X, used for statistics >- category_type => sub { return [qw( A C S I P )]->[ int( rand(5) ) ] }, >- min_password_length => undef, >- require_strong_password => undef, >+ category_type => sub { return [qw( A C S I P )]->[ int( rand(5) ) ] }, >+ min_password_length => undef, >+ require_strong_password => undef, >+ BlockExpiredPatronOpacActions => q{follow_syspref_BlockExpiredPatronOpacActions}, > }, > Branch => { > pickup_location => 0, >diff --git a/tools/batchMod.pl b/tools/batchMod.pl >index 92f19d2929..a94db601a9 100755 >--- a/tools/batchMod.pl >+++ b/tools/batchMod.pl >@@ -33,6 +33,7 @@ use Koha::Database; > use Koha::Exception; > use Koha::Biblios; > use Koha::Items; >+use Koha::Object; > use Koha::Patrons; > use Koha::Item::Attributes; > use Koha::BackgroundJob::BatchDeleteItem; >@@ -112,13 +113,13 @@ if ( $op eq "cud-action" ) { > > } else { # modification > >- my @item_columns = Koha::Items->columns; >+ my $items_info = Koha::Items->_resultset->result_source->columns_info; > > my $new_item_data; > my ($columns_with_regex); > my @subfields_to_blank = $input->multi_param('disable_input'); > my @more_subfields = $input->multi_param("items.more_subfields_xml"); >- for my $item_column (@item_columns) { >+ for my $item_column ( keys %$items_info ) { > my @attributes = ($item_column); > my $cgi_param_prefix = 'items.'; > if ( $item_column eq 'more_subfields_xml' ) { >@@ -136,8 +137,20 @@ if ( $op eq "cud-action" ) { > > if ( grep { $cgi_var_name eq $_ } @subfields_to_blank ) { > >- # Empty this column >- $new_item_data->{$attr} = undef; >+ # Empty this column, check nullable and data_type >+ next if !$items_info->{$attr}; # skip this weird case >+ if ( $items_info->{$attr}->{is_nullable} ) { >+ $new_item_data->{$attr} = undef; >+ } elsif ( Koha::Object::_numeric_column_type( $items_info->{$attr}->{data_type} ) ) { >+ $new_item_data->{$attr} = 0; >+ } elsif ( Koha::Object::_date_or_datetime_column_type( $items_info->{$attr}->{data_type} ) ) { >+ >+ # TODO Currently, we do not have NOT NULL date(time) columns in items >+ warn "batchMod: $attr must be blanked, but does not accept NULL values"; >+ next; >+ } else { >+ $new_item_data->{$attr} = q{}; >+ } > } elsif ( my $regex_search = $input->param( $cgi_var_name . '_regex_search' ) ) { > $columns_with_regex->{$attr} = { > search => $regex_search, >diff --git a/xt/api.t b/xt/api.t >index 78cf588c24..3aa2e7f90f 100755 >--- a/xt/api.t >+++ b/xt/api.t >@@ -14,15 +14,18 @@ > > use Modern::Perl; > >-use Test::More tests => 6; >+use Test::More tests => 8; >+use Test::NoWarnings; > > use Test::Mojo; > use Data::Dumper; >+use Koha::Database; > > use FindBin(); > use IPC::Cmd qw(can_run); > use List::MoreUtils qw(any); > use File::Slurp qw(read_file); >+use YAML::XS qw(LoadFile); > > my $t = Test::Mojo->new('Koha::REST::V1'); > my $spec = $t->get_ok( '/api/v1/', 'Correctly fetched the spec' )->tx->res->json; >@@ -194,3 +197,49 @@ subtest 'POST (201) have location header' => sub { > } > } > }; >+ >+subtest 'maxlength + enum' => sub { >+ my $def_map = { >+ >+ # api def => schema >+ item => 'Item', >+ library => 'Branch', >+ patron => 'Borrower', >+ patron_category => 'Category', >+ }; >+ plan tests => scalar keys %$def_map; >+ my $schema = Koha::Database->new->schema; >+ while ( my ( $def, $dbic_src ) = each %$def_map ) { >+ my @failures; >+ my $definition = LoadFile("api/v1/swagger/definitions/$def.yaml"); >+ my $source = $schema->source($dbic_src); >+ my $object_class = Koha::Object::_get_object_class( $source->result_class ); >+ eval "require $object_class"; >+ my $koha_object = $object_class->new; >+ my $api_mapping = $koha_object->to_api_mapping; >+ my $reversed_api_mapping = >+ { reverse map { defined $api_mapping->{$_} ? ( $_ => $api_mapping->{$_} ) : () } keys %$api_mapping }; >+ >+ my $db_columns = $koha_object->_result->columns_info; >+ while ( my ( $db_col, $column_info ) = each %{$db_columns} ) { >+ my $api_attr = $api_mapping->{$db_col} || $db_col; >+ my $properties = $definition->{properties}->{$api_attr}; >+ >+ next unless $properties; >+ >+ next unless $column_info->{size}; >+ >+ next if ref( $column_info->{size} ) eq 'ARRAY'; # decimal # FIXME Could have a test for this as well >+ >+ next >+ if $properties->{enum} >+ ; # FIXME This is not fully correct, we might want to make sure enum is set for both DB and spec. eg. now checkprevcheckout is enum only for the api spec >+ >+ if ( !exists $properties->{maxLength} || $column_info->{size} != $properties->{maxLength} ) { >+ push @failures, sprintf "%s.%s should have maxLength=%s in api spec", $def, $api_attr, >+ $column_info->{size}; >+ } >+ } >+ is( scalar(@failures), 0, "maxLength tests for $def" ) or diag Dumper @failures; >+ } >+}; >diff --git a/xt/author/codespell.t b/xt/author/codespell.t >index 781d1acd97..e31799ca51 100755 >--- a/xt/author/codespell.t >+++ b/xt/author/codespell.t >@@ -3,24 +3,23 @@ use Modern::Perl; > use Test::PerlTidy; > use Test::More; > >+use Koha::Devel::Files; >+ > my $codespell_version = qx{codespell --version}; > chomp $codespell_version; > $codespell_version =~ s/-.*$//; > if ( ( $codespell_version =~ s/\.//gr ) < 220 ) { # if codespell < 2.2.0 > plan skip_all => "codespell version $codespell_version too low, need at least 2.2.0"; > } >+my $dev_files = Koha::Devel::Files->new( { context => 'codespell' } ); > my @files; >-push @files, >- qx{git ls-files '*.pl' '*.PL' '*.pm' '*.t' ':(exclude)installer/data/mysql/updatedatabase.pl' ':(exclude)installer/data/mysql/update22to30.pl' ':(exclude)installer/data/mysql/db_revs/241200035.pl' ':(exclude)misc/cronjobs/build_browser_and_cloud.pl'}; >-push @files, qx{git ls-files svc opac/svc}; # Files without extension >-push @files, qx{git ls-files '*.tt' '*.inc'}; >-push @files, >- qx{git ls-files '*.js' '*.ts' '*.vue' ':(exclude)koha-tmpl/intranet-tmpl/lib' ':(exclude)koha-tmpl/intranet-tmpl/js/Gettext.js' ':(exclude)koha-tmpl/opac-tmpl/lib' ':(exclude)koha-tmpl/opac-tmpl/bootstrap/js/Gettext.js'}; >+push @files, $dev_files->ls_perl_files; >+push @files, $dev_files->ls_tt_files; >+push @files, $dev_files->ls_js_files; > > plan tests => scalar @files; > > for my $file (@files) { >- chomp $file; > my $output = qx{codespell -d --ignore-words .codespell-ignore $file}; > chomp $output; > is( $output, q{} ); >diff --git a/xt/author/pod_checker.t b/xt/author/pod_checker.t >index b039664083..53e32ddf1b 100755 >--- a/xt/author/pod_checker.t >+++ b/xt/author/pod_checker.t >@@ -4,11 +4,10 @@ use Modern::Perl; > use Test::More; > use Test::NoWarnings; > use Pod::Checker; >+use Koha::Devel::Files; > >-my @files; >-push @files, qx{git ls-files '*.pl' '*.PL' '*.pm' '*.t'}; >-push @files, qx{git ls-files svc opac/svc}; # Files without extension >-chomp for @files; >+my $dev_files = Koha::Devel::Files->new; >+my @files = $dev_files->ls_perl_files; > > plan tests => scalar @files + 1; > >diff --git a/xt/author/podcorrectness.t b/xt/author/podcorrectness.t >index c30a0df74d..9f73784554 100755 >--- a/xt/author/podcorrectness.t >+++ b/xt/author/podcorrectness.t >@@ -15,9 +15,9 @@ use Modern::Perl; > use Test::More; > use Test::Pod; > >-my @files; >-push @files, qx{git ls-files '*.pl' '*.PL' '*.pm' '*.t'}; >-push @files, qx{git ls-files svc opac/svc}; # Files without extension >-chomp for @files; >+use Koha::Devel::Files; >+ >+my $dev_files = Koha::Devel::Files->new; >+my @files = $dev_files->ls_perl_files; > > all_pod_files_ok(@files); >diff --git a/xt/find-license-problems.t b/xt/find-license-problems.t >index 5f93516aa5..3c576b7a51 100755 >--- a/xt/find-license-problems.t >+++ b/xt/find-license-problems.t >@@ -25,34 +25,32 @@ > > use Modern::Perl; > use Test::More; >+use Test::NoWarnings; > >-use File::Spec; >-use File::Find; >+my @files = map { >+ chomp; >+ my $name = $_; >+ !( $name =~ m{^koha-tmpl/} >+ || $name =~ m{\.(gif|jpg|odt|ogg|pdf|png|po|psd|svg|swf|zip)$} >+ || $name =~ m{xt/find-license-problems|xt/fix-old-fsf-address|misc/translator/po2json} >+ || $name =~ m[t/mock_templates/intranet-tmpl/prog] >+ || !-f $name ) >+ ? $_ >+ : () >+} `git ls-tree -r HEAD --name-only`; # only files part of git > >-my @files; >- >-sub wanted { >- my $name = $File::Find::name; >- push @files, $name >- unless $name =~ /\/(\.git|koha-tmpl|node_modules|swagger-ui)(\/.*)?$/ >- || $name =~ /\.(gif|jpg|odt|ogg|pdf|png|po|psd|svg|swf|zip|patch)$/ >- || $name =~ m[(xt/find-license-problems|xt/fix-old-fsf-address|misc/translator/po2json)] >- || !-f $name; >-} >- >-find( { wanted => \&wanted, no_chdir => 1 }, File::Spec->curdir() ); >+plan tests => scalar(@files) + 1; > > foreach my $name (@files) { > open( my $fh, '<', $name ) || die "cannot open file $name $!"; > my ( >- $hascopyright, $hasgpl, $hasv3, $hasorlater, $haslinktolicense, >+ $hasgpl, $hasv3, $hasorlater, $haslinktolicense, > $hasfranklinst, $is_not_us > ) = (0) x 7; > while ( my $line = <$fh> ) { >- $hascopyright = 1 if ( $line =~ /^(#|--)?\s*Copyright.*\d\d/ ); >- $hasgpl = 1 if ( $line =~ /GNU General Public License/ ); >- $hasv3 = 1 if ( $line =~ /either version 3/ ); >- $hasorlater = 1 >+ $hasgpl = 1 if ( $line =~ /GNU General Public License/ ); >+ $hasv3 = 1 if ( $line =~ /either version 3/ ); >+ $hasorlater = 1 > if ( $line =~ /any later version/ > || $line =~ /at your option/ ); > $haslinktolicense = 1 if $line =~ m|http://www\.gnu\.org/licenses|; >@@ -60,13 +58,16 @@ foreach my $name (@files) { > $is_not_us = 1 if $line =~ m|This file is part of the Zebra server|; > } > close $fh; >- next unless $hascopyright; >- next if $is_not_us; >- is( $hasgpl && $hasv3 && $hasorlater && $haslinktolicense && !$hasfranklinst, 1 ) >+ >+ if ( $is_not_us || !$hasgpl ) { >+ pass(); >+ next; >+ } >+ >+ ok( $hasgpl && $hasv3 && $hasorlater && $haslinktolicense && !$hasfranklinst ) > or diag( > sprintf > "File %s has wrong copyright: hasgpl=%s, hasv3=%s, hasorlater=%s, haslinktolicense=%s, hasfranklinst=%s", > $name, $hasgpl, $hasv3, $hasorlater, $haslinktolicense, $hasfranklinst > ); > } >-done_testing; >diff --git a/xt/find-missing-csrf.t b/xt/find-missing-csrf.t >index c10d7ee6b1..59610f9d28 100755 >--- a/xt/find-missing-csrf.t >+++ b/xt/find-missing-csrf.t >@@ -22,20 +22,15 @@ use Test::More tests => 2; > use File::Slurp; > use Data::Dumper; > >-my @files; >+use Koha::Devel::Files; > >-# OPAC >-push @files, `git ls-files 'koha-tmpl/opac-tmpl/bootstrap/en/*.tt'`; >-push @files, `git ls-files 'koha-tmpl/opac-tmpl/bootstrap/en/*.inc'`; >+my $dev_files = Koha::Devel::Files->new; >+my @files = $dev_files->ls_tt_files; > >-# Staff >-push @files, `git ls-files 'koha-tmpl/intranet-tmpl/prog/en/*.tt'`; >-push @files, `git ls-files 'koha-tmpl/intranet-tmpl/prog/en/*.inc'`; > ok( @files > 0, 'We should test something' ); > > my @errors; > for my $file (@files) { >- chomp $file; > my @e = check_csrf_in_forms($file); > push @errors, sprintf "%s:%s", $file, join( ",", @e ) if @e; > } >diff --git a/xt/find-missing-filters.t b/xt/find-missing-filters.t >index 0aefa3707c..425c73921e 100755 >--- a/xt/find-missing-filters.t >+++ b/xt/find-missing-filters.t >@@ -20,21 +20,15 @@ use Test::More tests => 2; > use File::Slurp qw( read_file ); > use Data::Dumper; > use t::lib::QA::TemplateFilters; >+use Koha::Devel::Files; > >-my @files; >+my $dev_files = Koha::Devel::Files->new; >+my @files = $dev_files->ls_tt_files; > >-# OPAC >-push @files, `git ls-files 'koha-tmpl/opac-tmpl/bootstrap/en/*.tt'`; >-push @files, `git ls-files 'koha-tmpl/opac-tmpl/bootstrap/en/*.inc'`; >- >-# Staff >-push @files, `git ls-files 'koha-tmpl/intranet-tmpl/prog/en/*.tt'`; >-push @files, `git ls-files 'koha-tmpl/intranet-tmpl/prog/en/*.inc'`; > ok( @files > 0, 'We should test something' ); > > my @errors; > for my $file (@files) { >- chomp $file; > my $content = read_file($file); > my @e = t::lib::QA::TemplateFilters::missing_filters($content); > push @errors, { file => $file, errors => \@e } if @e; >diff --git a/xt/find-missing-op-in-forms.t b/xt/find-missing-op-in-forms.t >index 840d8b994b..7699bfafcf 100755 >--- a/xt/find-missing-op-in-forms.t >+++ b/xt/find-missing-op-in-forms.t >@@ -22,20 +22,14 @@ use Test::More tests => 2; > use File::Slurp; > use Data::Dumper; > >-my @files; >+use Koha::Devel::Files; > >-# OPAC >-push @files, `git ls-files 'koha-tmpl/opac-tmpl/bootstrap/en/*.tt'`; >-push @files, `git ls-files 'koha-tmpl/opac-tmpl/bootstrap/en/*.inc'`; >- >-# Staff >-push @files, `git ls-files 'koha-tmpl/intranet-tmpl/prog/en/*.tt'`; >-push @files, `git ls-files 'koha-tmpl/intranet-tmpl/prog/en/*.inc'`; >+my $dev_files = Koha::Devel::Files->new; >+my @files = $dev_files->ls_tt_files; > ok( @files > 0, 'We should test something' ); > > my @errors; > for my $file (@files) { >- chomp $file; > my @e = catch_missing_op($file); > push @errors, sprintf "%s:%s", $file, join( ",", @e ) if @e; > } >diff --git a/xt/perltidy.t b/xt/perltidy.t >index f5b85031ab..edf8a5abd3 100755 >--- a/xt/perltidy.t >+++ b/xt/perltidy.t >@@ -3,13 +3,13 @@ use Modern::Perl; > use Test::PerlTidy; > use Test::More; > >-my @files; >-push @files, qx{git ls-files '*.pl' '*.PL' '*.pm' '*.t' ':(exclude)Koha/Schema/Result' ':(exclude)Koha/Schema.pm'}; >-push @files, qx{git ls-files svc opac/svc}; # Files without extension >+use Koha::Devel::Files; >+ >+my $dev_files = Koha::Devel::Files->new( { context => 'tidy' } ); >+my @files = $dev_files->ls_perl_files; > > plan tests => scalar @files; > > for my $file (@files) { >- chomp $file; > ok( Test::PerlTidy::is_file_tidy($file) ); > } >diff --git a/xt/pl_valid.t b/xt/pl_valid.t >index 028710deb2..ec7b8f5994 100755 >--- a/xt/pl_valid.t >+++ b/xt/pl_valid.t >@@ -24,17 +24,10 @@ use Pod::Checker; > use Parallel::ForkManager; > use Sys::CPU; > >-my @files; >-push @files, qx{git ls-files '*.pl' '*.PL' '*.pm' '*.t'}; >-push @files, qx{git ls-files svc opac/svc}; # Files without extension >-chomp for @files; >+use Koha::Devel::Files; > >-my @exceptions = qw( >- Koha/Account/Credit.pm >- Koha/Account/Debit.pm >- Koha/Old/Hold.pm >- misc/translator/TmplTokenizer.pm >-); >+my $dev_files = Koha::Devel::Files->new( { context => 'valid' } ); >+my @files = $dev_files->ls_perl_files; > > my $ncpu; > if ( $ENV{KOHA_PROVE_CPUS} ) { >@@ -48,10 +41,6 @@ my $pm = Parallel::ForkManager->new($ncpu); > plan tests => scalar(@files) + 1; > > for my $file (@files) { >- if ( grep { $file eq $_ } @exceptions ) { >- pass("$file is skipped - exception"); >- next; >- } > $pm->start and next; > my $output = `perl -cw '$file' 2>&1`; > chomp $output; >diff --git a/xt/single_quotes.t b/xt/single_quotes.t >index 0e84e4fc0e..a0e7413f9b 100755 >--- a/xt/single_quotes.t >+++ b/xt/single_quotes.t >@@ -21,20 +21,15 @@ use Modern::Perl; > use Test::More tests => 2; > use File::Slurp qw( read_file ); > >-my @files; >+use Koha::Devel::Files; > >-# OPAC >-push @files, `git ls-files 'koha-tmpl/opac-tmpl/bootstrap/en/*.tt'`; >-push @files, `git ls-files 'koha-tmpl/opac-tmpl/bootstrap/en/*.inc'`; >+my $dev_files = Koha::Devel::Files->new; >+my @files = $dev_files->ls_tt_files; > >-# Staff >-push @files, `git ls-files 'koha-tmpl/intranet-tmpl/prog/en/*.tt'`; >-push @files, `git ls-files 'koha-tmpl/intranet-tmpl/prog/en/*.inc'`; > ok( @files > 0, 'We should test something' ); > > my @errors; > for my $file (@files) { >- chomp $file; > my @lines = sort grep /\_\(\'/, read_file($file); > push @errors, { name => $file, lines => \@lines } if @lines; > } >diff --git a/xt/tt_tidy.t b/xt/tt_tidy.t >index c97ce7da54..895c26be7d 100755 >--- a/xt/tt_tidy.t >+++ b/xt/tt_tidy.t >@@ -23,7 +23,10 @@ use Test::Strict; > use Parallel::ForkManager; > use Sys::CPU; > >-my @tt_files = qx{git ls-files '*.tt' '*.inc'}; >+use Koha::Devel::Files; >+ >+my $dev_files = Koha::Devel::Files->new( { context => 'tidy' } ); >+my @tt_files = $dev_files->ls_tt_files; > > $Test::Strict::TEST_STRICT = 0; > >@@ -39,7 +42,6 @@ my $pm = Parallel::ForkManager->new($ncpu); > foreach my $filepath (@tt_files) { > $pm->start and next; > >- chomp $filepath; > my $tidy = qx{perl misc/devel/tidy.pl --silent --no-write $filepath}; > my $content = read_file $filepath; > ok( $content eq $tidy, "$filepath should be kept tidy" ); >diff --git a/yarn.lock b/yarn.lock >index 6436ae5618..5745d12e9e 100755 >--- a/yarn.lock >+++ b/yarn.lock >@@ -2688,6 +2688,11 @@ aws-sign2@~0.7.0: > resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" > integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= > >+aws-ssl-profiles@^1.1.1: >+ version "1.1.2" >+ resolved "https://registry.yarnpkg.com/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz#157dd77e9f19b1d123678e93f120e6f193022641" >+ integrity sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g== >+ > aws4@^1.8.0: > version "1.11.0" > resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59" >@@ -3975,13 +3980,6 @@ csstype@^3.1.3: > resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81" > integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== > >-cypress-mysql@^1.0.0: >- version "1.0.0" >- resolved "https://registry.yarnpkg.com/cypress-mysql/-/cypress-mysql-1.0.0.tgz#987dd693aa4ad6b3968c0ff24834b0ad1af00876" >- integrity sha512-UEy/jwzKEtctyWSQbgGBPsNJH0yCGaKN/xFZsNHAdjeBGOvI1prnhrsTVnOxclPwRmESnlTmap5P52u2Vl4TjA== >- dependencies: >- mysql2 "^2.3.3" >- > cypress@^12.17.2: > version "12.17.4" > resolved "https://registry.yarnpkg.com/cypress/-/cypress-12.17.4.tgz#b4dadf41673058493fa0d2362faa3da1f6ae2e6c" >@@ -4308,7 +4306,7 @@ delayed-stream@~1.0.0: > resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" > integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= > >-denque@^2.0.1: >+denque@^2.1.0: > version "2.1.0" > resolved "https://registry.yarnpkg.com/denque/-/denque-2.1.0.tgz#e93e1a6569fb5e66f16a3c2a2964617d349d6ab1" > integrity sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw== >@@ -8020,10 +8018,10 @@ log-update@^4.0.0: > slice-ansi "^4.0.0" > wrap-ansi "^6.2.0" > >-long@^4.0.0: >- version "4.0.0" >- resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" >- integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA== >+long@^5.2.1: >+ version "5.3.2" >+ resolved "https://registry.yarnpkg.com/long/-/long-5.3.2.tgz#1d84463095999262d7d7b7f8bfd4a8cc55167f83" >+ integrity sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA== > > loose-envify@^1.1.0, loose-envify@^1.4.0: > version "1.4.0" >@@ -8070,6 +8068,11 @@ lru-queue@^0.1.0: > dependencies: > es5-ext "~0.10.2" > >+lru.min@^1.0.0: >+ version "1.1.2" >+ resolved "https://registry.yarnpkg.com/lru.min/-/lru.min-1.1.2.tgz#01ce1d72cc50c7faf8bd1f809ebf05d4331021eb" >+ integrity sha512-Nv9KddBcQSlQopmBHXSsZVY5xsdlZkdH/Iey0BlcBYggMd4two7cZnKOK9vmy3nY0O5RGH99z1PCeTpPqszUYg== >+ > lunr@^2.3.9: > version "2.3.9" > resolved "https://registry.yarnpkg.com/lunr/-/lunr-2.3.9.tgz#18b123142832337dd6e964df1a5a7707b25d35e1" >@@ -8474,17 +8477,18 @@ mute-stdout@^1.0.0: > resolved "https://registry.yarnpkg.com/mute-stdout/-/mute-stdout-1.0.1.tgz#acb0300eb4de23a7ddeec014e3e96044b3472331" > integrity sha512-kDcwXR4PS7caBpuRYYBUz9iVixUk3anO3f5OYFiIPwK/20vCzKCHyKoulbiDY1S53zD2bxUpxN/IJ+TnXjfvxg== > >-mysql2@^2.3.3: >- version "2.3.3" >- resolved "https://registry.yarnpkg.com/mysql2/-/mysql2-2.3.3.tgz#944f3deca4b16629052ff8614fbf89d5552545a0" >- integrity sha512-wxJUev6LgMSgACDkb/InIFxDprRa6T95+VEoR+xPvtngtccNH2dGjEB/fVZ8yg1gWv1510c9CvXuJHi5zUm0ZA== >+mysql2@^3.14.1: >+ version "3.14.1" >+ resolved "https://registry.yarnpkg.com/mysql2/-/mysql2-3.14.1.tgz#7786160abf086fd279e0253e16e34c05b4ab3b3e" >+ integrity sha512-7ytuPQJjQB8TNAYX/H2yhL+iQOnIBjAMam361R7UAL0lOVXWjtdrmoL9HYKqKoLp/8UUTRcvo1QPvK9KL7wA8w== > dependencies: >- denque "^2.0.1" >+ aws-ssl-profiles "^1.1.1" >+ denque "^2.1.0" > generate-function "^2.3.1" > iconv-lite "^0.6.3" >- long "^4.0.0" >- lru-cache "^6.0.0" >- named-placeholders "^1.1.2" >+ long "^5.2.1" >+ lru.min "^1.0.0" >+ named-placeholders "^1.1.3" > seq-queue "^0.0.5" > sqlstring "^2.3.2" > >@@ -8498,7 +8502,7 @@ mysql@^2.18.1: > safe-buffer "5.1.2" > sqlstring "2.3.1" > >-named-placeholders@^1.1.2: >+named-placeholders@^1.1.3: > version "1.1.3" > resolved "https://registry.yarnpkg.com/named-placeholders/-/named-placeholders-1.1.3.tgz#df595799a36654da55dda6152ba7a137ad1d9351" > integrity sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w== >-- >2.15.0 >
You cannot view the attachment while viewing its details because your browser does not support IFRAMEs.
View the attachment on a separate page
.
View Attachment As Raw
Actions:
View
Attachments on
bug 16631
:
184422
|
184508
|
184509
|
184510
|
184511
|
184512
|
184513
|
184514
|
184515
|
184516
|
184517
|
184518
|
184519
|
184520
|
184521
|
184522
|
184525
|
184526
|
184657
|
184674
|
184686
|
184687
|
184688
|
184689
|
184690
|
184691
|
184692
|
184693
|
184694
|
184695
|
184845
|
184846
|
184847
|
184848
|
184859
|
184861