geoblacklight_admin 0.6.3 → 0.7.0
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 +2 -1
- data/app/assets/stylesheets/geoblacklight_admin/_core.scss +1 -1
- data/app/assets/stylesheets/geoblacklight_admin/modules/_icons.scss +9 -0
- data/app/controllers/admin/document_data_dictionaries_controller.rb +126 -0
- data/app/controllers/admin/document_data_dictionary_entries_controller.rb +124 -0
- data/app/controllers/admin/document_distributions_controller.rb +0 -38
- data/app/controllers/admin/import_distributions_controller.rb +126 -0
- data/app/javascript/index.js +1 -1
- data/app/jobs/import_distributions_run_job.rb +32 -0
- data/app/jobs/import_document_distribution_job.rb +23 -0
- data/app/models/document.rb +5 -0
- data/app/models/document_data_dictionary/csv_header_validator.rb +30 -0
- data/app/models/document_data_dictionary.rb +39 -0
- data/app/models/document_data_dictionary_entry.rb +9 -0
- data/app/models/import_distribution/csv_header_validator.rb +32 -0
- data/app/models/import_distribution.rb +55 -0
- data/app/models/import_distribution_state_machine.rb +14 -0
- data/app/models/import_distribution_transition.rb +26 -0
- data/app/models/import_document_distribution.rb +25 -0
- data/app/models/import_document_distribution_state_machine.rb +13 -0
- data/app/models/import_document_distribution_transition.rb +19 -0
- data/app/views/admin/document_data_dictionaries/_data_dictionaries_table.html.erb +40 -0
- data/app/views/admin/document_data_dictionaries/_document_data_dictionary.html.erb +37 -0
- data/app/views/admin/document_data_dictionaries/_document_data_dictionary.json.jbuilder +2 -0
- data/app/views/admin/document_data_dictionaries/_form.html.erb +17 -0
- data/app/views/admin/document_data_dictionaries/edit.html.erb +12 -0
- data/app/views/admin/document_data_dictionaries/index.html.erb +45 -0
- data/app/views/admin/document_data_dictionaries/index.json.jbuilder +1 -0
- data/app/views/admin/document_data_dictionaries/new.html.erb +78 -0
- data/app/views/admin/document_data_dictionaries/show.html.erb +78 -0
- data/app/views/admin/document_data_dictionaries/show.json.jbuilder +1 -0
- data/app/views/admin/document_data_dictionary_entries/_form.html.erb +19 -0
- data/app/views/admin/document_data_dictionary_entries/edit.html.erb +12 -0
- data/app/views/admin/document_data_dictionary_entries/new.html.erb +16 -0
- data/app/views/admin/document_distributions/index.html.erb +2 -2
- data/app/views/admin/documents/_form_nav.html.erb +4 -0
- data/app/views/admin/import_distributions/_form.html.erb +21 -0
- data/app/views/admin/import_distributions/_import_distribution.json.jbuilder +5 -0
- data/app/views/admin/import_distributions/_show_failed_tab.html.erb +36 -0
- data/app/views/admin/import_distributions/_show_success_tab.html.erb +35 -0
- data/app/views/admin/import_distributions/edit.html.erb +8 -0
- data/app/views/admin/import_distributions/index.html.erb +58 -0
- data/app/views/admin/import_distributions/index.json.jbuilder +3 -0
- data/app/views/admin/import_distributions/new.html.erb +7 -0
- data/app/views/admin/import_distributions/show.html.erb +121 -0
- data/app/views/admin/import_distributions/show.json.jbuilder +3 -0
- data/app/views/admin/imports/index.html.erb +2 -2
- data/app/views/admin/shared/_navbar.html.erb +2 -2
- data/app/views/catalog/_gbl_admin_data_dictionaries.html.erb +3 -0
- data/app/views/catalog/_show_gbl_admin_data_dictionaries.html.erb +44 -0
- data/app/views/catalog/data_dictionaries.html.erb +12 -0
- data/db/migrate/20241204163117_create_document_data_dictionaries.rb +14 -0
- data/db/migrate/20241218174455_create_document_data_dictionary_entries.rb +17 -0
- data/db/migrate/20250113213655_import_distributions.rb +56 -0
- data/lib/generators/geoblacklight_admin/config_generator.rb +61 -4
- data/lib/generators/geoblacklight_admin/templates/btaa_sample_document_data_dictionary_entries.csv +9 -0
- data/lib/generators/geoblacklight_admin/templates/btaa_sample_document_distributions.csv +17 -0
- data/lib/geoblacklight_admin/routes/data_dictionariesable.rb +17 -0
- data/lib/geoblacklight_admin/version.rb +1 -1
- metadata +49 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a28b441bd6abd075d9a44800659b3fd9d28a44a7eafc44b6cd5a35105c63c0c6
|
4
|
+
data.tar.gz: 98a2f2f6b11314db59f5d52f79b16c8a7d405322a7a59c8b3d3214ad7030e541
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 271fd18a5c03887aeffc871857c1c7ab1f79c40d1a2f8e054f5834bca57fcd890a5664353ceb34978e6df1849b00471d15388e41e34b27bfe2fb9dd88317f5eb
|
7
|
+
data.tar.gz: 27e668d0c301630f549705d930d8c182b31989f3f36b7e3d22be44b25f82123ddf2552804c0e604b0a30f060304272b150883cf310a88a5cd81152dba0b816f6
|
data/README.md
CHANGED
@@ -69,7 +69,8 @@ The gem is available as open source under the terms of the [Apache 2.0 License](
|
|
69
69
|
* ~~Debug Rails 7.2 support (remove devise_invitable, see [#915](https://github.com/scambra/devise_invitable/issues/915))~~
|
70
70
|
* ~~Separate dct_references_s support into a separate model~~
|
71
71
|
* ~~Import/Export dct_references_s outside of the main document model~~
|
72
|
-
*
|
72
|
+
* ~~Distributions: Move import to background queue~~
|
73
|
+
* ~~Data Dictionary: Add support for `document_data_dictionary`~~
|
73
74
|
* Gazetteer: Add GeoNames support
|
74
75
|
* Gazetteer: Add Who's On First support
|
75
76
|
* Gazetteer: Add Ollama support
|
@@ -13,3 +13,12 @@ svg.icon.baseline {
|
|
13
13
|
top: -0.125em;
|
14
14
|
position: relative;
|
15
15
|
}
|
16
|
+
|
17
|
+
// Sidebar Action Icons
|
18
|
+
#dataDictionariesLink {
|
19
|
+
margin-left:0rem;
|
20
|
+
padding-left: 1.5rem;
|
21
|
+
background-position: left center;
|
22
|
+
background-repeat:no-repeat;
|
23
|
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' version='1.1' width='14' height='14' viewBox='0 0 512 512' aria-label='Table' role='img'%3E%3Ctitle%3ETable%3C/title%3E%3Cpath fill='%236c757d' d='M464 32H48C21.49 32 0 53.49 0 80v352c0 26.51 21.49 48 48 48h416c26.51 0 48-21.49 48-48V80c0-26.51-21.49-48-48-48zM48 80h416v64H48V80zm0 128h128v64H48v-64zm0 128h128v64H48v-64zm192 64v-64h128v64H240zm128-128H240v-64h128v64zm64 128v-64h128v64H432zm0-128h128v64H432v-64z'%3E%3C/path%3E%3C/svg%3E");
|
24
|
+
}
|
@@ -0,0 +1,126 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Admin::DocumentDataDictionariesController
|
4
|
+
#
|
5
|
+
# This controller manages the document data dictionaries within the admin namespace.
|
6
|
+
# It provides actions to list, show, edit, update, and destroy data dictionaries.
|
7
|
+
module Admin
|
8
|
+
class DocumentDataDictionariesController < Admin::AdminController
|
9
|
+
before_action :set_document
|
10
|
+
before_action :set_document_data_dictionary, only: %i[show edit update destroy]
|
11
|
+
|
12
|
+
# GET /document_data_dictionaries or /document_data_dictionaries.json
|
13
|
+
def index
|
14
|
+
@document_data_dictionaries = DocumentDataDictionary.all
|
15
|
+
if params[:document_id]
|
16
|
+
@document_data_dictionaries = DocumentDataDictionary.where(friendlier_id: @document.friendlier_id).order(position: :asc)
|
17
|
+
else
|
18
|
+
@pagy, @document_data_dictionaries = pagy(DocumentDataDictionary.all.order(friendlier_id: :asc, updated_at: :desc), items: 20)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# GET /document_data_dictionaries/1 or /document_data_dictionaries/1.json
|
23
|
+
def show
|
24
|
+
@pagy, @document_data_dictionary_entries = pagy(@document_data_dictionary.document_data_dictionary_entries.order(position: :asc), items: 100)
|
25
|
+
end
|
26
|
+
|
27
|
+
# GET /document_data_dictionaries/new
|
28
|
+
def new
|
29
|
+
@document_data_dictionary = DocumentDataDictionary.new
|
30
|
+
end
|
31
|
+
|
32
|
+
# GET /document_data_dictionaries/1/edit
|
33
|
+
def edit
|
34
|
+
end
|
35
|
+
|
36
|
+
# POST /document_data_dictionaries or /document_data_dictionaries.json
|
37
|
+
def create
|
38
|
+
@document_data_dictionary = DocumentDataDictionary.new(document_data_dictionary_params)
|
39
|
+
|
40
|
+
respond_to do |format|
|
41
|
+
if @document_data_dictionary.save
|
42
|
+
format.html { redirect_to admin_document_document_data_dictionaries_path(@document), notice: "Document data dictionary was successfully created." }
|
43
|
+
format.json { render :show, status: :created, location: @document_data_dictionary }
|
44
|
+
else
|
45
|
+
logger.debug("Document data dictionary could not be created. #{@document_data_dictionary.errors.full_messages}")
|
46
|
+
format.html { render :new, status: :unprocessable_entity }
|
47
|
+
format.json { render json: @document_data_dictionary.errors, status: :unprocessable_entity }
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# PATCH/PUT /document_data_dictionaries/1 or /document_data_dictionaries/1.json
|
53
|
+
def update
|
54
|
+
respond_to do |format|
|
55
|
+
if @document_data_dictionary.update(document_data_dictionary_params)
|
56
|
+
format.html { redirect_to admin_document_document_data_dictionaries_path(@document), notice: "Document data dictionary was successfully updated." }
|
57
|
+
format.json { render :show, status: :ok, location: @document_data_dictionary }
|
58
|
+
else
|
59
|
+
format.html { render :edit, status: :unprocessable_entity }
|
60
|
+
format.json { render json: @document_data_dictionary.errors, status: :unprocessable_entity }
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# DELETE /document_data_dictionaries/1 or /document_data_dictionaries/1.json
|
66
|
+
def destroy
|
67
|
+
@document_data_dictionary.destroy!
|
68
|
+
|
69
|
+
respond_to do |format|
|
70
|
+
format.html { redirect_to admin_document_document_data_dictionaries_path(@document), status: :see_other, notice: "Document data dictionary was successfully destroyed." }
|
71
|
+
format.json { head :no_content }
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# DELETE /admin/document_data_dictionaries/destroy_all
|
76
|
+
#
|
77
|
+
# Destroys all document data dictionaries provided in the file parameter. If successful, redirects
|
78
|
+
# with a success notice. Otherwise, redirects with an error notice.
|
79
|
+
def destroy_all
|
80
|
+
return if request.get?
|
81
|
+
|
82
|
+
logger.debug("Destroy Data Dictionaries")
|
83
|
+
unless params.dig(:document_data_dictionary, :data_dictionaries, :file)
|
84
|
+
raise ArgumentError, "File does not exist or is invalid."
|
85
|
+
end
|
86
|
+
|
87
|
+
respond_to do |format|
|
88
|
+
if DocumentDataDictionary.destroy_all(params.dig(:document_data_dictionary, :data_dictionaries, :file))
|
89
|
+
format.html { redirect_to admin_document_document_data_dictionaries_path, notice: "Data dictionaries were destroyed." }
|
90
|
+
else
|
91
|
+
format.html { redirect_to admin_document_document_data_dictionaries_path, notice: "Data dictionaries could not be destroyed." }
|
92
|
+
end
|
93
|
+
rescue => e
|
94
|
+
format.html { redirect_to admin_document_document_data_dictionaries_path, notice: "Data dictionaries could not be destroyed. #{e}" }
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
private
|
99
|
+
|
100
|
+
# Sets the document based on the document_id parameter.
|
101
|
+
# If not nested, it does nothing.
|
102
|
+
def set_document
|
103
|
+
return unless params[:document_id] # If not nested
|
104
|
+
|
105
|
+
@document = Document.includes(:leaf_representative).find_by!(friendlier_id: params[:document_id])
|
106
|
+
end
|
107
|
+
|
108
|
+
# Sets the document data dictionary based on the id parameter.
|
109
|
+
def set_document_data_dictionary
|
110
|
+
@document_data_dictionary = DocumentDataDictionary.find(params[:id])
|
111
|
+
end
|
112
|
+
|
113
|
+
# Only allow a list of trusted parameters through.
|
114
|
+
def document_data_dictionary_params
|
115
|
+
params.require(:document_data_dictionary).permit(
|
116
|
+
:friendlier_id,
|
117
|
+
:name,
|
118
|
+
:description,
|
119
|
+
:staff_notes,
|
120
|
+
:tags,
|
121
|
+
:csv_file,
|
122
|
+
:position
|
123
|
+
)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Admin::DocumentDataDictionaryEntriesController
|
4
|
+
#
|
5
|
+
# This controller manages the document data dictionary entries within the admin namespace.
|
6
|
+
# It provides actions to list, show, edit, update, destroy, and import data dictionaries.
|
7
|
+
module Admin
|
8
|
+
class DocumentDataDictionaryEntriesController < Admin::AdminController
|
9
|
+
before_action :set_document
|
10
|
+
before_action :set_document_data_dictionary
|
11
|
+
before_action :set_document_data_dictionary_entry, only: %i[edit update destroy]
|
12
|
+
|
13
|
+
# GET /document_data_dictionaries/1/entries/new
|
14
|
+
def new
|
15
|
+
@document_data_dictionary_entry = DocumentDataDictionaryEntry.new
|
16
|
+
end
|
17
|
+
|
18
|
+
# GET /document_data_dictionaries/1/entries/1/edit
|
19
|
+
def edit
|
20
|
+
end
|
21
|
+
|
22
|
+
# POST /document_data_dictionaries/1/entries or /document_data_dictionaries/1/entries.json
|
23
|
+
def create
|
24
|
+
@document_data_dictionary_entry = DocumentDataDictionaryEntry.new(document_data_dictionary_entry_params)
|
25
|
+
|
26
|
+
respond_to do |format|
|
27
|
+
if @document_data_dictionary_entry.save
|
28
|
+
format.html { redirect_to admin_document_document_data_dictionary_path(@document, @document_data_dictionary), notice: "Document data dictionary entry was successfully created." }
|
29
|
+
format.json { render :show, status: :created, location: @document_data_dictionary_entry }
|
30
|
+
else
|
31
|
+
logger.debug("Document data dictionary entry could not be created. #{@document_data_dictionary_entry.errors.full_messages}")
|
32
|
+
format.html { render :new, status: :unprocessable_entity }
|
33
|
+
format.json { render json: @document_data_dictionary_entry.errors, status: :unprocessable_entity }
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# PATCH/PUT /document_data_dictionaries/1/entries/1 or /document_data_dictionaries/1/entries/1.json
|
39
|
+
def update
|
40
|
+
respond_to do |format|
|
41
|
+
if @document_data_dictionary_entry.update(document_data_dictionary_entry_params)
|
42
|
+
format.html { redirect_to admin_document_document_data_dictionary_path(@document, @document_data_dictionary), notice: "Document data dictionary entry was successfully updated." }
|
43
|
+
format.json { render :show, status: :ok, location: @document_data_dictionary_entry }
|
44
|
+
else
|
45
|
+
logger.debug("Document data dictionary entry could not be updated. #{@document_data_dictionary_entry.errors.full_messages}")
|
46
|
+
format.html { render :edit, status: :unprocessable_entity }
|
47
|
+
format.json { render json: @document_data_dictionary_entry.errors, status: :unprocessable_entity }
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# DELETE /document_data_dictionaries/1/entries/1 or /document_data_dictionaries/1/entries/1.json
|
53
|
+
def destroy
|
54
|
+
@document_data_dictionary_entry.destroy!
|
55
|
+
|
56
|
+
respond_to do |format|
|
57
|
+
format.html { redirect_to admin_document_document_data_dictionary_path(@document, @document_data_dictionary), status: :see_other, notice: "Document data dictionary entry was successfully destroyed." }
|
58
|
+
format.json { head :no_content }
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# DELETE /admin/document_data_dictionaries/1/entries/destroy_all
|
63
|
+
#
|
64
|
+
# Destroys all document data dictionaries provided in the file parameter. If successful, redirects
|
65
|
+
# with a success notice. Otherwise, redirects with an error notice.
|
66
|
+
def destroy_all
|
67
|
+
return if request.get?
|
68
|
+
|
69
|
+
logger.debug("Destroy Data Dictionary Entries")
|
70
|
+
|
71
|
+
respond_to do |format|
|
72
|
+
if @document_data_dictionary.document_data_dictionary_entries.destroy_all
|
73
|
+
format.html { redirect_to admin_document_document_data_dictionary_document_data_dictionary_entries_path(@document_data_dictionary.friendlier_id, @document_data_dictionary), notice: "Data dictionary entries were destroyed." }
|
74
|
+
else
|
75
|
+
format.html { redirect_to admin_document_document_data_dictionary_document_data_dictionary_entries_path(@document_data_dictionary.friendlier_id, @document_data_dictionary), notice: "Data dictionary entries could not be destroyed." }
|
76
|
+
end
|
77
|
+
rescue => e
|
78
|
+
format.html { redirect_to admin_document_document_data_dictionary_document_data_dictionary_entries_path(@document_data_dictionary.friendlier_id, @document_data_dictionary), notice: "Data dictionary entries could not be destroyed. #{e}" }
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# POST /document_data_dictionaries/1/entries/sort
|
83
|
+
# Sorts document data dictionary entries based on the provided list of IDs.
|
84
|
+
# Renders an empty response body.
|
85
|
+
def sort
|
86
|
+
DocumentDataDictionary.sort_entries(params[:id_list])
|
87
|
+
render body: nil
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
# Sets the document based on the document_id parameter.
|
93
|
+
# If not nested, it does nothing.
|
94
|
+
def set_document
|
95
|
+
return unless params[:document_id] # If not nested
|
96
|
+
@document = Document.includes(:leaf_representative).find_by!(friendlier_id: params[:document_id])
|
97
|
+
end
|
98
|
+
|
99
|
+
# Sets the document data dictionary based on the id parameter.
|
100
|
+
def set_document_data_dictionary
|
101
|
+
@document_data_dictionary = DocumentDataDictionary.find(params[:document_data_dictionary_id])
|
102
|
+
end
|
103
|
+
|
104
|
+
# Sets the document data dictionary entry based on the id parameter.
|
105
|
+
def set_document_data_dictionary_entry
|
106
|
+
@document_data_dictionary_entry = DocumentDataDictionaryEntry.find(params[:id])
|
107
|
+
end
|
108
|
+
|
109
|
+
# Only allow a list of trusted parameters through.
|
110
|
+
def document_data_dictionary_entry_params
|
111
|
+
params.require(:document_data_dictionary_entry).permit(
|
112
|
+
:friendlier_id,
|
113
|
+
:document_data_dictionary_id,
|
114
|
+
:field_name,
|
115
|
+
:field_type,
|
116
|
+
:values,
|
117
|
+
:definition,
|
118
|
+
:definition_source,
|
119
|
+
:parent_field_name,
|
120
|
+
:position
|
121
|
+
)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
@@ -112,44 +112,6 @@ module Admin
|
|
112
112
|
end
|
113
113
|
end
|
114
114
|
|
115
|
-
# GET/POST /documents/1/distributions/import
|
116
|
-
#
|
117
|
-
# Imports document distributions from a file. If successful, redirects with a success notice.
|
118
|
-
# Otherwise, redirects with an error notice.
|
119
|
-
def import
|
120
|
-
return if request.get?
|
121
|
-
|
122
|
-
logger.debug("Import Distributions")
|
123
|
-
|
124
|
-
unless params.dig(:document_distribution, :distributions, :file)
|
125
|
-
raise ArgumentError, "File does not exist or is invalid."
|
126
|
-
end
|
127
|
-
|
128
|
-
success, errors = DocumentDistribution.import(params.dig(:document_distribution, :distributions, :file))
|
129
|
-
if success == true
|
130
|
-
logger.debug("Distributions were created successfully.")
|
131
|
-
if params[:document_id]
|
132
|
-
redirect_to admin_document_document_distributions_path(@document), notice: "Distributions were created successfully."
|
133
|
-
else
|
134
|
-
redirect_to admin_document_distributions_path, notice: "Distributions were created successfully."
|
135
|
-
end
|
136
|
-
else
|
137
|
-
logger.debug("Some distributions could not be created. #{errors.join(", ")}")
|
138
|
-
if params[:document_id]
|
139
|
-
redirect_to admin_document_document_distributions_path(@document), notice: "Some distributions could not be created. #{errors.join(", ")}"
|
140
|
-
else
|
141
|
-
redirect_to admin_document_distributions_path, notice: "Some distributions could not be created. #{errors.join(", ")}"
|
142
|
-
end
|
143
|
-
end
|
144
|
-
rescue => e
|
145
|
-
logger.debug("Distributions could not be created. #{e}")
|
146
|
-
if params[:document_id]
|
147
|
-
redirect_to admin_document_document_distributions_path(@document), notice: "Distributions could not be created. #{e}"
|
148
|
-
else
|
149
|
-
redirect_to admin_document_distributions_path, notice: "Distributions could not be created. #{e}"
|
150
|
-
end
|
151
|
-
end
|
152
|
-
|
153
115
|
private
|
154
116
|
|
155
117
|
# Sets the document based on the document_id parameter.
|
@@ -0,0 +1,126 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Admin::ImportDistributionsController
|
4
|
+
#
|
5
|
+
# This controller handles the CRUD operations for ImportDistribution objects within the admin namespace.
|
6
|
+
# It provides actions to list, show, create, update, and delete import distributions, as well as run an import distribution.
|
7
|
+
#
|
8
|
+
# Before Actions:
|
9
|
+
# - set_import_distribution: Sets the @import_distribution instance variable for actions that require an import distribution ID.
|
10
|
+
#
|
11
|
+
# Actions:
|
12
|
+
# - index: Lists all imports with pagination.
|
13
|
+
# - show: Displays a specific import and its associated documents, with pagination for success and failed states.
|
14
|
+
# - new: Initializes a new Import object.
|
15
|
+
# - edit: Prepares an existing Import object for editing.
|
16
|
+
# - create: Creates a new Import object and redirects to import mappings if successful.
|
17
|
+
# - update: Updates an existing Import object and redirects to the import if successful.
|
18
|
+
# - destroy: Deletes an Import object and redirects to the imports list.
|
19
|
+
# - run: Executes the import process and redirects to the import show page.
|
20
|
+
#
|
21
|
+
# Private Methods:
|
22
|
+
# - set_import: Finds and sets the import based on the provided ID.
|
23
|
+
# - permittable_params: Returns an array of permitted parameters for import.
|
24
|
+
# - import_params: Permits parameters for creating or updating an import, including nested attributes.
|
25
|
+
module Admin
|
26
|
+
class ImportDistributionsController < Admin::AdminController
|
27
|
+
before_action :set_import_distribution, only: %i[show edit update destroy run]
|
28
|
+
|
29
|
+
# GET /import_distributions
|
30
|
+
# GET /import_distributions.json
|
31
|
+
# Lists all import distributions with pagination.
|
32
|
+
def index
|
33
|
+
@pagy, @import_distributions = pagy(ImportDistribution.all.order("created_at DESC"), items: 20)
|
34
|
+
end
|
35
|
+
|
36
|
+
# GET /import_distributions/1
|
37
|
+
# GET /import_distributions/1.json
|
38
|
+
# Displays a specific import distribution and its associated documents, with pagination for success and failed states.
|
39
|
+
def show
|
40
|
+
@pagy_failed, @import_failed_distributions = pagy(@import_distribution.import_document_distributions.not_in_state(:success), items: 50, page_param: :failed_page)
|
41
|
+
@pagy_success, @import_success_distributions = pagy(@import_distribution.import_document_distributions.in_state(:success), items: 50, page_param: :success_page)
|
42
|
+
end
|
43
|
+
|
44
|
+
# GET /import_distributions/new
|
45
|
+
# Initializes a new ImportDistribution object.
|
46
|
+
def new
|
47
|
+
@import_distribution = ImportDistribution.new
|
48
|
+
end
|
49
|
+
|
50
|
+
# GET /import_distributions/1/edit
|
51
|
+
# Prepares an existing ImportDistribution object for editing.
|
52
|
+
def edit
|
53
|
+
end
|
54
|
+
|
55
|
+
# POST /import_distributions
|
56
|
+
# POST /import_distributions.json
|
57
|
+
# Creates a new ImportDistribution object
|
58
|
+
def create
|
59
|
+
@import_distribution = ImportDistribution.new(import_distribution_params)
|
60
|
+
|
61
|
+
respond_to do |format|
|
62
|
+
if @import_distribution.save
|
63
|
+
format.html do
|
64
|
+
redirect_to admin_import_distribution_path(@import_distribution),
|
65
|
+
notice: "Import distribution was successful."
|
66
|
+
end
|
67
|
+
format.json { render :show, status: :created, location: @import_distribution }
|
68
|
+
else
|
69
|
+
format.html { render :new, status: :unprocessable_entity }
|
70
|
+
format.json { render json: @import_distribution.errors, status: :unprocessable_entity }
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# PATCH/PUT /import_distributions/1
|
76
|
+
# PATCH/PUT /import_distributions/1.json
|
77
|
+
# Updates an existing ImportDistribution object and redirects to the import distribution if successful.
|
78
|
+
def update
|
79
|
+
respond_to do |format|
|
80
|
+
if @import_distribution.update(import_distribution_params)
|
81
|
+
format.html { redirect_to admin_import_distribution_path(@import_distribution), notice: "Import distribution was successfully updated." }
|
82
|
+
format.json { render :show, status: :ok, location: @import_distribution }
|
83
|
+
else
|
84
|
+
format.html { render :edit, status: :unprocessable_entity }
|
85
|
+
format.json { render json: @import_distribution.errors, status: :unprocessable_entity }
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# DELETE /import_distributions/1
|
91
|
+
# DELETE /import_distributions/1.json
|
92
|
+
# Deletes an ImportDistribution object and redirects to the import distributions list.
|
93
|
+
def destroy
|
94
|
+
@import_distribution.destroy
|
95
|
+
respond_to do |format|
|
96
|
+
format.html { redirect_to admin_import_distributions_url, notice: "Import distribution was successfully destroyed." }
|
97
|
+
format.json { head :no_content }
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# Runs the import process and redirects to the import show page.
|
102
|
+
def run
|
103
|
+
@import_distribution.run!
|
104
|
+
redirect_to admin_import_distribution_url(@import_distribution), notice: "Import distribution is running. Check back soon for results."
|
105
|
+
end
|
106
|
+
|
107
|
+
private
|
108
|
+
|
109
|
+
# Use callbacks to share common setup or constraints between actions.
|
110
|
+
# Finds and sets the import distribution based on the provided ID.
|
111
|
+
def set_import_distribution
|
112
|
+
@import_distribution = ImportDistribution.find(params[:id])
|
113
|
+
end
|
114
|
+
|
115
|
+
# Returns an array of permitted parameters for import distribution.
|
116
|
+
def permittable_params
|
117
|
+
%i[name filename source description row_count encoding content_type extension validity validation_result
|
118
|
+
csv_file run]
|
119
|
+
end
|
120
|
+
|
121
|
+
# Permits parameters for creating or updating an import distribution, including nested attributes.
|
122
|
+
def import_distribution_params
|
123
|
+
params.require(:import_distribution).permit(permittable_params)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
data/app/javascript/index.js
CHANGED
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# ImportDistributionsRunJob class
|
4
|
+
class ImportDistributionsRunJob < ApplicationJob
|
5
|
+
queue_as :priority
|
6
|
+
|
7
|
+
def perform(import)
|
8
|
+
data = CSV.parse(import.csv_file.download, headers: true)
|
9
|
+
|
10
|
+
data.each do |dist|
|
11
|
+
extract_hash = dist.to_h
|
12
|
+
|
13
|
+
document_distribution_hash = {
|
14
|
+
friendlier_id: extract_hash["friendlier_id"],
|
15
|
+
reference_type: extract_hash["reference_type"],
|
16
|
+
distribution_url: extract_hash["distribution_url"],
|
17
|
+
label: extract_hash["label"],
|
18
|
+
import_distribution_id: import.id
|
19
|
+
}
|
20
|
+
|
21
|
+
# Capture document distribution for import attempt
|
22
|
+
import_document_distribution = ImportDocumentDistribution.create(document_distribution_hash)
|
23
|
+
|
24
|
+
# Add import document distribution to background job queue
|
25
|
+
ImportDocumentDistributionJob.perform_later(import_document_distribution)
|
26
|
+
rescue => e
|
27
|
+
logger.debug "\n\nCANNOT IMPORT DISTRIBUTION: #{extract_hash.inspect}"
|
28
|
+
logger.debug "Error: #{e.inspect}\n\n"
|
29
|
+
next
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# ImportDocumentDistributionJob class
|
4
|
+
class ImportDocumentDistributionJob < ApplicationJob
|
5
|
+
queue_as :priority
|
6
|
+
|
7
|
+
def perform(import_document_distribution)
|
8
|
+
document_distribution = DocumentDistribution.find_or_create_by(
|
9
|
+
friendlier_id: import_document_distribution.friendlier_id,
|
10
|
+
reference_type: ReferenceType.find_by(name: import_document_distribution.reference_type),
|
11
|
+
url: import_document_distribution.distribution_url
|
12
|
+
)
|
13
|
+
|
14
|
+
if document_distribution.update(import_document_distribution.to_hash)
|
15
|
+
import_document_distribution.state_machine.transition_to!(:success)
|
16
|
+
else
|
17
|
+
import_document_distribution.state_machine.transition_to!(:failed, "Failed - #{document_distribution.errors.inspect}")
|
18
|
+
end
|
19
|
+
rescue => e
|
20
|
+
logger.debug("Error: #{e}")
|
21
|
+
import_document_distribution.state_machine.transition_to!(:failed, "Error - #{e.inspect}")
|
22
|
+
end
|
23
|
+
end
|
data/app/models/document.rb
CHANGED
@@ -21,6 +21,7 @@ class Document < Kithe::Work
|
|
21
21
|
# - Publication State
|
22
22
|
has_many :document_transitions, foreign_key: "kithe_model_id", autosave: false, dependent: :destroy,
|
23
23
|
inverse_of: :document
|
24
|
+
|
24
25
|
# - Thumbnail State
|
25
26
|
has_many :document_thumbnail_transitions, foreign_key: "kithe_model_id", autosave: false, dependent: :destroy,
|
26
27
|
inverse_of: :document
|
@@ -34,6 +35,10 @@ class Document < Kithe::Work
|
|
34
35
|
has_many :document_downloads, primary_key: "friendlier_id", foreign_key: "friendlier_id", autosave: false, dependent: :destroy,
|
35
36
|
inverse_of: :document
|
36
37
|
|
38
|
+
# - DocumentDataDictionaries
|
39
|
+
has_many :document_data_dictionaries, primary_key: "friendlier_id", foreign_key: "friendlier_id", autosave: false, dependent: :destroy,
|
40
|
+
inverse_of: :document
|
41
|
+
|
37
42
|
# - DocumentDistributions
|
38
43
|
has_many :document_distributions, primary_key: "friendlier_id", foreign_key: "friendlier_id", autosave: false, dependent: :destroy,
|
39
44
|
inverse_of: :document
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "csv"
|
4
|
+
|
5
|
+
# CSV Header Validation
|
6
|
+
class DocumentDataDictionary
|
7
|
+
# CsvHeaderValidator
|
8
|
+
class CsvHeaderValidator < ActiveModel::Validator
|
9
|
+
def validate(record)
|
10
|
+
valid_csv_header = true
|
11
|
+
unless valid_csv_headers?(record&.csv_file)
|
12
|
+
valid_csv_header = false
|
13
|
+
record.errors.add(:csv_file,
|
14
|
+
"Missing a required CSV header. friendlier_id, field_name, field_type, values, definition, definition_source, and parent_field_name are required.")
|
15
|
+
|
16
|
+
# Log the CSV file content
|
17
|
+
Rails.logger.error("CSV validation failed. CSV content: #{record.csv_file.download}")
|
18
|
+
end
|
19
|
+
|
20
|
+
valid_csv_header
|
21
|
+
end
|
22
|
+
|
23
|
+
def valid_csv_headers?(csv_file)
|
24
|
+
headers = CSV.parse(csv_file.download)[0]
|
25
|
+
(["friendlier_id", "field_name", "field_type", "values", "definition", "definition_source", "parent_field_name"] - headers).empty?
|
26
|
+
rescue ArgumentError, ActiveStorage::FileNotFoundError
|
27
|
+
false
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "csv"
|
4
|
+
|
5
|
+
class DocumentDataDictionary < ApplicationRecord
|
6
|
+
include ActiveModel::Validations
|
7
|
+
|
8
|
+
# Callbacks (keep at top)
|
9
|
+
after_save :parse_csv_file
|
10
|
+
|
11
|
+
# Associations
|
12
|
+
has_one_attached :csv_file
|
13
|
+
belongs_to :document, foreign_key: :friendlier_id, primary_key: :friendlier_id
|
14
|
+
has_many :document_data_dictionary_entries, -> { order(position: :asc) }, dependent: :destroy
|
15
|
+
|
16
|
+
# Validations
|
17
|
+
validates :name, presence: true
|
18
|
+
validates :csv_file, attached: true, content_type: {in: "text/csv", message: "is not a CSV file"}
|
19
|
+
|
20
|
+
validates_with DocumentDataDictionary::CsvHeaderValidator
|
21
|
+
|
22
|
+
def parse_csv_file
|
23
|
+
if csv_file.attached?
|
24
|
+
csv_data = CSV.parse(csv_file.download, headers: true)
|
25
|
+
csv_data.each do |row|
|
26
|
+
document_data_dictionary_entries.create!(row.to_h)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.sort_entries(id_array)
|
32
|
+
transaction do
|
33
|
+
logger.debug { id_array.inspect }
|
34
|
+
id_array.each_with_index do |entry_id, i|
|
35
|
+
DocumentDataDictionaryEntry.update(entry_id, position: i)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "csv"
|
4
|
+
|
5
|
+
# CSV Header Validation
|
6
|
+
class ImportDistribution
|
7
|
+
# CsvHeaderValidator
|
8
|
+
class CsvHeaderValidator < ActiveModel::Validator
|
9
|
+
def validate(record)
|
10
|
+
if record.csv_file.nil?
|
11
|
+
record.errors.add(:csv_file, "Missing a required CSV header. friendlier_id, reference_type, distribution_url, and label are required.")
|
12
|
+
return false
|
13
|
+
end
|
14
|
+
|
15
|
+
valid_csv_header = true
|
16
|
+
unless valid_csv_headers?(record&.csv_file)
|
17
|
+
valid_csv_header = false
|
18
|
+
record.errors.add(:csv_file,
|
19
|
+
"Missing a required CSV header. friendlier_id, reference_type, distribution_url, and label are required.")
|
20
|
+
end
|
21
|
+
|
22
|
+
valid_csv_header
|
23
|
+
end
|
24
|
+
|
25
|
+
def valid_csv_headers?(csv_file)
|
26
|
+
headers = CSV.parse(csv_file.download)[0]
|
27
|
+
(["friendlier_id", "reference_type", "distribution_url", "label"] - headers).empty?
|
28
|
+
rescue ArgumentError, ActiveStorage::FileNotFoundError
|
29
|
+
false
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|