diff --git a/app/controllers/admin/document_data_dictionaries_controller.rb b/app/controllers/admin/document_data_dictionaries_controller.rb
new file mode 100644
index 00000000..be176196
--- /dev/null
+++ b/app/controllers/admin/document_data_dictionaries_controller.rb
@@ -0,0 +1,147 @@
+module Admin
+ class DocumentDataDictionariesController < ApplicationController
+ before_action :set_document
+ before_action :set_document_data_dictionary, only: %i[ show edit update destroy ]
+
+ # GET /document_data_dictionaries or /document_data_dictionaries.json
+ def index
+ @document_data_dictionaries = DocumentDataDictionary.all
+ if params[:document_id]
+ @document_data_dictionaries = DocumentDataDictionary.where(friendlier_id: @document.friendlier_id).order(position: :asc)
+ else
+ @pagy, @document_data_dictionaries = pagy(DocumentDataDictionary.all.order(friendlier_id: :asc, updated_at: :desc), items: 20)
+ end
+ end
+
+ # GET /document_data_dictionaries/1 or /document_data_dictionaries/1.json
+ def show
+ end
+
+ # GET /document_data_dictionaries/new
+ def new
+ @document_data_dictionary = DocumentDataDictionary.new
+ end
+
+ # GET /document_data_dictionaries/1/edit
+ def edit
+ end
+
+ # POST /document_data_dictionaries or /document_data_dictionaries.json
+ def create
+ @document_data_dictionary = DocumentDataDictionary.new(document_data_dictionary_params)
+
+ respond_to do |format|
+ if @document_data_dictionary.save
+ format.html { redirect_to admin_document_document_data_dictionaries_path(@document), notice: "Document data dictionary was successfully created." }
+ format.json { render :show, status: :created, location: @document_data_dictionary }
+ else
+ format.html { render :new, status: :unprocessable_entity }
+ format.json { render json: @document_data_dictionary.errors, status: :unprocessable_entity }
+ end
+ end
+ end
+
+ # PATCH/PUT /document_data_dictionaries/1 or /document_data_dictionaries/1.json
+ def update
+ respond_to do |format|
+ if @document_data_dictionary.update(document_data_dictionary_params)
+ format.html { redirect_to admin_document_document_data_dictionaries_path(@document), notice: "Document data dictionary was successfully updated." }
+ format.json { render :show, status: :ok, location: @document_data_dictionary }
+ else
+ format.html { render :edit, status: :unprocessable_entity }
+ format.json { render json: @document_data_dictionary.errors, status: :unprocessable_entity }
+ end
+ end
+ end
+
+ # DELETE /document_data_dictionaries/1 or /document_data_dictionaries/1.json
+ def destroy
+ @document_data_dictionary.destroy!
+
+ respond_to do |format|
+ format.html { redirect_to admin_document_document_data_dictionaries_path(@document), status: :see_other, notice: "Document data dictionary was successfully destroyed." }
+ format.json { head :no_content }
+ end
+ end
+
+ # DELETE /admin/document_data_dictionaries/destroy_all
+ #
+ # Destroys all document data dictionaries provided in the file parameter. If successful, redirects
+ # with a success notice. Otherwise, redirects with an error notice.
+ def destroy_all
+ return if request.get?
+
+ logger.debug("Destroy Data Dictionaries")
+ unless params.dig(:document_data_dictionary, :data_dictionaries, :file)
+ raise ArgumentError, "File does not exist or is invalid."
+ end
+
+ respond_to do |format|
+ if DocumentDataDictionary.destroy_all(params.dig(:document_data_dictionary, :data_dictionaries, :file))
+ format.html { redirect_to admin_document_document_data_dictionaries_path, notice: "Data dictionaries were destroyed." }
+ else
+ format.html { redirect_to admin_document_document_data_dictionaries_path, notice: "Data dictionaries could not be destroyed." }
+ end
+ rescue => e
+ format.html { redirect_to admin_document_document_data_dictionaries_path, notice: "Data dictionaries could not be destroyed. #{e}" }
+ end
+ end
+
+ # GET/POST /documents/1/data_dictionaries/import
+ #
+ # Imports document data dictionaries from a file. If successful, redirects with a success notice.
+ # Otherwise, redirects with an error notice.
+ def import
+ return if request.get?
+
+ logger.debug("Import Data Dictionaries")
+
+ unless params.dig(:document_data_dictionary, :data_dictionaries, :file)
+ raise ArgumentError, "File does not exist or is invalid."
+ end
+
+ if DocumentDataDictionary.import(params.dig(:document_data_dictionary, :data_dictionaries, :file))
+ logger.debug("Data dictionaries were created successfully.")
+ if params[:document_id]
+ redirect_to admin_document_document_data_dictionaries_path(@document), notice: "Data dictionaries were created successfully."
+ else
+ redirect_to admin_document_document_data_dictionaries_path, notice: "Data dictionaries were created successfully."
+ end
+ else
+ logger.debug("Data dictionaries could not be created.")
+ if params[:document_id]
+ redirect_to admin_document_document_data_dictionaries_path(@document), warning: "Data dictionaries could not be created."
+ else
+ redirect_to admin_document_document_data_dictionaries_path, warning: "Data dictionaries could not be created."
+ end
+ end
+ rescue => e
+ logger.debug("Data dictionaries could not be created. #{e}")
+ if params[:document_id]
+ redirect_to admin_document_document_data_dictionaries_path(@document), notice: "Data dictionaries could not be created. #{e}"
+ else
+ redirect_to admin_document_document_data_dictionaries_path, notice: "Data dictionaries could not be created. #{e}"
+ end
+ end
+
+ private
+
+ # Sets the document based on the document_id parameter.
+ # If not nested, it does nothing.
+ def set_document
+ return unless params[:document_id] # If not nested
+
+ @document = Document.includes(:leaf_representative).find_by!(friendlier_id: params[:document_id])
+ end
+
+ # Sets the document data dictionary based on the id parameter.
+ def set_document_data_dictionary
+ @document_data_dictionary = DocumentDataDictionary.find(params[:id])
+ end
+
+ # Only allow a list of trusted parameters through.
+ def document_data_dictionary_params
+ params.require(:document_data_dictionary).permit(:friendlier_id, :label, :type, :values, :definition, :definition_source, :parent_friendlier_id, :position)
+ end
+ end
+end
\ No newline at end of file
diff --git a/app/models/document_data_dictionary.rb b/app/models/document_data_dictionary.rb
new file mode 100644
index 00000000..41a628ea
--- /dev/null
+++ b/app/models/document_data_dictionary.rb
@@ -0,0 +1,2 @@
+class DocumentDataDictionary < ApplicationRecord
+end
diff --git a/app/views/admin/document_data_dictionaries/_document_data_dictionary.html.erb b/app/views/admin/document_data_dictionaries/_document_data_dictionary.html.erb
new file mode 100644
index 00000000..3339b63b
--- /dev/null
+++ b/app/views/admin/document_data_dictionaries/_document_data_dictionary.html.erb
@@ -0,0 +1,37 @@
+
+
+ Friendlier:
+ <%= document_data_dictionary.friendlier_id %>
+
+
+
+ Label:
+ <%= document_data_dictionary.label %>
+
+
+
+ Type:
+ <%= document_data_dictionary.type %>
+
+
+
+ Values:
+ <%= document_data_dictionary.values %>
+
+
+
+ Definition:
+ <%= document_data_dictionary.definition %>
+
+
+
+ Definition source:
+ <%= document_data_dictionary.definition_source %>
+
+
+
+ Parent friendlier:
+ <%= document_data_dictionary.parent_friendlier_id %>
+
+
+
diff --git a/app/views/admin/document_data_dictionaries/_document_data_dictionary.json.jbuilder b/app/views/admin/document_data_dictionaries/_document_data_dictionary.json.jbuilder
new file mode 100644
index 00000000..34e3aedf
--- /dev/null
+++ b/app/views/admin/document_data_dictionaries/_document_data_dictionary.json.jbuilder
@@ -0,0 +1,2 @@
+json.extract! document_data_dictionary, :id, :friendlier_id, :label, :type, :values, :definition, :definition_source, :parent_friendlier_id, :created_at, :updated_at
+json.url document_data_dictionary_url(document_data_dictionary, format: :json)
diff --git a/app/views/admin/document_data_dictionaries/_form.html.erb b/app/views/admin/document_data_dictionaries/_form.html.erb
new file mode 100644
index 00000000..dc1b1b63
--- /dev/null
+++ b/app/views/admin/document_data_dictionaries/_form.html.erb
@@ -0,0 +1,19 @@
+
+<%= simple_form_for(@document_data_dictionary) do |f| %>
+ <%= f.error_notification %>
+ <%= f.error_notification message: f.object.errors[:base].to_sentence if f.object.errors[:base].present? %>
+
+
+ <%= f.input :friendlier_id %>
+ <%= f.input :label %>
+ <%= f.input :type %>
+ <%= f.input :values %>
+ <%= f.input :definition %>
+ <%= f.input :definition_source %>
+ <%= f.input :parent_friendlier_id %>
+
+
+
+ <%= f.button :submit %>
+
+<% end %>
diff --git a/app/views/admin/document_data_dictionaries/edit.html.erb b/app/views/admin/document_data_dictionaries/edit.html.erb
new file mode 100644
index 00000000..5c1c6ad8
--- /dev/null
+++ b/app/views/admin/document_data_dictionaries/edit.html.erb
@@ -0,0 +1,12 @@
+<% content_for :title, "Editing document data dictionary" %>
+
+Editing document data dictionary
+
+<%= render "form", document_data_dictionary: @document_data_dictionary %>
+
+
+
+
+ <%= link_to "Show this document data dictionary", @document_data_dictionary %> |
+ <%= link_to "Back to document data dictionaries", document_data_dictionaries_path %>
+
diff --git a/app/views/admin/document_data_dictionaries/index.html.erb b/app/views/admin/document_data_dictionaries/index.html.erb
new file mode 100644
index 00000000..25d8fe7d
--- /dev/null
+++ b/app/views/admin/document_data_dictionaries/index.html.erb
@@ -0,0 +1,16 @@
+<%= notice %>
+
+<% content_for :title, "Document data dictionaries" %>
+
+Document data dictionaries
+
+
+ <% @document_data_dictionaries.each do |document_data_dictionary| %>
+ <%= render document_data_dictionary %>
+
+ <%= link_to "Show this document data dictionary", document_data_dictionary %>
+
+ <% end %>
+
+
+<%= link_to "New document data dictionary", new_admin_document_document_data_dictionary_path(@document) %>
diff --git a/app/views/admin/document_data_dictionaries/index.json.jbuilder b/app/views/admin/document_data_dictionaries/index.json.jbuilder
new file mode 100644
index 00000000..b7a0992d
--- /dev/null
+++ b/app/views/admin/document_data_dictionaries/index.json.jbuilder
@@ -0,0 +1 @@
+json.array! @document_data_dictionaries, partial: "document_data_dictionaries/document_data_dictionary", as: :document_data_dictionary
diff --git a/app/views/admin/document_data_dictionaries/new.html.erb b/app/views/admin/document_data_dictionaries/new.html.erb
new file mode 100644
index 00000000..edcce3b8
--- /dev/null
+++ b/app/views/admin/document_data_dictionaries/new.html.erb
@@ -0,0 +1,11 @@
+<% content_for :title, "New document data dictionary" %>
+
+New document data dictionary
+
+<%= render "form", document_data_dictionary: @document_data_dictionary %>
+
+
+
+
+ <%= link_to "Back to document data dictionaries", document_data_dictionaries_path %>
+
diff --git a/app/views/admin/document_data_dictionaries/show.html.erb b/app/views/admin/document_data_dictionaries/show.html.erb
new file mode 100644
index 00000000..7ed4ba8a
--- /dev/null
+++ b/app/views/admin/document_data_dictionaries/show.html.erb
@@ -0,0 +1,10 @@
+<%= notice %>
+
+<%= render @document_data_dictionary %>
+
+
+ <%= link_to "Edit this document data dictionary", edit_document_data_dictionary_path(@document_data_dictionary) %> |
+ <%= link_to "Back to document data dictionaries", document_data_dictionaries_path %>
+
+ <%= button_to "Destroy this document data dictionary", @document_data_dictionary, method: :delete %>
+
diff --git a/app/views/admin/document_data_dictionaries/show.json.jbuilder b/app/views/admin/document_data_dictionaries/show.json.jbuilder
new file mode 100644
index 00000000..aad64a2d
--- /dev/null
+++ b/app/views/admin/document_data_dictionaries/show.json.jbuilder
@@ -0,0 +1 @@
+json.partial! "document_data_dictionaries/document_data_dictionary", document_data_dictionary: @document_data_dictionary
diff --git a/db/migrate/20241204163117_create_document_data_dictionaries.rb b/db/migrate/20241204163117_create_document_data_dictionaries.rb
new file mode 100644
index 00000000..8a959a4d
--- /dev/null
+++ b/db/migrate/20241204163117_create_document_data_dictionaries.rb
@@ -0,0 +1,16 @@
+class CreateDocumentDataDictionaries < ActiveRecord::Migration[7.2]
+ def change
+ create_table :document_data_dictionaries do |t|
+ t.string :friendlier_id
+ t.string :label
+ t.string :type
+ t.string :values
+ t.string :definition
+ t.string :definition_source
+ t.string :parent_friendlier_id
+ t.integer :position
+
+ t.timestamps
+ end
+ end
+end
diff --git a/lib/generators/geoblacklight_admin/config_generator.rb b/lib/generators/geoblacklight_admin/config_generator.rb
index 106ec528..ece79c7d 100644
--- a/lib/generators/geoblacklight_admin/config_generator.rb
+++ b/lib/generators/geoblacklight_admin/config_generator.rb
@@ -205,6 +205,17 @@ def set_routes
end
end
+ # Data Dictionaries
+ resources :document_data_dictionaries, path: "data_dictionaries" do
+ collection do
+ get "import"
+ post "import"
+
+ get "destroy_all"
+ post "destroy_all"
+ end
+ end
+
# DocumentDownloads
resources :document_downloads, path: "downloads" do
collection do
diff --git a/lib/generators/geoblacklight_admin/templates/.env.development.example b/lib/generators/geoblacklight_admin/templates/.env.development.example
index 1772f5cb..73e63f95 100644
--- a/lib/generators/geoblacklight_admin/templates/.env.development.example
+++ b/lib/generators/geoblacklight_admin/templates/.env.development.example
@@ -11,7 +11,7 @@ SOLR_URL=http://127.0.0.1:8983/solr/blacklight-core
# PostgreSQL
POSTGRES_HOST=127.0.0.1
-POSTGRES_PORT=5555
+POSTGRES_PORT=5432
POSTGRES_DB=geoblacklight_admin_development
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
diff --git a/test/controllers/document_data_dictionary_controller_test.rb b/test/controllers/document_data_dictionary_controller_test.rb
new file mode 100644
index 00000000..d01079dc
--- /dev/null
+++ b/test/controllers/document_data_dictionary_controller_test.rb
@@ -0,0 +1,79 @@
+require "test_helper"
+
+class DocumentDataDictionaryControllerTest < ActionDispatch::IntegrationTest
+ setup do
+ @document = documents(:ls)
+ @document_data_dictionary = document_data_dictionaries(:one)
+ @file = fixture_file_upload("import_data_dictionary.csv", "text/csv")
+
+ get "/users/sign_in"
+ sign_in_as users(:user_001)
+ post user_session_url
+
+ follow_redirect!
+ assert_response :success
+ end
+
+ test "should get index" do
+ get admin_document_document_data_dictionary_url(@document)
+ assert_response :success
+ end
+
+ test "should get new" do
+ get new_admin_document_document_data_dictionary_url(@document)
+ assert_response :success
+ end
+
+ test "should create document_data_dictionary" do
+ assert_difference("DocumentDataDictionary.count") do
+ post admin_document_document_data_dictionary_url(@document), params: {document_data_dictionary: {
+ friendlier_id: "35c8a641589c4e13b7aa11e37f3f00a1_0",
+ reference_type_id: ReferenceType.first.id,
+ url: "https://example.com"
+ }}
+ end
+
+ assert_redirected_to admin_document_document_distributions_url(@document)
+ end
+
+ test "should show document_data_dictionary" do
+ get admin_document_document_data_dictionary_url(@document, @document_data_dictionary)
+ assert_response :success
+ end
+
+ test "should get edit" do
+ get edit_admin_document_document_data_dictionary_url(@document, @document_data_dictionary)
+ assert_response :success
+ end
+
+ test "should update document_data_dictionary" do
+ patch admin_document_document_data_dictionary_url(@document, @document_data_dictionary), params: {
+ document_data_dictionary: {
+ friendlier_id: "35c8a641589c4e13b7aa11e37f3f00a1_0",
+ reference_type_id: ReferenceType.first.id,
+ url: "https://example2.com"
+ }
+ }
+ assert_redirected_to admin_document_document_data_dictionary_url(@document)
+ end
+
+ test "should destroy document_data_dictionary" do
+ assert_difference("DocumentDataDictionary.count", -1) do
+ delete admin_document_document_data_dictionary_url(@document, @document_data_dictionary)
+ end
+
+ assert_redirected_to admin_document_document_data_dictionary_url(@document)
+ end
+
+ test "should import data dictionary successfully" do
+ post import_admin_document_document_data_dictionary_url(@document), params: {document_id: @document.friendlier_id, document_data_dictionary: {data_dictionary: {file: @file}}}
+ assert_redirected_to admin_document_document_data_dictionary_path(@document)
+ assert_equal "Data dictionary was created successfully.", flash[:notice]
+ end
+
+ test "should not import data dictionary with invalid file" do
+ post import_admin_document_document_data_dictionary_url(@document), params: {document_id: @document.friendlier_id, document_data_dictionary: {data_dictionary: {file: nil}}}
+ assert_redirected_to admin_document_document_data_dictionary_path(@document)
+ assert_equal "Data dictionary could not be created. File does not exist or is invalid.", flash[:notice]
+ end
+end