bulkrax 9.4.1 → 9.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +8 -2
- data/app/assets/javascripts/bulkrax/datatables.js +43 -8
- data/app/assets/javascripts/bulkrax/importers_stepper.js +221 -26
- data/app/assets/stylesheets/bulkrax/stepper/_review.scss +14 -12
- data/app/controllers/bulkrax/entries_controller.rb +2 -2
- data/app/controllers/bulkrax/exporters_controller.rb +3 -3
- data/app/controllers/bulkrax/guided_imports_controller.rb +3 -1
- data/app/controllers/bulkrax/importers_controller.rb +5 -5
- data/app/matchers/bulkrax/application_matcher.rb +5 -6
- data/app/models/bulkrax/csv_entry.rb +1 -1
- data/app/parsers/bulkrax/csv_parser.rb +15 -12
- data/app/parsers/concerns/bulkrax/csv_parser/csv_template_generation.rb +4 -1
- data/app/parsers/concerns/bulkrax/csv_parser/csv_validation.rb +10 -8
- data/app/parsers/concerns/bulkrax/csv_parser/csv_validation_helpers.rb +68 -35
- data/app/parsers/concerns/bulkrax/csv_parser/csv_validation_hierarchy.rb +9 -7
- data/app/services/bulkrax/csv_template/file_validator.rb +1 -1
- data/app/services/bulkrax/csv_template/mapping_manager.rb +15 -6
- data/app/services/bulkrax/csv_template/split_formatter.rb +10 -3
- data/app/services/bulkrax/split_pattern_coercion.rb +42 -0
- data/app/services/bulkrax/stepper_response_formatter.rb +2 -1
- data/app/services/bulkrax/validation_error_csv_builder.rb +36 -12
- data/app/validators/bulkrax/csv_row/child_reference.rb +2 -1
- data/app/validators/bulkrax/csv_row/parent_reference.rb +1 -1
- data/app/validators/bulkrax/csv_row/required_values.rb +17 -3
- data/app/views/bulkrax/exporters/edit.html.erb +1 -1
- data/app/views/bulkrax/exporters/index.html.erb +3 -1
- data/app/views/bulkrax/exporters/new.html.erb +1 -1
- data/app/views/bulkrax/exporters/show.html.erb +1 -1
- data/app/views/bulkrax/guided_imports/new.html.erb +7 -0
- data/app/views/bulkrax/importers/_edit_item_buttons.html.erb +3 -3
- data/app/views/bulkrax/importers/index.html.erb +2 -0
- data/app/views/bulkrax/importers/new.html.erb +1 -1
- data/app/views/bulkrax/importers/show.html.erb +3 -1
- data/app/views/bulkrax/shared/_datatable_i18n.html.erb +3 -0
- data/config/locales/bulkrax.de.yml +89 -0
- data/config/locales/bulkrax.en.yml +52 -0
- data/config/locales/bulkrax.es.yml +89 -0
- data/config/locales/bulkrax.fr.yml +89 -0
- data/config/locales/bulkrax.it.yml +89 -0
- data/config/locales/bulkrax.pt-BR.yml +89 -0
- data/config/locales/bulkrax.zh.yml +90 -0
- data/db/migrate/20260424081537_remove_parents_from_bulkrax_importer_runs.rb +9 -0
- data/lib/bulkrax/version.rb +1 -1
- data/lib/bulkrax.rb +15 -1
- metadata +7 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 71b1c3954d163d1bf9466de4754d00af59da446bf3772c401b11fbbd4de03a2b
|
|
4
|
+
data.tar.gz: 3a96a3b0d58992dd3c57a5253cd981bfd672645c67b47c08b2f684603467cce4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f278c8b666e3b9460ed4a06aec63f963f982b11a66a3c9e4d6282a5e475238e1dad5f5a5fb006b5f913649d879f9fbe947a1caf5ad29a6d26f8846d56cff0870
|
|
7
|
+
data.tar.gz: 94a2041d470a33d8ed790f819b3e017a4cba6be85749b4693796e8a55fe4759810123dfe82525bcc08e52343f5a174ae69ba919750c99c927e80e1dd0dadd766
|
data/README.md
CHANGED
|
@@ -225,9 +225,15 @@ This project is intended to be a safe, welcoming space for collaboration, and co
|
|
|
225
225
|
```bash
|
|
226
226
|
/path/to/ruby/install/bin/gem install bundler -v '~> 2.4.0'
|
|
227
227
|
```
|
|
228
|
-
- Decide on your version of Hyrax to test against and export it to your environment, then bundle install. The Hyrax version should be greater or equal to 2.3.
|
|
228
|
+
- Decide on your version of Hyrax & Rails to test against and export it to your environment, then bundle install. The Hyrax version should be greater or equal to 2.3.
|
|
229
|
+
|
|
230
|
+
You can find version combinations that should work in [.github/workflows/test.yml](.github/workflows/test.yml)
|
|
231
|
+
|
|
232
|
+
If you have previously bundle installed with a different combination of versions, you may need to remove your Gemfile.lock `rm Gemfile.lock`
|
|
233
|
+
|
|
229
234
|
```bash
|
|
230
|
-
export
|
|
235
|
+
export RAILS_GEM_VERSION="~> 7.2"
|
|
236
|
+
export HYRAX_VERSION="~> 5.2"
|
|
231
237
|
bundle install
|
|
232
238
|
```
|
|
233
239
|
- Run the test migrations
|
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
function bulkraxDatatableLanguage() {
|
|
2
|
+
var i18n = (window.BulkraxI18n && window.BulkraxI18n.datatable && window.BulkraxI18n.datatable.language) || {}
|
|
3
|
+
return {
|
|
4
|
+
emptyTable: i18n.empty_table,
|
|
5
|
+
info: i18n.info,
|
|
6
|
+
infoEmpty: i18n.info_empty,
|
|
7
|
+
infoFiltered: i18n.info_filtered,
|
|
8
|
+
lengthMenu: i18n.length_menu,
|
|
9
|
+
loadingRecords: i18n.loading_records,
|
|
10
|
+
processing: i18n.processing,
|
|
11
|
+
search: i18n.search,
|
|
12
|
+
zeroRecords: i18n.zero_records,
|
|
13
|
+
paginate: {
|
|
14
|
+
next: i18n.next,
|
|
15
|
+
previous: i18n.previous
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
1
20
|
Blacklight.onLoad(function() {
|
|
2
21
|
if($('#importer-show-table').length) {
|
|
3
22
|
$('#importer-show-table').DataTable( {
|
|
@@ -10,6 +29,7 @@ Blacklight.onLoad(function() {
|
|
|
10
29
|
"ajax": window.location.href.replace(/(\/(importers|exporters)\/\d+)/, "$1/entry_table.json"),
|
|
11
30
|
"pageLength": 30,
|
|
12
31
|
"lengthMenu": [[30, 100, 200], [30, 100, 200]],
|
|
32
|
+
"language": bulkraxDatatableLanguage(),
|
|
13
33
|
"columns": [
|
|
14
34
|
{ "data": "identifier" },
|
|
15
35
|
{ "data": "id" },
|
|
@@ -45,6 +65,7 @@ Blacklight.onLoad(function() {
|
|
|
45
65
|
"ajax": window.location.href.replace(/(\/importers)/, "$1/importer_table.json"),
|
|
46
66
|
"pageLength": 30,
|
|
47
67
|
"lengthMenu": [[30, 100, 200], [30, 100, 200]],
|
|
68
|
+
"language": bulkraxDatatableLanguage(),
|
|
48
69
|
"order": [[2, 'desc']],
|
|
49
70
|
"columns": [
|
|
50
71
|
{ "data": "name" },
|
|
@@ -76,6 +97,7 @@ Blacklight.onLoad(function() {
|
|
|
76
97
|
"ajax": window.location.href.replace(/(\/exporters)/, "$1/exporter_table.json"),
|
|
77
98
|
"pageLength": 30,
|
|
78
99
|
"lengthMenu": [[30, 100, 200], [30, 100, 200]],
|
|
100
|
+
"language": bulkraxDatatableLanguage(),
|
|
79
101
|
"columns": [
|
|
80
102
|
{ "data": "name" },
|
|
81
103
|
{ "data": "status_message" },
|
|
@@ -94,13 +116,22 @@ Blacklight.onLoad(function() {
|
|
|
94
116
|
|
|
95
117
|
})
|
|
96
118
|
|
|
119
|
+
function bulkraxDatatableFilters() {
|
|
120
|
+
return (window.BulkraxI18n && window.BulkraxI18n.datatable && window.BulkraxI18n.datatable.filters) || {}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function bulkraxDatatableStatuses() {
|
|
124
|
+
return (window.BulkraxI18n && window.BulkraxI18n.datatable && window.BulkraxI18n.datatable.status) || {}
|
|
125
|
+
}
|
|
126
|
+
|
|
97
127
|
function entrySelect() {
|
|
98
128
|
let entrySelect = document.createElement('select')
|
|
99
129
|
entrySelect.id = 'entry-filter'
|
|
100
130
|
entrySelect.classList.value = 'form-control input-sm'
|
|
101
131
|
entrySelect.style.marginRight = '10px'
|
|
102
132
|
|
|
103
|
-
|
|
133
|
+
var filters = bulkraxDatatableFilters()
|
|
134
|
+
entrySelect.add(new Option(filters.filter_by_entry_class || 'Filter by Entry Class', ''))
|
|
104
135
|
// Read the options from the footer and add them to the entrySelect
|
|
105
136
|
$('#importer-entry-classes').text().split('|').forEach(function (col, i) {
|
|
106
137
|
entrySelect.add(new Option(col.trim()))
|
|
@@ -122,13 +153,17 @@ function statusSelect() {
|
|
|
122
153
|
statusSelect.classList.value = 'form-control input-sm'
|
|
123
154
|
statusSelect.style.marginRight = '10px'
|
|
124
155
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
statusSelect.add(new Option('
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
statusSelect.add(new Option('
|
|
131
|
-
statusSelect.add(new Option(
|
|
156
|
+
var filters = bulkraxDatatableFilters()
|
|
157
|
+
var statuses = bulkraxDatatableStatuses()
|
|
158
|
+
statusSelect.add(new Option(filters.filter_by_status || 'Filter by Status', ''));
|
|
159
|
+
// The option value must remain the English status string, as that is what
|
|
160
|
+
// the backend search/filter logic matches against.
|
|
161
|
+
statusSelect.add(new Option(statuses.complete || 'Complete', 'Complete'))
|
|
162
|
+
statusSelect.add(new Option(statuses.pending || 'Pending', 'Pending'))
|
|
163
|
+
statusSelect.add(new Option(statuses.failed || 'Failed', 'Failed'))
|
|
164
|
+
statusSelect.add(new Option(statuses.skipped || 'Skipped', 'Skipped'))
|
|
165
|
+
statusSelect.add(new Option(statuses.deleted || 'Deleted', 'Deleted'))
|
|
166
|
+
statusSelect.add(new Option(statuses.complete_with_failures || 'Complete (with failures)', 'Complete (with failures)'))
|
|
132
167
|
|
|
133
168
|
document.querySelector('div.dataTables_filter').firstChild.prepend(statusSelect)
|
|
134
169
|
|
|
@@ -91,6 +91,9 @@
|
|
|
91
91
|
// Hierarchy rendering limits
|
|
92
92
|
MAX_TREE_DEPTH: 50, // Prevent stack overflow on deeply nested hierarchies
|
|
93
93
|
|
|
94
|
+
// Validation issue list cap — accordion shows up to this many items; full list is in the downloaded CSV
|
|
95
|
+
MAX_ISSUE_ITEMS: 10,
|
|
96
|
+
|
|
94
97
|
// API endpoints
|
|
95
98
|
ENDPOINTS: {
|
|
96
99
|
DEMO_SCENARIOS: '/importers/guided_import/demo_scenarios',
|
|
@@ -1341,6 +1344,8 @@
|
|
|
1341
1344
|
$('input[name="importer[parser_fields][override_rights_statement]"]').prop('checked', false)
|
|
1342
1345
|
|
|
1343
1346
|
// Clear review step warnings from previous run
|
|
1347
|
+
$('.review-errors-list').empty()
|
|
1348
|
+
$('.review-errors').hide()
|
|
1344
1349
|
$('.review-warnings-list').empty()
|
|
1345
1350
|
$('.review-warnings').hide()
|
|
1346
1351
|
$('.large-import-warning').hide()
|
|
@@ -1658,8 +1663,11 @@
|
|
|
1658
1663
|
|
|
1659
1664
|
// Download errors button
|
|
1660
1665
|
if (data.validationErrorsCacheKey) {
|
|
1666
|
+
var locale = $('input[name="locale"]').val()
|
|
1661
1667
|
var downloadUrl =
|
|
1662
|
-
CONSTANTS.ENDPOINTS.DOWNLOAD_VALIDATION_ERRORS +
|
|
1668
|
+
CONSTANTS.ENDPOINTS.DOWNLOAD_VALIDATION_ERRORS +
|
|
1669
|
+
'?key=' + encodeURIComponent(data.validationErrorsCacheKey) +
|
|
1670
|
+
(locale ? '&locale=' + encodeURIComponent(locale) : '')
|
|
1663
1671
|
var $btn = $(
|
|
1664
1672
|
'<a class="btn btn-outline-secondary btn-sm mt-2" href="' +
|
|
1665
1673
|
downloadUrl +
|
|
@@ -1745,12 +1753,14 @@
|
|
|
1745
1753
|
return grouped
|
|
1746
1754
|
}
|
|
1747
1755
|
|
|
1748
|
-
// Render missing required fields grouped by model
|
|
1756
|
+
// Render missing required fields grouped by model. Group count capped at
|
|
1757
|
+
// CONSTANTS.MAX_ISSUE_ITEMS so we never show a partial group.
|
|
1749
1758
|
function renderMissingRequiredFields(items) {
|
|
1750
1759
|
var groupedByModel = groupItemsByModel(items)
|
|
1760
|
+
var modelNames = Object.keys(groupedByModel).slice(0, CONSTANTS.MAX_ISSUE_ITEMS)
|
|
1751
1761
|
var parts = []
|
|
1752
1762
|
|
|
1753
|
-
|
|
1763
|
+
modelNames.forEach(function (modelName) {
|
|
1754
1764
|
parts.push('<div class="missing-field-group">')
|
|
1755
1765
|
parts.push('<strong class="missing-field-model">' + modelName + '</strong>')
|
|
1756
1766
|
parts.push('<ul>')
|
|
@@ -1768,9 +1778,14 @@
|
|
|
1768
1778
|
return parts.join('')
|
|
1769
1779
|
}
|
|
1770
1780
|
|
|
1771
|
-
// Render default issue items (unrecognized fields, file references, etc.)
|
|
1781
|
+
// Render default issue items (unrecognized fields, file references, etc.).
|
|
1782
|
+
// When items carry a `category` (row-level errors/warnings), show up to
|
|
1783
|
+
// CONSTANTS.MAX_ISSUE_ITEMS per category so each distinct problem type is
|
|
1784
|
+
// represented. Otherwise fall back to a simple slice of the first N items.
|
|
1772
1785
|
function renderDefaultIssueItems(items) {
|
|
1773
|
-
var
|
|
1786
|
+
var hasCategories = items.some(function (item) { return item.category })
|
|
1787
|
+
var capped = hasCategories ? capItemsPerCategory(items) : items.slice(0, CONSTANTS.MAX_ISSUE_ITEMS)
|
|
1788
|
+
var listItems = capped.map(function (item) {
|
|
1774
1789
|
var msg = item.message ? ' — ' + item.message : ''
|
|
1775
1790
|
return '<li>• ' + item.field + msg + '</li>'
|
|
1776
1791
|
})
|
|
@@ -1778,6 +1793,19 @@
|
|
|
1778
1793
|
return '<ul>' + listItems.join('') + '</ul>'
|
|
1779
1794
|
}
|
|
1780
1795
|
|
|
1796
|
+
// Keep the first CONSTANTS.MAX_ISSUE_ITEMS entries per category, preserving
|
|
1797
|
+
// their original order so rows still read top-to-bottom within each category.
|
|
1798
|
+
function capItemsPerCategory(items) {
|
|
1799
|
+
var counts = {}
|
|
1800
|
+
var result = []
|
|
1801
|
+
items.forEach(function (item) {
|
|
1802
|
+
var cat = item.category || 'uncategorized'
|
|
1803
|
+
counts[cat] = (counts[cat] || 0) + 1
|
|
1804
|
+
if (counts[cat] <= CONSTANTS.MAX_ISSUE_ITEMS) result.push(item)
|
|
1805
|
+
})
|
|
1806
|
+
return result
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1781
1809
|
// Render issue items based on issue type
|
|
1782
1810
|
function renderIssueItems(issue) {
|
|
1783
1811
|
var hasModelField = issue.items.some(function (item) { return item.model })
|
|
@@ -1845,7 +1873,7 @@
|
|
|
1845
1873
|
issue.title,
|
|
1846
1874
|
issue.icon,
|
|
1847
1875
|
issue.severity,
|
|
1848
|
-
issue
|
|
1876
|
+
countDisplayFor(issue),
|
|
1849
1877
|
issue.defaultOpen,
|
|
1850
1878
|
issueContent
|
|
1851
1879
|
)
|
|
@@ -1855,6 +1883,185 @@
|
|
|
1855
1883
|
|
|
1856
1884
|
}
|
|
1857
1885
|
|
|
1886
|
+
// Build the count badge display for a validation issue accordion.
|
|
1887
|
+
// Returns a plain number (or null) normally, or an HTML fragment with a
|
|
1888
|
+
// truncation note when the inline list is capped.
|
|
1889
|
+
function countDisplayFor(issue) {
|
|
1890
|
+
if (issue.count == null) return null
|
|
1891
|
+
if (!isIssueTruncated(issue)) return issue.count
|
|
1892
|
+
|
|
1893
|
+
var noteText = t('validation_truncated_note', {
|
|
1894
|
+
shown: issueShownCount(issue)
|
|
1895
|
+
})
|
|
1896
|
+
var tooltip = t('validation_truncated_tooltip')
|
|
1897
|
+
return issue.count +
|
|
1898
|
+
' <span class="accordion-count-note" title="' + escapeHtml(tooltip) +
|
|
1899
|
+
'" aria-label="' + escapeHtml(tooltip) + '">' +
|
|
1900
|
+
escapeHtml(noteText) + '</span>'
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
// How many items are actually rendered in the accordion body, mirroring the
|
|
1904
|
+
// slicing logic in renderDefaultIssueItems / renderMissingRequiredFields.
|
|
1905
|
+
function issueShownCount(issue) {
|
|
1906
|
+
var items = issue.items || []
|
|
1907
|
+
if (!items.length) return 0
|
|
1908
|
+
|
|
1909
|
+
var hasModelField = issue.type === 'missing_required_fields' &&
|
|
1910
|
+
items.some(function (item) { return item.model })
|
|
1911
|
+
if (hasModelField) {
|
|
1912
|
+
return Math.min(issueDisplayUnitCount(issue), CONSTANTS.MAX_ISSUE_ITEMS)
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
var hasCategories = items.some(function (item) { return item.category })
|
|
1916
|
+
if (hasCategories) return capItemsPerCategory(items).length
|
|
1917
|
+
|
|
1918
|
+
return Math.min(items.length, CONSTANTS.MAX_ISSUE_ITEMS)
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
// Decide whether an issue's rendered item list is capped. Mirrors the
|
|
1922
|
+
// slicing logic in renderDefaultIssueItems / renderMissingRequiredFields so
|
|
1923
|
+
// the header note only appears when items are actually dropped.
|
|
1924
|
+
function isIssueTruncated(issue) {
|
|
1925
|
+
var items = issue.items || []
|
|
1926
|
+
if (!items.length) return false
|
|
1927
|
+
|
|
1928
|
+
var hasModelField = issue.type === 'missing_required_fields' &&
|
|
1929
|
+
items.some(function (item) { return item.model })
|
|
1930
|
+
if (hasModelField) return issueDisplayUnitCount(issue) > CONSTANTS.MAX_ISSUE_ITEMS
|
|
1931
|
+
|
|
1932
|
+
var hasCategories = items.some(function (item) { return item.category })
|
|
1933
|
+
if (hasCategories) {
|
|
1934
|
+
var counts = {}
|
|
1935
|
+
for (var i = 0; i < items.length; i++) {
|
|
1936
|
+
var cat = items[i].category || 'uncategorized'
|
|
1937
|
+
counts[cat] = (counts[cat] || 0) + 1
|
|
1938
|
+
if (counts[cat] > CONSTANTS.MAX_ISSUE_ITEMS) return true
|
|
1939
|
+
}
|
|
1940
|
+
return false
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
return items.length > CONSTANTS.MAX_ISSUE_ITEMS
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1946
|
+
// Render one review-section (errors or warnings) on Step 3 from a list of
|
|
1947
|
+
// validation issues. Each issue gets a breakdown appropriate to its shape:
|
|
1948
|
+
// row-level issues group by category, missing_required_fields groups by
|
|
1949
|
+
// model, everything else is a flat list of field names.
|
|
1950
|
+
function renderReviewIssueSection(issues, sectionSelector, listSelector) {
|
|
1951
|
+
if (!issues.length) {
|
|
1952
|
+
$(listSelector).empty()
|
|
1953
|
+
$(sectionSelector).hide()
|
|
1954
|
+
return
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1957
|
+
var html = ''
|
|
1958
|
+
issues.forEach(function (issue) {
|
|
1959
|
+
var label = issue.title
|
|
1960
|
+
if (issue.count) { label += ' (' + issue.count + ')' }
|
|
1961
|
+
var detail = issue.summary || issue.description || ''
|
|
1962
|
+
html += '<p>' + label
|
|
1963
|
+
if (detail) { html += ' — ' + detail }
|
|
1964
|
+
html += '</p>'
|
|
1965
|
+
|
|
1966
|
+
html += renderReviewIssueBreakdown(issue)
|
|
1967
|
+
})
|
|
1968
|
+
$(listSelector).html(html)
|
|
1969
|
+
$(sectionSelector).show()
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
// Pick the right breakdown renderer for an issue on the Step 3 summary.
|
|
1973
|
+
function renderReviewIssueBreakdown(issue) {
|
|
1974
|
+
var items = issue.items || []
|
|
1975
|
+
if (!items.length) return ''
|
|
1976
|
+
|
|
1977
|
+
if (issue.type === 'row_level_errors' || issue.type === 'row_level_warnings') {
|
|
1978
|
+
return renderWarningCategoryBreakdown(items)
|
|
1979
|
+
}
|
|
1980
|
+
if (issue.type === 'missing_required_fields' && items.some(function (i) { return i.model })) {
|
|
1981
|
+
return renderMissingRequiredBreakdown(items)
|
|
1982
|
+
}
|
|
1983
|
+
return renderFieldListBreakdown(items)
|
|
1984
|
+
}
|
|
1985
|
+
|
|
1986
|
+
// Missing required fields grouped by model, matching the Step 1 accordion layout.
|
|
1987
|
+
function renderMissingRequiredBreakdown(items) {
|
|
1988
|
+
var grouped = groupItemsByModel(items)
|
|
1989
|
+
var html = '<ul class="review-warning-categories">'
|
|
1990
|
+
Object.keys(grouped).forEach(function (modelName) {
|
|
1991
|
+
var fields = grouped[modelName].map(escapeHtml).join(', ')
|
|
1992
|
+
html += '<li><strong>' + escapeHtml(modelName) + ':</strong> ' + fields + '</li>'
|
|
1993
|
+
})
|
|
1994
|
+
html += '</ul>'
|
|
1995
|
+
return html
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
// Flat list of `field — message` entries — used for unrecognized fields,
|
|
1999
|
+
// file references, notices. Message provides the context a bare field name
|
|
2000
|
+
// lacks (e.g. "No model column found — all rows will be imported as …").
|
|
2001
|
+
function renderFieldListBreakdown(items) {
|
|
2002
|
+
var html = '<ul class="review-warning-categories">'
|
|
2003
|
+
items.forEach(function (item) {
|
|
2004
|
+
if (!item || !item.field) return
|
|
2005
|
+
var line = escapeHtml(item.field)
|
|
2006
|
+
if (item.message) line += ' — ' + escapeHtml(item.message)
|
|
2007
|
+
html += '<li>' + line + '</li>'
|
|
2008
|
+
})
|
|
2009
|
+
html += '</ul>'
|
|
2010
|
+
return html
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
// Render a bulleted breakdown of row-warning items grouped by category.
|
|
2014
|
+
// Used on the Review & Start Import step under "Row Validation Warnings".
|
|
2015
|
+
function renderWarningCategoryBreakdown(items) {
|
|
2016
|
+
var counts = items.reduce(function (acc, item) {
|
|
2017
|
+
var cat = item.category || 'uncategorized'
|
|
2018
|
+
acc[cat] = (acc[cat] || 0) + 1
|
|
2019
|
+
return acc
|
|
2020
|
+
}, {})
|
|
2021
|
+
var html = '<ul class="review-warning-categories">'
|
|
2022
|
+
Object.keys(counts).forEach(function (cat) {
|
|
2023
|
+
html += '<li>' + escapeHtml(warningCategoryLabel(cat)) + ' (' + counts[cat] + ')</li>'
|
|
2024
|
+
})
|
|
2025
|
+
html += '</ul>'
|
|
2026
|
+
return html
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
// Localized label for a warning category, with a humanized fallback so a
|
|
2030
|
+
// new validator category doesn't produce raw snake_case in the UI.
|
|
2031
|
+
function warningCategoryLabel(category) {
|
|
2032
|
+
var key = 'warning_category_' + category
|
|
2033
|
+
var label = t(key)
|
|
2034
|
+
if (label === key) return humanizeCategory(category)
|
|
2035
|
+
return label
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
function humanizeCategory(category) {
|
|
2039
|
+
if (!category) return ''
|
|
2040
|
+
return category
|
|
2041
|
+
.replace(/_/g, ' ')
|
|
2042
|
+
.replace(/\b\w/g, function (c) { return c.toUpperCase() })
|
|
2043
|
+
}
|
|
2044
|
+
|
|
2045
|
+
// Count the display units for an issue. For missing-required-fields grouped
|
|
2046
|
+
// by model the unit is a model-group; for everything else it's an item.
|
|
2047
|
+
function issueDisplayUnitCount(issue) {
|
|
2048
|
+
if (!issue.items || !issue.items.length) return 0
|
|
2049
|
+
var hasModelField = issue.type === 'missing_required_fields' &&
|
|
2050
|
+
issue.items.some(function (item) { return item.model })
|
|
2051
|
+
if (!hasModelField) return issue.items.length
|
|
2052
|
+
|
|
2053
|
+
var seen = {}
|
|
2054
|
+
var groups = 0
|
|
2055
|
+
issue.items.forEach(function (item) {
|
|
2056
|
+
var key = item.model || 'Unknown'
|
|
2057
|
+
if (!seen[key]) {
|
|
2058
|
+
seen[key] = true
|
|
2059
|
+
groups += 1
|
|
2060
|
+
}
|
|
2061
|
+
})
|
|
2062
|
+
return groups
|
|
2063
|
+
}
|
|
2064
|
+
|
|
1858
2065
|
// Create accordion HTML
|
|
1859
2066
|
function createAccordion(title, icon, variant, count, defaultOpen, content) {
|
|
1860
2067
|
var variantClass = 'accordion-' + variant
|
|
@@ -2313,26 +2520,14 @@
|
|
|
2313
2520
|
|
|
2314
2521
|
$('.review-settings').html(settingsHtml)
|
|
2315
2522
|
|
|
2316
|
-
//
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
if (issue.count) { label += ' (' + issue.count + ')' }
|
|
2325
|
-
var detail = issue.summary || issue.description || ''
|
|
2326
|
-
warningsHtml += '<p>' + label
|
|
2327
|
-
if (detail) { warningsHtml += ' — ' + detail }
|
|
2328
|
-
warningsHtml += '</p>'
|
|
2329
|
-
})
|
|
2330
|
-
$('.review-warnings-list').html(warningsHtml)
|
|
2331
|
-
$('.review-warnings').show()
|
|
2332
|
-
} else {
|
|
2333
|
-
$('.review-warnings-list').empty()
|
|
2334
|
-
$('.review-warnings').hide()
|
|
2335
|
-
}
|
|
2523
|
+
// Errors and warnings — derive from messages.issues so every severity is covered.
|
|
2524
|
+
// Row-level errors/warnings get a per-category breakdown beneath the summary line.
|
|
2525
|
+
var allIssues = (data && data.messages && data.messages.issues) || []
|
|
2526
|
+
var errorIssues = allIssues.filter(function (issue) { return issue.severity === 'error' })
|
|
2527
|
+
var warningIssues = allIssues.filter(function (issue) { return issue.severity === 'warning' })
|
|
2528
|
+
|
|
2529
|
+
renderReviewIssueSection(errorIssues, '.review-errors', '.review-errors-list')
|
|
2530
|
+
renderReviewIssueSection(warningIssues, '.review-warnings', '.review-warnings-list')
|
|
2336
2531
|
|
|
2337
2532
|
// Large import warning
|
|
2338
2533
|
$('.total-items-count').text(totalItems)
|
|
@@ -50,20 +50,22 @@
|
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
.review-
|
|
54
|
-
|
|
55
|
-
color: $color-warning-dark;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
53
|
+
.review-errors h4 { color: $color-error-dark; }
|
|
54
|
+
.review-warnings h4 { color: $color-warning-dark; }
|
|
58
55
|
|
|
59
|
-
.review-
|
|
60
|
-
|
|
61
|
-
color: $color-warning-dark;
|
|
56
|
+
.review-errors-list { color: $color-error-dark; }
|
|
57
|
+
.review-warnings-list { color: $color-warning-dark; }
|
|
62
58
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
59
|
+
.review-errors-list,
|
|
60
|
+
.review-warnings-list {
|
|
61
|
+
.review-warning-categories {
|
|
62
|
+
margin: 4px 0 8px 0;
|
|
63
|
+
padding-left: 20px;
|
|
64
|
+
list-style: disc;
|
|
65
|
+
|
|
66
|
+
li {
|
|
67
|
+
font-size: 13px;
|
|
68
|
+
}
|
|
67
69
|
}
|
|
68
70
|
}
|
|
69
71
|
|
|
@@ -75,7 +75,7 @@ module Bulkrax
|
|
|
75
75
|
return unless defined?(::Hyrax)
|
|
76
76
|
add_breadcrumb t(:'hyrax.controls.home'), main_app.root_path
|
|
77
77
|
add_breadcrumb t(:'hyrax.dashboard.breadcrumbs.admin'), hyrax.dashboard_path
|
|
78
|
-
add_breadcrumb '
|
|
78
|
+
add_breadcrumb t(:'bulkrax.headings.importers'), bulkrax.importers_path
|
|
79
79
|
add_breadcrumb @importer.name, bulkrax.importer_path(@importer.id)
|
|
80
80
|
add_breadcrumb @entry.id
|
|
81
81
|
end
|
|
@@ -88,7 +88,7 @@ module Bulkrax
|
|
|
88
88
|
return unless defined?(::Hyrax)
|
|
89
89
|
add_breadcrumb t(:'hyrax.controls.home'), main_app.root_path
|
|
90
90
|
add_breadcrumb t(:'hyrax.dashboard.breadcrumbs.admin'), hyrax.dashboard_path
|
|
91
|
-
add_breadcrumb '
|
|
91
|
+
add_breadcrumb t(:'bulkrax.headings.exporters'), bulkrax.exporters_path
|
|
92
92
|
add_breadcrumb @exporter.name, bulkrax.exporter_path(@exporter.id)
|
|
93
93
|
add_breadcrumb @entry.id
|
|
94
94
|
end
|
|
@@ -48,7 +48,7 @@ module Bulkrax
|
|
|
48
48
|
@exporter = Exporter.new
|
|
49
49
|
return unless defined?(::Hyrax)
|
|
50
50
|
add_exporter_breadcrumbs
|
|
51
|
-
add_breadcrumb '
|
|
51
|
+
add_breadcrumb t(:'bulkrax.headings.new_exporter')
|
|
52
52
|
end
|
|
53
53
|
|
|
54
54
|
# GET /exporters/1/edit
|
|
@@ -56,7 +56,7 @@ module Bulkrax
|
|
|
56
56
|
if defined?(::Hyrax)
|
|
57
57
|
add_exporter_breadcrumbs
|
|
58
58
|
add_breadcrumb @exporter.name, bulkrax.exporter_path(@exporter.id)
|
|
59
|
-
add_breadcrumb '
|
|
59
|
+
add_breadcrumb t(:'bulkrax.headings.edit_exporter')
|
|
60
60
|
end
|
|
61
61
|
|
|
62
62
|
# Correctly populate export_source_collection input
|
|
@@ -141,7 +141,7 @@ module Bulkrax
|
|
|
141
141
|
def add_exporter_breadcrumbs
|
|
142
142
|
add_breadcrumb t(:'hyrax.controls.home'), main_app.root_path
|
|
143
143
|
add_breadcrumb t(:'hyrax.dashboard.breadcrumbs.admin'), hyrax.dashboard_path
|
|
144
|
-
add_breadcrumb '
|
|
144
|
+
add_breadcrumb t(:'bulkrax.headings.exporters'), bulkrax.exporters_path
|
|
145
145
|
end
|
|
146
146
|
|
|
147
147
|
# Download methods
|
|
@@ -48,6 +48,8 @@ module Bulkrax
|
|
|
48
48
|
end
|
|
49
49
|
|
|
50
50
|
def download_validation_errors
|
|
51
|
+
set_locale_from_params
|
|
52
|
+
|
|
51
53
|
cache_key = params[:key].to_s
|
|
52
54
|
expected_prefix = "guided_import_errors:#{session.id}:"
|
|
53
55
|
return head :not_found unless cache_key.start_with?(expected_prefix)
|
|
@@ -165,7 +167,7 @@ module Bulkrax
|
|
|
165
167
|
def add_importer_breadcrumbs
|
|
166
168
|
add_breadcrumb t(:'hyrax.controls.home'), main_app.root_path
|
|
167
169
|
add_breadcrumb t(:'hyrax.dashboard.breadcrumbs.admin'), hyrax.dashboard_path
|
|
168
|
-
add_breadcrumb '
|
|
170
|
+
add_breadcrumb t(:'bulkrax.headings.importers'), bulkrax.importers_path
|
|
169
171
|
end
|
|
170
172
|
|
|
171
173
|
def check_permissions
|
|
@@ -63,7 +63,7 @@ module Bulkrax
|
|
|
63
63
|
json_response('new')
|
|
64
64
|
elsif defined?(::Hyrax)
|
|
65
65
|
add_importer_breadcrumbs
|
|
66
|
-
add_breadcrumb '
|
|
66
|
+
add_breadcrumb t(:'bulkrax.headings.new_importer')
|
|
67
67
|
end
|
|
68
68
|
end
|
|
69
69
|
|
|
@@ -84,7 +84,7 @@ module Bulkrax
|
|
|
84
84
|
elsif defined?(::Hyrax)
|
|
85
85
|
add_importer_breadcrumbs
|
|
86
86
|
add_breadcrumb @importer.name, bulkrax.importer_path(@importer.id)
|
|
87
|
-
add_breadcrumb '
|
|
87
|
+
add_breadcrumb t(:'bulkrax.headings.edit_importer')
|
|
88
88
|
end
|
|
89
89
|
end
|
|
90
90
|
|
|
@@ -189,9 +189,9 @@ module Bulkrax
|
|
|
189
189
|
return unless defined?(::Hyrax)
|
|
190
190
|
add_breadcrumb t(:'hyrax.controls.home'), main_app.root_path
|
|
191
191
|
add_breadcrumb t(:'hyrax.dashboard.breadcrumbs.admin'), hyrax.dashboard_path
|
|
192
|
-
add_breadcrumb '
|
|
192
|
+
add_breadcrumb t(:'bulkrax.headings.importers'), bulkrax.importers_path
|
|
193
193
|
add_breadcrumb @importer.name, bulkrax.importer_path(@importer.id)
|
|
194
|
-
add_breadcrumb '
|
|
194
|
+
add_breadcrumb t(:'bulkrax.headings.upload_corrected_entries_action')
|
|
195
195
|
end
|
|
196
196
|
|
|
197
197
|
# POST /importer/1/upload_corrected_entries_file
|
|
@@ -313,7 +313,7 @@ module Bulkrax
|
|
|
313
313
|
def add_importer_breadcrumbs
|
|
314
314
|
add_breadcrumb t(:'hyrax.controls.home'), main_app.root_path
|
|
315
315
|
add_breadcrumb t(:'hyrax.dashboard.breadcrumbs.admin'), hyrax.dashboard_path
|
|
316
|
-
add_breadcrumb '
|
|
316
|
+
add_breadcrumb t(:'bulkrax.headings.importers'), bulkrax.importers_path
|
|
317
317
|
end
|
|
318
318
|
|
|
319
319
|
def setup_client(url)
|
|
@@ -33,12 +33,11 @@ module Bulkrax
|
|
|
33
33
|
end
|
|
34
34
|
|
|
35
35
|
def process_split
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
end
|
|
36
|
+
pattern = Bulkrax::SplitPatternCoercion.coerce(self.split)
|
|
37
|
+
return unless pattern
|
|
38
|
+
|
|
39
|
+
@result = @result.split(pattern)
|
|
40
|
+
@result = @result.map(&:strip).select(&:present?) unless self.split.is_a?(TrueClass)
|
|
42
41
|
end
|
|
43
42
|
|
|
44
43
|
def process_parse
|
|
@@ -165,7 +165,7 @@ module Bulkrax
|
|
|
165
165
|
def add_file
|
|
166
166
|
self.parsed_metadata['file'] ||= []
|
|
167
167
|
if record['file']&.is_a?(String)
|
|
168
|
-
self.parsed_metadata['file'] = record['file'].split(Bulkrax.
|
|
168
|
+
self.parsed_metadata['file'] = record['file'].split(Bulkrax::CsvParser.file_split_pattern)
|
|
169
169
|
elsif record['file'].is_a?(Array)
|
|
170
170
|
self.parsed_metadata['file'] = record['file']
|
|
171
171
|
end
|
|
@@ -13,6 +13,16 @@ module Bulkrax
|
|
|
13
13
|
true
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
+
# @return [Regexp] the pattern String#split should use on a `file` cell.
|
|
17
|
+
# Honours the `file` mapping's `split:` when set, otherwise falls back
|
|
18
|
+
# to {Bulkrax.multi_value_element_split_on}.
|
|
19
|
+
def self.file_split_pattern
|
|
20
|
+
file_mapping = Bulkrax.field_mappings.dig(to_s, 'file') ||
|
|
21
|
+
Bulkrax.field_mappings.dig(to_s, :file) || {}
|
|
22
|
+
split_value = file_mapping['split'] || file_mapping[:split]
|
|
23
|
+
Bulkrax::SplitPatternCoercion.coerce(split_value) || Bulkrax.multi_value_element_split_on
|
|
24
|
+
end
|
|
25
|
+
|
|
16
26
|
def records(_opts = {})
|
|
17
27
|
return @records if @records.present?
|
|
18
28
|
|
|
@@ -352,20 +362,13 @@ module Bulkrax
|
|
|
352
362
|
raise StandardError, 'No records were found' if records.blank?
|
|
353
363
|
return [] if importerexporter.metadata_only?
|
|
354
364
|
|
|
365
|
+
# Compute once — these don't vary per record.
|
|
366
|
+
file_mapping = Bulkrax.field_mappings.dig(self.class.to_s, 'file', :from)&.first&.to_sym || :file
|
|
367
|
+
split_pattern = self.class.file_split_pattern
|
|
368
|
+
files_dir = path_to_files
|
|
369
|
+
|
|
355
370
|
@file_paths ||= records.map do |r|
|
|
356
|
-
file_mapping = Bulkrax.field_mappings.dig(self.class.to_s, 'file', :from)&.first&.to_sym || :file
|
|
357
371
|
next if r[file_mapping].blank?
|
|
358
|
-
|
|
359
|
-
split_value = Bulkrax.field_mappings.dig(self.class.to_s, :file, :split)
|
|
360
|
-
split_pattern = case split_value
|
|
361
|
-
when Regexp
|
|
362
|
-
split_value
|
|
363
|
-
when String
|
|
364
|
-
Regexp.new(split_value)
|
|
365
|
-
else
|
|
366
|
-
Bulkrax.multi_value_element_split_on
|
|
367
|
-
end
|
|
368
|
-
files_dir = path_to_files
|
|
369
372
|
raise StandardError, "Record references local files but no files directory could be resolved from the import path" if files_dir.nil?
|
|
370
373
|
|
|
371
374
|
r[file_mapping].split(split_pattern).map do |f|
|
|
@@ -27,7 +27,10 @@ module Bulkrax
|
|
|
27
27
|
|
|
28
28
|
def initialize(models: nil, admin_set_id: nil)
|
|
29
29
|
@admin_set_id = admin_set_id
|
|
30
|
-
|
|
30
|
+
# Template generation excludes system-maintained fields (generated:
|
|
31
|
+
# true) so users don't see columns like date_uploaded, depositor,
|
|
32
|
+
# etc. on the downloadable template.
|
|
33
|
+
@mapping_manager = CsvTemplate::MappingManager.new(include_generated: false)
|
|
31
34
|
@mappings = @mapping_manager.mappings
|
|
32
35
|
@field_analyzer = CsvTemplate::FieldAnalyzer.new(@mappings, admin_set_id)
|
|
33
36
|
@all_models = CsvTemplate::ModelLoader.new(Array.wrap(models)).models
|