From b49f908c969f367cf6b3974a6d890c0d12cc5c33 Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Fri, 7 Jul 2023 16:02:15 -0400 Subject: [PATCH 1/3] Escape percent signs in URL-encoded search values. --- Catalog.lua | 8 +++++++- Config.xml | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Catalog.lua b/Catalog.lua index f3258c4..86cbaed 100644 --- a/Catalog.lua +++ b/Catalog.lua @@ -213,9 +213,15 @@ function PerformSearch(searchInfo) end local searchUrl = ""; + local encodedSiteCode = Utility.URLEncode(settings.PrimoSiteCode):gsub("%%", "%%%%"); + local encodedSearchType = Utility.URLEncode(DataMapping.SearchTypes[searchInfo[1]]["PrimoField"]):gsub("%%", "%%%%"); + local encodedSearchTerm = Utility.URLEncode(searchTerm):gsub("%%", "%%%%"); --Construct the search url based on the base catalog url and search style. - searchUrl = settings.CatalogUrl .. DataMapping.SearchStyleUrls[searchInfo[2]]:gsub("{PrimoSiteCode}", settings.PrimoSiteCode):gsub("{SearchType}", DataMapping.SearchTypes[searchInfo[1]]["PrimoField"]):gsub("{SearchTerm}", Utility.URLEncode(searchTerm)); + searchUrl = settings.CatalogUrl .. DataMapping.SearchStyleUrls[searchInfo[2]] + :gsub("{PrimoSiteCode}", encodedSiteCode) + :gsub("{SearchType}", encodedSearchType) + :gsub("{SearchTerm}", encodedSearchTerm); log:InfoFormat("Navigating to {0}", searchUrl); catalogSearchForm.Browser:Navigate(searchUrl); diff --git a/Config.xml b/Config.xml index 0f41cd4..e9ba718 100644 --- a/Config.xml +++ b/Config.xml @@ -2,7 +2,7 @@ Alma Primo Definitive Catalog Search Atlas Systems - 1.0.0 + 1.0.1 True Addon Catalog Search and Import Addon that uses Alma as the catalog and Primo or Primo VE as the discovery layer. From b58a7a8829817a727632ee7739d78227fc4fe5b4 Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Tue, 11 Jul 2023 11:17:49 -0400 Subject: [PATCH 2/3] Add support for custom import profiles, improve error handling. --- Catalog.lua | 93 +++++++++++++++++++++++++++++++++---------------- DataMapping.lua | 27 ++++++++++---- 2 files changed, 84 insertions(+), 36 deletions(-) diff --git a/Catalog.lua b/Catalog.lua index 86cbaed..f240b04 100644 --- a/Catalog.lua +++ b/Catalog.lua @@ -22,7 +22,7 @@ catalogSearchForm.Form = nil; catalogSearchForm.Browser = nil; catalogSearchForm.RibbonPage = nil; catalogSearchForm.ItemsButton = nil; -catalogSearchForm.ImportButton = nil; +catalogSearchForm.ImportButtons = {}; catalogSearchForm.SearchButtons = {}; local mmsIdsCache = {}; @@ -59,7 +59,7 @@ local watcherEnabled = false; local recordsLastRetrievedFrom = ""; local layoutMode = "browse"; local browserType = nil; -if AddonInfo.Browsers ~= nil and AddonInfo.Browsers.WebView2 ~= nil and AddonInfo.Browsers.WebView2 == true then +if AddonInfo.Browsers and AddonInfo.Browsers.WebView2 then browserType = "WebView2"; else browserType = "Chromium"; @@ -82,15 +82,34 @@ function Init() catalogSearchForm.SearchButtons["Home"] = catalogSearchForm.RibbonPage:CreateButton("New Search", GetClientImage(DataMapping.Icons[product]["Web"]), "ShowCatalogHome", "Search Options"); log:Info("Creating buttons for available search types."); - for _, searchType in ipairs(settings.AvailableSearchTypes) do - local searchStyle = DataMapping.SearchTypes[searchType].SearchStyle; + local success, err = pcall(function() + for _, searchType in ipairs(settings.AvailableSearchTypes) do + local searchStyle = DataMapping.SearchTypes[searchType].SearchStyle; - log:DebugFormat("Creating button for search type {0} with search style {1}", searchType, searchStyle); + log:DebugFormat("Creating button for search type {0} with search style {1}", searchType, searchStyle); - catalogSearchForm.SearchButtons[searchType] = catalogSearchForm.RibbonPage:CreateButton(DataMapping.SearchTypes[searchType].ButtonText, GetClientImage(DataMapping.SearchTypes[searchType][product .. "Icon"]), "Placeholder", "Search Options"); + catalogSearchForm.SearchButtons[searchType] = catalogSearchForm.RibbonPage:CreateButton(DataMapping.SearchTypes[searchType].ButtonText, GetClientImage(DataMapping.SearchTypes[searchType][product .. "Icon"]), "Placeholder", "Search Options"); - catalogSearchForm.SearchButtons[searchType].BarButton:add_ItemClick(ButtonSearch); - catalogSearchForm.SearchButtons[searchType].BarButton.Tag = {SearchType = searchType, SearchStyle = searchStyle}; + catalogSearchForm.SearchButtons[searchType].BarButton:add_ItemClick(ButtonSearch); + catalogSearchForm.SearchButtons[searchType].BarButton.Tag = {SearchType = searchType, SearchStyle = searchStyle}; + end + end); + if not success then + log:ErrorFormat("{0}. Search types may be configured incorrectly. Please ensure SearchTypes exist in DataMapping.lua for each search type in the AvailableSearchTypes setting.", TraverseError(err)); + interfaceMngr:ShowMessage("Search types may be configured incorrectly. Please ensure SearchTypes exist in DataMapping.lua for each search type in the AvailableSearchTypes setting. See client log for details.", "Configuration Error"); + end + + log:Info("Creating buttons for import profiles."); + for importProfileName, importProfile in pairs(DataMapping.ImportProfiles) do + if importProfile.Product == product then + log:DebugFormat("Creating button for import profile {0}", importProfileName); + + catalogSearchForm.ImportButtons[importProfileName] = catalogSearchForm.RibbonPage:CreateButton(DataMapping.ImportProfiles[importProfileName].ButtonText, GetClientImage(DataMapping.ImportProfiles[importProfileName].Icon), "Placeholder", "Process"); + + catalogSearchForm.ImportButtons[importProfileName].BarButton:add_ItemClick(DoItemImport); + catalogSearchForm.ImportButtons[importProfileName].BarButton.Tag = {ImportProfileName = importProfileName}; + catalogSearchForm.ImportButtons[importProfileName].BarButton.Enabled = false; + end end if (not settings.AutoRetrieveItems) then @@ -98,10 +117,6 @@ function Init() catalogSearchForm.ItemsButton.BarButton.ItemShortcut = types["DevExpress.XtraBars.BarShortcut"](types["System.Windows.Forms.Shortcut"].CtrlR); end - catalogSearchForm.ImportButton = catalogSearchForm.RibbonPage:CreateButton("Import", GetClientImage(DataMapping.Icons[product]["Import"]), "DoItemImport", "Process"); - catalogSearchForm.ImportButton.BarButton.ItemShortcut = types["DevExpress.XtraBars.BarShortcut"](types["System.Windows.Forms.Shortcut"].CtrlI); - catalogSearchForm.ImportButton.BarButton.Enabled = false; - BuildItemsGrid(); catalogSearchForm.Form:LoadLayout("CatalogLayout_Browse_" .. browserType .. ".xml"); @@ -257,7 +272,10 @@ function ExtractIds(itemDetails) local idMatches = {}; local urlId = (catalogSearchForm.Browser.Address):match("%d+" .. settings.IdSuffix); -- Easy way to prevent duplicates regardless of order since the keys get overwritten. - idMatches[urlId] = true; + -- In rare cases the URL won't contain an ID. + if urlId then + idMatches[urlId] = true; + end -- MMS Ids (and presumably IE IDs) all have the same last four digits specific to the institution. -- 99 is the prefix for MMS IDs, and IE IDs always have 1, 2, or 5 as their first digit and 1 as the second digit. @@ -372,7 +390,9 @@ function ToggleItemsUIElements(enabled) -- If there's an item in the Item Grid if(catalogSearchForm.Grid.GridControl.MainView.FocusedRowHandle > -1) then catalogSearchForm.Grid.GridControl.Enabled = true; - catalogSearchForm.ImportButton.BarButton.Enabled = true; + for buttonName, _ in pairs(catalogSearchForm.ImportButtons) do + catalogSearchForm.ImportButtons[buttonName].BarButton.Enabled = true; + end end end else @@ -380,7 +400,9 @@ function ToggleItemsUIElements(enabled) ClearItems(); recordsLastRetrievedFrom = ""; catalogSearchForm.Grid.GridControl.Enabled = false; - catalogSearchForm.ImportButton.BarButton.Enabled = false; + for buttonName, _ in pairs(catalogSearchForm.ImportButtons) do + catalogSearchForm.ImportButtons[buttonName].BarButton.Enabled = false; + end if (not settings.AutoRetrieveItems) then catalogSearchForm.ItemsButton.BarButton.Enabled = false; @@ -478,10 +500,14 @@ end function ItemsGridFocusedRowChanged(sender, args) if (args.FocusedRowHandle > -1) then - catalogSearchForm.ImportButton.BarButton.Enabled = true; + for buttonName, _ in pairs(catalogSearchForm.ImportButtons) do + catalogSearchForm.ImportButtons[buttonName].BarButton.Enabled = true; + end catalogSearchForm.Grid.GridControl.Enabled = true; else - catalogSearchForm.ImportButton.BarButton.Enabled = false; + for buttonName, _ in pairs(catalogSearchForm.ImportButtons) do + catalogSearchForm.ImportButtons[buttonName].BarButton.Enabled = false; + end end; end @@ -653,9 +679,11 @@ function PopulateItemsDataSources( response, itemsDataTable ) catalogSearchForm.Grid.GridControl:EndUpdate(); end -function DoItemImport() +function DoItemImport(sender, args) cursor.Current = cursors.WaitCursor; + local importProfileName = args.Item.Tag.ImportProfileName; + log:Debug("Retrieving import row."); local importRow = catalogSearchForm.Grid.GridControl.MainView:GetFocusedRow(); @@ -665,18 +693,24 @@ function DoItemImport() end; log:Info("Importing item values."); - for _, target in ipairs(DataMapping.ImportFields.Item[product]) do - local importValue = importRow:get_Item(target.Value); + local success, err = pcall(function() + for _, target in ipairs(DataMapping.ImportFields.Item[importProfileName]) do + local importValue = importRow:get_Item(target.Value); - log:DebugFormat("Importing value '{0}' to {1}", importValue, target.Field); - ImportField(target.Table, target.Field, importValue, target.MaxSize); + log:DebugFormat("Importing value '{0}' to {1}", importValue, target.Field); + ImportField(target.Table, target.Field, importValue, target.MaxSize); + end + end); + if not success then + log:ErrorFormat("{0}. Import profiles may not be configured correctly. Please ensure that each import profile in DataMapping.lua corresponds to a set of item import fields.", TraverseError(err)); + interfaceMngr:ShowMessage("Import profiles may not be configured correctly. Please ensure that each import profile in DataMapping.lua corresponds to a set of item import fields. See client log for details.", "Configuration Error"); end local mmsId = importRow:get_Item("ReferenceNumber"); local holdingId = importRow:get_Item("HoldingId"); - local holdingInformation = GetMarcInformation(mmsId, holdingId); - local bibliographicInformation = GetMarcInformation(mmsId); + local holdingInformation = GetMarcInformation(importProfileName, mmsId, holdingId); + local bibliographicInformation = GetMarcInformation(importProfileName, mmsId); log:Info("Importing bib values."); for _, target in ipairs(bibliographicInformation) do @@ -694,7 +728,7 @@ function DoItemImport() ExecuteCommand("SwitchTab", "Detail"); end -function GetMarcInformation(mmsId, holdingId) +function GetMarcInformation(importProfileName, mmsId, holdingId) local marcInformation = {}; local marcXmlDoc = nil; @@ -719,9 +753,9 @@ function GetMarcInformation(mmsId, holdingId) -- Loops through each import mapping local mappingTable = {}; if holdingId then - mappingTable = DataMapping.ImportFields.Holding[product]; + mappingTable = DataMapping.ImportFields.Holding[importProfileName]; else - mappingTable = DataMapping.ImportFields.Bibliographic[product]; + mappingTable = DataMapping.ImportFields.Bibliographic[importProfileName]; end for _, target in ipairs(mappingTable) do @@ -776,12 +810,11 @@ end function TraverseError(e) if not e.GetType then -- Not a .NET type - return nil; + return e; else if not e.Message then -- Not a .NET exception - log:Debug(e:ToString()); - return nil; + return e; end end diff --git a/DataMapping.lua b/DataMapping.lua index c7da76c..ca23103 100644 --- a/DataMapping.lua +++ b/DataMapping.lua @@ -3,6 +3,7 @@ DataMapping.Icons = {}; DataMapping.SearchTypes = {}; DataMapping.SearchStyleUrls = {}; DataMapping.SourceFields = {}; +DataMapping.ImportProfiles = {}; DataMapping.ImportFields = {}; DataMapping.ImportFields.Bibliographic = {}; DataMapping.ImportFields.Holding = {}; @@ -18,7 +19,6 @@ DataMapping.Icons["Aeon"] = {}; DataMapping.Icons["Aeon"]["Home"] = "home_32x32"; DataMapping.Icons["Aeon"]["Web"] = "web_32x32"; DataMapping.Icons["Aeon"]["Retrieve Items"] = "record_32x32"; -DataMapping.Icons["Aeon"]["Import"] = "impt_32x32"; --[[ SearchTypes @@ -87,10 +87,25 @@ DataMapping.SearchStyleUrls["Browse"] = "browse?vid={PrimoSiteCode}&browseQuery= DataMapping.SourceFields["Aeon"] = {}; DataMapping.SourceFields["Aeon"]["TransactionNumber"] = { Table = "Transaction", Field = "TransactionNumber" }; +--[[ +Import Profiles +Each import profile defines a set of fields to be imported. A button will be generated for each +profile with a Product property matching the product you're using. The key for the import profile +must have a corresponding set of import fields with matching keys in the next section. + - ButtonText: The text that appears on the ribbon button for the import. + - Product: The product the import profile is for. + - Icon: The name of the icon file to use as the button's image. +--]] + +DataMapping.ImportProfiles["Default"] = { + ButtonText = "Import", + Product = "Aeon", + Icon = "impt_32x32" +} --- Import Fields: The table for each ImportFields section must be defined for the product (Aeon, ILLiad, or Ares) +-- Import Fields: The table for each ImportFields section must be defined for each import profile above. -- running the addon to work properly, even if it is empty. Example of an empty ImportFields table: - -- DataMapping.ImportFields.Bibliographic["Aeon"] = { }; + -- DataMapping.ImportFields.Bibliographic["Default"] = { }; --[[ Bib-level import fields. @@ -101,7 +116,7 @@ DataMapping.SourceFields["Aeon"]["TransactionNumber"] = { Table = "Transaction", in parentheses next to the field type. - Value must be an XPath expression. --]] -DataMapping.ImportFields.Bibliographic["Aeon"] = { +DataMapping.ImportFields.Bibliographic["Default"] = { { Table = "Transaction", Field = "ItemTitle", MaxSize = 255, @@ -140,12 +155,12 @@ DataMapping.ImportFields.Bibliographic["Aeon"] = { }; -- Holding-level import fields. Value must be an XPath expression. - DataMapping.ImportFields.Holding["Aeon"] = { + DataMapping.ImportFields.Holding["Default"] = { } -- Item-level import fields. Value should not be changed. -DataMapping.ImportFields.Item["Aeon"] = { +DataMapping.ImportFields.Item["Default"] = { { Table = "Transaction", Field = "ReferenceNumber", MaxSize = 50, From c9409035a32109eb62cdcc8c85d83a2daa1b13ea Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Tue, 11 Jul 2023 11:26:51 -0400 Subject: [PATCH 3/3] Update Readme with import profile explanation. --- README.md | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 7bb44cc..30f1f8d 100644 --- a/README.md +++ b/README.md @@ -89,13 +89,6 @@ DataMapping.SearchTypes["Author"] = { AeonIcon = "srch_32x32", AeonSourceField = { Table = "Transaction", Field = "ItemAuthor" } }; -DataMapping.SearchTypes["Call Number"] = { - ButtonText = "Call Number", - PrimoField = "lsr01", - SearchStyle = "Query", - AeonIcon = "srch_32x32", - AeonSourceField = { Table = "Transaction", Field = "CallNumber" } -}; DataMapping.SearchTypes["ISBN"] = { ButtonText = "ISBN", PrimoField = "isbn", @@ -119,7 +112,7 @@ DataMapping.SearchTypes["Catalog Number"] = { }; ``` ->**Note:** The *Catalog Number* search type performs an `any` search because Primo does not have a search type for MMS ID. +>**Note:** The *Catalog Number* search type performs an `any` search because Primo does not have a search type for MMS ID by default. ### Source Fields The field that the addon reads from for values used by the addon that are not used in searches. @@ -133,6 +126,19 @@ DataMapping.SourceFields["Aeon"] = {}; DataMapping.SourceFields["Aeon"]["TransactionNumber"] = { Table = "Transaction", Field = "TransactionNumber" }; ``` +### Import Profiles +Similar to SearchTypes, custom import profiles can be configured. Each import profile in DataMapping.lua will generate an import button with the ButtonText as its label. Each import profile must correspond to a set of bibliographic, holding, and item import fields with matching keys. + +*Default Configuration:* + +```lua +DataMapping.ImportProfiles["Default"] = { + ButtonText = "Import", + Product = "Aeon", + Icon = "impt_32x32" +} +``` + ### Bibliographic Import The information within this data mapping is used to perform the bibliographic api call. The `Field` is the product field that the data will be imported into, `MaxSize` is the maximum character size the data going into the product field can be, and `Value` is the XPath query to the information. @@ -150,7 +156,7 @@ The information within this data mapping is used import the correct information *Default Configuration:* ```lua -DataMapping.ImportFields.Item["Aeon"] = { +DataMapping.ImportFields.Item["Default"] = { { Table = "Transaction", Field = "ReferenceNumber", MaxSize = 50,