From 7abdfca4ceac6706335be46fb088784748ee9046 Mon Sep 17 00:00:00 2001 From: Andy Kuny Date: Fri, 8 Mar 2024 12:59:06 -0500 Subject: [PATCH 01/63] Hover state of "logo" el --- nad_ch/controllers/web/sass/components/_usa-sidenav.scss | 6 ++++-- nad_ch/controllers/web/templates/_layouts/sidebar.html | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/nad_ch/controllers/web/sass/components/_usa-sidenav.scss b/nad_ch/controllers/web/sass/components/_usa-sidenav.scss index 4611cbf..a79e5ab 100644 --- a/nad_ch/controllers/web/sass/components/_usa-sidenav.scss +++ b/nad_ch/controllers/web/sass/components/_usa-sidenav.scss @@ -59,8 +59,10 @@ .usa-sidenav__logo { padding: 16px 24px 32px 0; - // Default hover state - &:hover { + a { + &:hover { + background-color: var(--Primary-primary-darker, #162e51); + } } .logo-bottom { diff --git a/nad_ch/controllers/web/templates/_layouts/sidebar.html b/nad_ch/controllers/web/templates/_layouts/sidebar.html index 2d1e528..ef541e5 100644 --- a/nad_ch/controllers/web/templates/_layouts/sidebar.html +++ b/nad_ch/controllers/web/templates/_layouts/sidebar.html @@ -4,8 +4,8 @@
{% if current_user.is_authenticated %} From 73416282a4e683b5d10d8cfa7dce90da4f53595b Mon Sep 17 00:00:00 2001 From: Andy Kuny Date: Mon, 11 Mar 2024 10:17:29 -0400 Subject: [PATCH 02/63] Add about and card component/macro --- nad_ch/controllers/web/flask.py | 4 ++ .../web/routes/data_submissions.py | 4 +- .../web/templates/_layouts/sidebar.html | 12 +++- nad_ch/controllers/web/templates/about.html | 66 +++++++++++++++++++ .../web/templates/components/card.html | 10 +++ 5 files changed, 92 insertions(+), 4 deletions(-) create mode 100644 nad_ch/controllers/web/templates/about.html create mode 100644 nad_ch/controllers/web/templates/components/card.html diff --git a/nad_ch/controllers/web/flask.py b/nad_ch/controllers/web/flask.py index 3d51204..a8a383a 100644 --- a/nad_ch/controllers/web/flask.py +++ b/nad_ch/controllers/web/flask.py @@ -22,6 +22,10 @@ def create_flask_application(ctx: ApplicationContext): def index(): return render_template("index.html") + @app.route("/about") + def about(): + return render_template("about.html") + @app.errorhandler(404) def page_not_found(e): return render_template("404.html"), 404 diff --git a/nad_ch/controllers/web/routes/data_submissions.py b/nad_ch/controllers/web/routes/data_submissions.py index e8711fa..eb70d8b 100644 --- a/nad_ch/controllers/web/routes/data_submissions.py +++ b/nad_ch/controllers/web/routes/data_submissions.py @@ -20,7 +20,7 @@ def home(): @submissions_bp.route("/reports") -@login_required +# @login_required def reports(): # For demo purposes, hard-code the producer name view_model = list_data_submissions_by_producer(g.ctx, "New Jersey") @@ -28,7 +28,7 @@ def reports(): @submissions_bp.route("/reports/") -@login_required +# @login_required def view_report(submission_id): view_model = get_data_submission(g.ctx, submission_id) return render_template("data_submissions/show.html", submission=view_model) diff --git a/nad_ch/controllers/web/templates/_layouts/sidebar.html b/nad_ch/controllers/web/templates/_layouts/sidebar.html index ef541e5..d09e1c6 100644 --- a/nad_ch/controllers/web/templates/_layouts/sidebar.html +++ b/nad_ch/controllers/web/templates/_layouts/sidebar.html @@ -8,7 +8,7 @@ Collaboration Hub
- {% if current_user.is_authenticated %} + {# {% if current_user.is_authenticated %} #}
  • {% if request.path.startswith('/reports') %} Reports {% endif %}
  • +
  • + {% if request.path.startswith('/about') %} + About + {% else %} + About + {% endif %} +
  • + {#
  • - {% endif %} + #} {# {% endif %} #} {% endblock %} diff --git a/nad_ch/controllers/web/templates/about.html b/nad_ch/controllers/web/templates/about.html new file mode 100644 index 0000000..eced795 --- /dev/null +++ b/nad_ch/controllers/web/templates/about.html @@ -0,0 +1,66 @@ +{% extends "_layouts/base.html" %} {% from "components/card.html" import card %} +{% block title %}About{% endblock %} {% block content %} + +
    +
    +
    + +
    +
    +
    +
    +

    + The 10x team is piloting the National Address Database Collaboration Hub + (NAD-CH) with the Department of Transportation (DOT) and the U.S. Virgin + Island's Geospatial Information Services Division. +

    +

    + This product will provide a platform for State, Local, Tribal, and + Territorial (SLTT) and federal data administrators to connect, collaborate, + and share data in an efficient, streamlined manner—enabling a more complete + and authoritative public domain address list to aid federal agencies with + emergency and disaster response, location data sharing, and reduction of + duplicative efforts—all while saving tax-payer dollars. +

    + + {% call card("Help Shape the NAD-CH") %} +

    + If you manage State, Local, Tribal, or Territorial address point data, sign + up to participate in research with the 10x team. We'll use your feedback to + guide the future of this platform. +

    + +

    + Your inputs will make it possible for us to continue our mission of enabling + every household within the U.S. Territories to be reached efficiently and + effectively for purposes of communication, benefits administration, and + disaster relief through address point data. +

    + +

    + Sign Up + +

    + + {% endcall %} +
    +{% endblock %} diff --git a/nad_ch/controllers/web/templates/components/card.html b/nad_ch/controllers/web/templates/components/card.html new file mode 100644 index 0000000..8a30686 --- /dev/null +++ b/nad_ch/controllers/web/templates/components/card.html @@ -0,0 +1,10 @@ +{% macro card(title) %} +
    +
    +

    {{ title }}

    +
    +
    {{ caller() }}
    +
    +{% endmacro %} From d31514b94247b412834571334f2a6482e1d75c85 Mon Sep 17 00:00:00 2001 From: Andy Kuny Date: Mon, 11 Mar 2024 10:24:10 -0400 Subject: [PATCH 03/63] Update 4xx templates --- nad_ch/controllers/web/templates/401.html | 10 ++-------- nad_ch/controllers/web/templates/404.html | 10 ++-------- 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/nad_ch/controllers/web/templates/401.html b/nad_ch/controllers/web/templates/401.html index 10ffda2..5a1c527 100644 --- a/nad_ch/controllers/web/templates/401.html +++ b/nad_ch/controllers/web/templates/401.html @@ -9,12 +9,6 @@ -
    -
    -
    -

    401 Unauthorized

    -

    Your request lacks valid authentication credentials.

    -
    -
    -
    +

    401 Unauthorized

    +

    Your request lacks valid authentication credentials.

    {% endblock %} diff --git a/nad_ch/controllers/web/templates/404.html b/nad_ch/controllers/web/templates/404.html index 2e83e49..7086063 100644 --- a/nad_ch/controllers/web/templates/404.html +++ b/nad_ch/controllers/web/templates/404.html @@ -9,12 +9,6 @@ -
    -
    -
    -

    404 Not Found

    -

    The server cannot find the requested resource.

    -
    -
    -
    +

    404 Not Found

    +

    The server cannot find the requested resource.

    {% endblock %} From 95011eeca9e449915ac37c1fdf3e7181e6bccbfd Mon Sep 17 00:00:00 2001 From: Andy Kuny Date: Mon, 11 Mar 2024 12:52:14 -0400 Subject: [PATCH 04/63] Add data checklist view --- nad_ch/controllers/web/flask.py | 12 ++ .../web/templates/_layouts/base.html | 1 + .../web/templates/_layouts/head.html | 1 - .../web/templates/_layouts/sidebar.html | 26 ++- .../web/templates/data-checklist.html | 196 ++++++++++++++++++ 5 files changed, 225 insertions(+), 11 deletions(-) create mode 100644 nad_ch/controllers/web/templates/data-checklist.html diff --git a/nad_ch/controllers/web/flask.py b/nad_ch/controllers/web/flask.py index a8a383a..f309832 100644 --- a/nad_ch/controllers/web/flask.py +++ b/nad_ch/controllers/web/flask.py @@ -26,6 +26,18 @@ def index(): def about(): return render_template("about.html") + @app.route("/data-checklist") + def data_checklist(): + data_checklist = [ + {"field_name": "AddNum_Pre", "alias": "Address number prefix", + "description": "The prefix of the address number", "type": "String", + "length": 15, "required": "*"}, + {"field_name": "Add_Number", "alias": "Address number", + "description": "The address number", "type": "Integer", + "length": "-", "required": "*"}, + ] + return render_template("data-checklist.html", data_checklist=data_checklist) + @app.errorhandler(404) def page_not_found(e): return render_template("404.html"), 404 diff --git a/nad_ch/controllers/web/templates/_layouts/base.html b/nad_ch/controllers/web/templates/_layouts/base.html index 6b262fe..3e15654 100644 --- a/nad_ch/controllers/web/templates/_layouts/base.html +++ b/nad_ch/controllers/web/templates/_layouts/base.html @@ -15,5 +15,6 @@ + diff --git a/nad_ch/controllers/web/templates/_layouts/head.html b/nad_ch/controllers/web/templates/_layouts/head.html index f9e64af..837954f 100644 --- a/nad_ch/controllers/web/templates/_layouts/head.html +++ b/nad_ch/controllers/web/templates/_layouts/head.html @@ -6,5 +6,4 @@ content="width=device-width, initial-scale=1, viewport-fit=cover" /> - {% endblock %} diff --git a/nad_ch/controllers/web/templates/_layouts/sidebar.html b/nad_ch/controllers/web/templates/_layouts/sidebar.html index d09e1c6..c439793 100644 --- a/nad_ch/controllers/web/templates/_layouts/sidebar.html +++ b/nad_ch/controllers/web/templates/_layouts/sidebar.html @@ -10,20 +10,26 @@ {# {% if current_user.is_authenticated %} #}
  • - {% if request.path.startswith('/reports') %} - Data Checklist +
  • +
  • + Reports - {% else %} - Reports - {% endif %}
  • +
  • - {% if request.path.startswith('/about') %} - About - {% else %} - About - {% endif %} + About
  • {# diff --git a/nad_ch/controllers/web/templates/data-checklist.html b/nad_ch/controllers/web/templates/data-checklist.html new file mode 100644 index 0000000..9f0bc00 --- /dev/null +++ b/nad_ch/controllers/web/templates/data-checklist.html @@ -0,0 +1,196 @@ +{% extends "_layouts/base.html" %} {% from "components/card.html" import card %} +{% block title %}Data Checklist{% endblock %} {% block content %} +
    +
    +
    +
    + + +
    +
    +
    + +
    +
    + + + + + + + + + + + + + {% for field in data_checklist %} + + + + + + + + + {% endfor %} + +
    *NAD field + Alias
    + Description +
    TypeLengthExample
    + {{ field.required }} + {{ field.field_name }} + {{ field.alias }}
    + {{ field.description }} +
    {{ field.type}}{{ field.length }}
    +
    +
    + +
    +
    +

    + +

    +
    +

    DomainName: AddressNumber

    +

    + Description: The numeric identifier of + a location along a thoroughfare or within a defined community. +

    +

    FieldType: Integer

    +

    Domain Type: Range

    + + + + + + + + + + + + + +
    Minimum ValueMaximum Value
    0999999
    +
    +

    + +

    +
    +

    DomainName: Delivery_Type

    +

    + Description: Flag to separate address + records with no subaddress information from address records that + include subaddress information. +

    +

    FieldType: String

    +

    Domain Type: CodedValue

    + + + + + + + + + + + + + + + + + + + + + +
    CodeName
    Subaddress IncludedSubaddress Included
    Subaddress ExcludedSubaddress Excluded
    UnstatedUnstated
    +
    +
    +
    +
    + + +{% endblock %} From 550af85bacd151fd51f5606a2a8fdf2635a25974 Mon Sep 17 00:00:00 2001 From: Andy Kuny Date: Tue, 12 Mar 2024 14:16:27 -0400 Subject: [PATCH 05/63] Mapping index --- nad_ch/controllers/web/flask.py | 24 +- nad_ch/controllers/web/images/mapping.png | Bin 0 -> 31674 bytes nad_ch/controllers/web/package-lock.json | 236 ++++++++++++++++++ nad_ch/controllers/web/package.json | 1 + .../web/routes/data_submissions.py | 9 +- nad_ch/controllers/web/routes/mappings.py | 27 ++ .../web/sass/components/_usa-nav.scss | 12 + nad_ch/controllers/web/src/index.ts | 5 + nad_ch/controllers/web/templates/401.html | 12 +- nad_ch/controllers/web/templates/404.html | 12 +- .../web/templates/_layouts/sidebar.html | 7 + nad_ch/controllers/web/templates/about.html | 20 +- .../web/templates/components/page-header.html | 11 + .../web/templates/data-checklist.html | 62 ++--- .../web/templates/data_submissions/index.html | 16 +- nad_ch/controllers/web/templates/index.html | 13 +- .../web/templates/mappings/create.html | 3 + .../web/templates/mappings/index.html | 135 ++++++++++ nad_ch/controllers/web/webpack.config.js | 15 +- scripts/seed.py | 2 +- 20 files changed, 500 insertions(+), 122 deletions(-) create mode 100644 nad_ch/controllers/web/images/mapping.png create mode 100644 nad_ch/controllers/web/routes/mappings.py create mode 100644 nad_ch/controllers/web/templates/components/page-header.html create mode 100644 nad_ch/controllers/web/templates/mappings/create.html create mode 100644 nad_ch/controllers/web/templates/mappings/index.html diff --git a/nad_ch/controllers/web/flask.py b/nad_ch/controllers/web/flask.py index f309832..19bf1e8 100644 --- a/nad_ch/controllers/web/flask.py +++ b/nad_ch/controllers/web/flask.py @@ -3,6 +3,7 @@ from nad_ch.application.interfaces import ApplicationContext from nad_ch.controllers.web.routes.auth import setup_auth, user_loader, auth_bp from nad_ch.controllers.web.routes.data_submissions import submissions_bp +from nad_ch.controllers.web.routes.mappings import mappings_bp def create_flask_application(ctx: ApplicationContext): @@ -29,12 +30,22 @@ def about(): @app.route("/data-checklist") def data_checklist(): data_checklist = [ - {"field_name": "AddNum_Pre", "alias": "Address number prefix", - "description": "The prefix of the address number", "type": "String", - "length": 15, "required": "*"}, - {"field_name": "Add_Number", "alias": "Address number", - "description": "The address number", "type": "Integer", - "length": "-", "required": "*"}, + { + "field_name": "AddNum_Pre", + "alias": "Address number prefix", + "description": "The prefix of the address number", + "type": "String", + "length": 15, + "required": "*", + }, + { + "field_name": "Add_Number", + "alias": "Address number", + "description": "The address number", + "type": "Integer", + "length": "-", + "required": "*", + }, ] return render_template("data-checklist.html", data_checklist=data_checklist) @@ -44,5 +55,6 @@ def page_not_found(e): app.register_blueprint(auth_bp, url_prefix="/auth") app.register_blueprint(submissions_bp) + app.register_blueprint(mappings_bp) return app diff --git a/nad_ch/controllers/web/images/mapping.png b/nad_ch/controllers/web/images/mapping.png new file mode 100644 index 0000000000000000000000000000000000000000..ade903f87e850edda3a1d4ea989200da1d36c3aa GIT binary patch literal 31674 zcmeFYcT`hb*Efm+0@73zkZwgq2#EBmsMr7%=}kaDq=aHffFuYANVkB9lvwG#wpdk*nqJO^$*4}I@sD00NM-m)>WDCSIn{_^vT ztEE)VBb+XF=ZBNk&b}vL*3jz+e_{wrV;!YD-!0G1wsO+qeD_rqwr@g5ZcbljKk9t% z-qFwAPuYa@*^BfOJLL|PJ2@TSQ_-}JUl})A;9q(7={5nyU=3W{3fn8icAf9*;?7=s zsjpmn8uMXJH;%DgISN*)y>q^$WRDlm?E}-C7t(VZ%`T)DH3rY>^7wi_w_&?;cgF8j zDqD_2sP?DlDLg9ubeRBQa>Q}AM|{s2Yiny;2eL1D@*=(-=-fzM)LVGSG*U)ff8E(7 z(Hv9z&FIv1OPTIjV}@QQ87mL6&=hSEQ>{FI!!q5oyTrTNf9=FWOa5v5yFvFaeziUj za$s$idEishd3Vc?&C)8#XJmPZw@^li@C6F_G zy;aqPN8|3z2CSmovD<&J+j1Jcf3sZU-?mxvF7IwtHy^izaSJ;t7=}za356-upVfgxA^3I7EWs;F9gd%OA4z$h}fSEy38hWUH|j}Hp5fO>c_1nPK33??(i|6U1!@L z*~Zxs%=w;s%^}LNwfEq|Z^5y@Pxv*OK3~`8_!cpAZ|h9∨qrjtk8pSLU1B!nfv{ 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 literal 0 HcmV?d00001 diff --git a/nad_ch/controllers/web/package-lock.json b/nad_ch/controllers/web/package-lock.json index d9339bd..09c51b7 100644 --- a/nad_ch/controllers/web/package-lock.json +++ b/nad_ch/controllers/web/package-lock.json @@ -14,6 +14,7 @@ }, "devDependencies": { "@types/alpinejs": "^3.13.6", + "copy-webpack-plugin": "^12.0.2", "css-loader": "^6.10.0", "mini-css-extract-plugin": "^2.8.0", "node-sass": "^9.0.0", @@ -259,6 +260,41 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/@npmcli/fs": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-2.1.2.tgz", @@ -286,6 +322,18 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/@sindresorhus/merge-streams": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", + "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@tootallnate/once": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", @@ -1142,6 +1190,42 @@ "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", "dev": true }, + "node_modules/copy-webpack-plugin": { + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-12.0.2.tgz", + "integrity": "sha512-SNwdBeHyII+rWvee/bTnAYyO8vfVdcSTud4EIb6jcZ8inLeWucJE0DnxXQBjlQ5zlteuuvooGQy3LIyGxhvlOA==", + "dev": true, + "dependencies": { + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.1", + "globby": "^14.0.0", + "normalize-path": "^3.0.0", + "schema-utils": "^4.2.0", + "serialize-javascript": "^6.0.2" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + } + }, + "node_modules/copy-webpack-plugin/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", @@ -1453,6 +1537,22 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -1468,6 +1568,15 @@ "node": ">= 4.9.1" } }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -1630,6 +1739,26 @@ "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", "dev": true }, + "node_modules/globby": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/globby/-/globby-14.0.1.tgz", + "integrity": "sha512-jOMLD2Z7MAhyG8aJpNOpmziMOP4rPLcc95oQPKXBazW82z+CEgPFBQvEpRUa1KeIMUJo4Wsm+q6uzO/Q/4BksQ==", + "dev": true, + "dependencies": { + "@sindresorhus/merge-streams": "^2.1.0", + "fast-glob": "^3.3.2", + "ignore": "^5.2.4", + "path-type": "^5.0.0", + "slash": "^5.1.0", + "unicorn-magic": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/globule": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/globule/-/globule-1.3.4.tgz", @@ -1797,6 +1926,15 @@ "postcss": "^8.1.0" } }, + "node_modules/ignore": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, "node_modules/immutable": { "version": "4.3.5", "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.5.tgz", @@ -2237,6 +2375,15 @@ "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", "dev": true }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, "node_modules/micromatch": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", @@ -2881,6 +3028,18 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, + "node_modules/path-type": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-5.0.0.tgz", + "integrity": "sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -3082,6 +3241,26 @@ "node": ">=6" } }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/quick-lru": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", @@ -3309,6 +3488,16 @@ "node": ">= 4" } }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, "node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -3324,6 +3513,29 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -3524,6 +3736,18 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, + "node_modules/slash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", + "dev": true, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/smart-buffer": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", @@ -3984,6 +4208,18 @@ "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", "dev": true }, + "node_modules/unicorn-magic": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", + "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/unique-filename": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-2.0.1.tgz", diff --git a/nad_ch/controllers/web/package.json b/nad_ch/controllers/web/package.json index 879f308..fa44a7f 100644 --- a/nad_ch/controllers/web/package.json +++ b/nad_ch/controllers/web/package.json @@ -14,6 +14,7 @@ }, "devDependencies": { "@types/alpinejs": "^3.13.6", + "copy-webpack-plugin": "^12.0.2", "css-loader": "^6.10.0", "mini-css-extract-plugin": "^2.8.0", "node-sass": "^9.0.0", diff --git a/nad_ch/controllers/web/routes/data_submissions.py b/nad_ch/controllers/web/routes/data_submissions.py index eb70d8b..5e7eb93 100644 --- a/nad_ch/controllers/web/routes/data_submissions.py +++ b/nad_ch/controllers/web/routes/data_submissions.py @@ -14,13 +14,8 @@ def before_request(): g.ctx = current_app.extensions["ctx"] -@submissions_bp.route("/") -def home(): - return render_template("index.html") - - @submissions_bp.route("/reports") -# @login_required +@login_required def reports(): # For demo purposes, hard-code the producer name view_model = list_data_submissions_by_producer(g.ctx, "New Jersey") @@ -28,7 +23,7 @@ def reports(): @submissions_bp.route("/reports/") -# @login_required +@login_required def view_report(submission_id): view_model = get_data_submission(g.ctx, submission_id) return render_template("data_submissions/show.html", submission=view_model) diff --git a/nad_ch/controllers/web/routes/mappings.py b/nad_ch/controllers/web/routes/mappings.py new file mode 100644 index 0000000..2dc7ec8 --- /dev/null +++ b/nad_ch/controllers/web/routes/mappings.py @@ -0,0 +1,27 @@ +from flask import Blueprint, current_app, render_template, g, request, abort + +# from flask_login import login_required + + +mappings_bp = Blueprint("mappings", __name__) + + +@mappings_bp.before_request +def before_request(): + g.ctx = current_app.extensions["ctx"] + + +@mappings_bp.route("/mappings") +# @login_required +def index(): + return render_template("mappings/index.html") + + +@mappings_bp.route("/mappings/create") +# @login_required +def create(): + if "title" not in request.args: + abort(404) + + title = request.args.get("title") + return render_template("mappings/create.html", title=title) diff --git a/nad_ch/controllers/web/sass/components/_usa-nav.scss b/nad_ch/controllers/web/sass/components/_usa-nav.scss index aec8394..8743b5e 100644 --- a/nad_ch/controllers/web/sass/components/_usa-nav.scss +++ b/nad_ch/controllers/web/sass/components/_usa-nav.scss @@ -34,3 +34,15 @@ .usa-accordion__button.usa-nav__link { text-align: center; } + +.usa-nav__primary { + padding: 0; +} + +.usa-nav__primary-item { + list-style-type: none; +} + +.usa-nav__link { + cursor: pointer; +} diff --git a/nad_ch/controllers/web/src/index.ts b/nad_ch/controllers/web/src/index.ts index 0f470d9..2cc03f6 100644 --- a/nad_ch/controllers/web/src/index.ts +++ b/nad_ch/controllers/web/src/index.ts @@ -1,4 +1,5 @@ import "@uswds/uswds/css/uswds.css"; +import "@uswds/uswds"; import Alpine from "alpinejs"; declare global { @@ -9,4 +10,8 @@ declare global { window.Alpine = Alpine; +Alpine.store("config", { + BASE_URL: "http://localhost:8080", +}); + Alpine.start(); diff --git a/nad_ch/controllers/web/templates/401.html b/nad_ch/controllers/web/templates/401.html index 5a1c527..e6b19d6 100644 --- a/nad_ch/controllers/web/templates/401.html +++ b/nad_ch/controllers/web/templates/401.html @@ -1,14 +1,6 @@ {% extends "_layouts/base.html" %} {% block title %}Home Page{% endblock %} {% -block content %} -

    -
    -
    - -
    -
    -
    +block content %} {% from "components/page-header.html" import page_header %} {{ +page_header("NAD Collaboration Hub") }}

    401 Unauthorized

    Your request lacks valid authentication credentials.

    {% endblock %} diff --git a/nad_ch/controllers/web/templates/404.html b/nad_ch/controllers/web/templates/404.html index 7086063..4a0595b 100644 --- a/nad_ch/controllers/web/templates/404.html +++ b/nad_ch/controllers/web/templates/404.html @@ -1,14 +1,6 @@ {% extends "_layouts/base.html" %} {% block title %}Home Page{% endblock %} {% -block content %} -
    -
    -
    - -
    -
    -
    +block content %} {% from "components/page-header.html" import page_header %} {{ +page_header("NAD Collaboration Hub") }}

    404 Not Found

    The server cannot find the requested resource.

    {% endblock %} diff --git a/nad_ch/controllers/web/templates/_layouts/sidebar.html b/nad_ch/controllers/web/templates/_layouts/sidebar.html index c439793..edd7bb9 100644 --- a/nad_ch/controllers/web/templates/_layouts/sidebar.html +++ b/nad_ch/controllers/web/templates/_layouts/sidebar.html @@ -16,6 +16,13 @@ >Data Checklist
    +
  • + Mappings +
  • .about-lead { color: var(--Base-ink, #1b1b1b); @@ -15,15 +15,10 @@ line-height: 150%; } -
    -
    -
    - -
    -
    -
    + +{% from "components/page-header.html" import page_header %} {{ +page_header("About") }} +

    The 10x team is piloting the National Address Database Collaboration Hub @@ -39,7 +34,8 @@ duplicative efforts—all while saving tax-payer dollars.

    - {% call card("Help Shape the NAD-CH") %} + {% from "components/card.html" import card %} {% call card("Help Shape the + NAD-CH") %}

    If you manage State, Local, Tribal, or Territorial address point data, sign up to participate in research with the 10x team. We'll use your feedback to diff --git a/nad_ch/controllers/web/templates/components/page-header.html b/nad_ch/controllers/web/templates/components/page-header.html new file mode 100644 index 0000000..88f922c --- /dev/null +++ b/nad_ch/controllers/web/templates/components/page-header.html @@ -0,0 +1,11 @@ +{% macro page_header(title) %} +

    +
    +
    + +
    +
    +
    +{% endmacro %} diff --git a/nad_ch/controllers/web/templates/data-checklist.html b/nad_ch/controllers/web/templates/data-checklist.html index 9f0bc00..03bf0eb 100644 --- a/nad_ch/controllers/web/templates/data-checklist.html +++ b/nad_ch/controllers/web/templates/data-checklist.html @@ -9,14 +9,12 @@
  • -
  • diff --git a/nad_ch/controllers/web/templates/column_maps/index.html b/nad_ch/controllers/web/templates/column_maps/index.html index 6082be1..a1f5b97 100644 --- a/nad_ch/controllers/web/templates/column_maps/index.html +++ b/nad_ch/controllers/web/templates/column_maps/index.html @@ -41,11 +41,7 @@

    Create Your First Mapping

    New mapping

    - +

    diff --git a/nad_ch/controllers/web/templates/column_maps/show.html b/nad_ch/controllers/web/templates/column_maps/show.html index 849d563..5011ed5 100644 --- a/nad_ch/controllers/web/templates/column_maps/show.html +++ b/nad_ch/controllers/web/templates/column_maps/show.html @@ -8,9 +8,9 @@ Edit
    -
    Your Field
    +
    Your Field
    -
    NAD Field
    +
    NAD Field
    {% for nad_field, mapped_field in column_map.mapping.items() %} {% if mapped_field %} diff --git a/nad_ch/domain/repositories.py b/nad_ch/domain/repositories.py index 9e86fbe..608f74b 100644 --- a/nad_ch/domain/repositories.py +++ b/nad_ch/domain/repositories.py @@ -65,3 +65,6 @@ def get_by_name_and_version(self, name: str, version: int) -> Optional[ColumnMap def get_by_producer(self, producer: DataProducer) -> Iterable[ColumnMap]: ... + + def update_mapping(self, column_map: ColumnMap) -> ColumnMap: + ... diff --git a/nad_ch/infrastructure/database.py b/nad_ch/infrastructure/database.py index f7af28a..d271700 100644 --- a/nad_ch/infrastructure/database.py +++ b/nad_ch/infrastructure/database.py @@ -433,4 +433,19 @@ def get_by_producer(self, producer: DataProducer) -> List[ColumnMap]: .all() ) column_map_entities = [column_map.to_entity() for column_map in column_map_models] - return column_map_entities \ No newline at end of file + return column_map_entities + + def update(self, column_map: ColumnMap) -> ColumnMap: + with session_scope(self.session_factory) as session: + existing_column_map = ( + session.query(ColumnMapModel) + .filter(ColumnMapModel.id == column_map.id) + .first() + ) + + existing_column_map.name = column_map.name + existing_column_map.mapping = column_map.mapping + existing_column_map.version_id += 1 + session.commit() + session.refresh(existing_column_map) + return existing_column_map.to_entity() From 4191e9accc90843689557dcbfb70fef0d16d276c Mon Sep 17 00:00:00 2001 From: Andy Kuny Date: Mon, 18 Mar 2024 17:42:45 -0400 Subject: [PATCH 29/63] Successfully use form --- nad_ch/controllers/web/routes/column_maps.py | 7 ++- .../web/templates/column_maps/edit.html | 49 ++++++------------- 2 files changed, 17 insertions(+), 39 deletions(-) diff --git a/nad_ch/controllers/web/routes/column_maps.py b/nad_ch/controllers/web/routes/column_maps.py index cf85e1d..5353246 100644 --- a/nad_ch/controllers/web/routes/column_maps.py +++ b/nad_ch/controllers/web/routes/column_maps.py @@ -96,14 +96,13 @@ def update(request): if request.form.get('_formType') == 'required_field': user_field = request.form.get('mappedRequiredField') nad_field = request.form.get('_nadField') - elif request.form.get('_formType') == 'optional_field': - user_field = request.form.get('mappedOptionalField') + elif request.form.get('_formType') == 'delete_field': + user_field = request.form.get('_nullField') nad_field = request.form.get('_nadField') elif request.form.get('_formType') == 'new_field': user_field = request.form.get('newField') nad_field = request.form.get('newNadField') - - if not id or not user_field or not nad_field: + else: abort(404) print(f'user_field: {user_field}, nad_field: {nad_field}') diff --git a/nad_ch/controllers/web/templates/column_maps/edit.html b/nad_ch/controllers/web/templates/column_maps/edit.html index 19b55bf..a24e7f1 100644 --- a/nad_ch/controllers/web/templates/column_maps/edit.html +++ b/nad_ch/controllers/web/templates/column_maps/edit.html @@ -32,14 +32,25 @@ Edit {% if nad_field not in column_map.required_nad_fields %} - + + + + + + + + + {% endif %}
    - {% if nad_field in column_map.required_nad_fields %} {# Form for editing a - required mapping #}
    @@ -69,38 +80,6 @@
    - - {% else %} {# Form for editing an optional mapping #} -
    - - - - - -
    -
    - -
    -
    {{ arrow_right() }}
    -
    {{ nad_field }}
    -
    - - -
    -
    -
    - {% endif %} {% endif %} {% endfor %} From cafc16aa130ae724cc73145af6f042b5561f1f19 Mon Sep 17 00:00:00 2001 From: Andy Kuny Date: Mon, 18 Mar 2024 18:31:20 -0400 Subject: [PATCH 30/63] Testing and tweaking formatting --- .../web/templates/column_maps/edit.html | 35 ++++++++++++------- .../web/templates/column_maps/show.html | 12 ++++--- .../web/templates/components/icons/edit.html | 12 +++++++ 3 files changed, 42 insertions(+), 17 deletions(-) create mode 100644 nad_ch/controllers/web/templates/components/icons/edit.html diff --git a/nad_ch/controllers/web/templates/column_maps/edit.html b/nad_ch/controllers/web/templates/column_maps/edit.html index a24e7f1..4cd957b 100644 --- a/nad_ch/controllers/web/templates/column_maps/edit.html +++ b/nad_ch/controllers/web/templates/column_maps/edit.html @@ -1,20 +1,26 @@ {% extends "_layouts/base.html" %} {% block title %}Mapping {{ column_map.name}}{% endblock %} {% from "components/page-header.html" import page_header %} {% from "components/icons/arrow_right.html" import arrow_right %} -{% block content %} {{ page_header("Mapping: " ~ column_map.name ) }} +{% from "components/icons/edit.html" import edit %} {% block content %} {{ +page_header("Mapping: " ~ column_map.name ~ edit()) }}
    -
    -

    Version number: {{ column_map.version }}

    - Cancel +
    +
    + Version number: + {{ column_map.version }} +
    +
    + Cancel +
    -
    Your Field
    -
    {{ arrow_right() }}
    -
    NAD Field
    +
    Your Field
    +
    +
    NAD Field
    {% for nad_field, mapped_field in column_map.mapping.items() %} {% if mapped_field %} @@ -104,7 +110,7 @@
    -
    +
    +
    {% endblock %} diff --git a/nad_ch/controllers/web/templates/column_maps/show.html b/nad_ch/controllers/web/templates/column_maps/show.html index 5011ed5..565a1b8 100644 --- a/nad_ch/controllers/web/templates/column_maps/show.html +++ b/nad_ch/controllers/web/templates/column_maps/show.html @@ -3,14 +3,16 @@ page_header %} {% block content %} {{ page_header("Mapping: " ~ column_map.name ) }}
    -
    -

    Version number: {{ column_map.version }}

    - Edit +
    +
    Version number: {{ column_map.version }}
    +
    + Edit +
    -
    Your Field
    +
    Your Field
    -
    NAD Field
    +
    NAD Field
    {% for nad_field, mapped_field in column_map.mapping.items() %} {% if mapped_field %} diff --git a/nad_ch/controllers/web/templates/components/icons/edit.html b/nad_ch/controllers/web/templates/components/icons/edit.html new file mode 100644 index 0000000..14bed65 --- /dev/null +++ b/nad_ch/controllers/web/templates/components/icons/edit.html @@ -0,0 +1,12 @@ +{% macro edit() %} + + + +{% endmacro %} From cbca71e27a2e6383d88f5b024f42e35fd92136aa Mon Sep 17 00:00:00 2001 From: Andy Kuny Date: Tue, 19 Mar 2024 07:14:26 -0400 Subject: [PATCH 31/63] Adjusting formatting --- nad_ch/controllers/web/sass/base/_base.scss | 20 ++++ .../web/sass/components/_usa-button.scss | 7 ++ .../web/templates/column_maps/edit.html | 101 ++++++++++-------- .../web/templates/column_maps/show.html | 17 +-- 4 files changed, 88 insertions(+), 57 deletions(-) diff --git a/nad_ch/controllers/web/sass/base/_base.scss b/nad_ch/controllers/web/sass/base/_base.scss index ad77f09..652511e 100644 --- a/nad_ch/controllers/web/sass/base/_base.scss +++ b/nad_ch/controllers/web/sass/base/_base.scss @@ -1,3 +1,23 @@ html { overflow-y: scroll; } + +.mapping-row { + padding-bottom: 0.5rem; + padding-top: 0.5rem; + height: 50px; + display: flex; + align-items: center; +} + +.usa-input--small { + width: 200px; + height: 30px; +} + +.usa-select--small { + width: 200px; + height: 40px; + font-size: 0.875rem; + padding-right: 0px; +} diff --git a/nad_ch/controllers/web/sass/components/_usa-button.scss b/nad_ch/controllers/web/sass/components/_usa-button.scss index c1d9e10..e4c047f 100644 --- a/nad_ch/controllers/web/sass/components/_usa-button.scss +++ b/nad_ch/controllers/web/sass/components/_usa-button.scss @@ -1,3 +1,10 @@ .usa-button-group li { list-style: none; } + +.usa-button--small { + padding: 0.5rem 1rem; + font-size: 0.875rem; + width: auto; + height: 30px; +} diff --git a/nad_ch/controllers/web/templates/column_maps/edit.html b/nad_ch/controllers/web/templates/column_maps/edit.html index 4cd957b..7cb14fa 100644 --- a/nad_ch/controllers/web/templates/column_maps/edit.html +++ b/nad_ch/controllers/web/templates/column_maps/edit.html @@ -17,7 +17,7 @@ >
    -
    +
    Your Field
    NAD Field
    @@ -26,13 +26,13 @@ mapped_field %}
    -
    +
    {{ mapped_field }}
    {{ arrow_right() }}
    {{ nad_field }}
    + {% endif %}
    @@ -63,12 +68,12 @@ -
    -
    +
    +
    @@ -77,12 +82,14 @@
    {{ nad_field }}
    - +
    @@ -99,23 +106,27 @@ -
    - +
    +
    +
    +
    +
    + +
    -
    -
    +
    +
    @@ -123,39 +134,39 @@
    {{ arrow_right() }}
    -
    - {% for field in column_map.available_nad_fields %} {% endfor %}
    +
    +
    + +
    +
    + +
    +
    -
    -
      -
    • - -
    • -
    • - -
    • -
    -
    -
    {% endblock %} diff --git a/nad_ch/controllers/web/templates/column_maps/show.html b/nad_ch/controllers/web/templates/column_maps/show.html index 565a1b8..c7aa3ba 100644 --- a/nad_ch/controllers/web/templates/column_maps/show.html +++ b/nad_ch/controllers/web/templates/column_maps/show.html @@ -1,6 +1,7 @@ {% extends "_layouts/base.html" %} {% block title %}Mapping {{ column_map.name}}{% endblock %} {% from "components/page-header.html" import -page_header %} {% block content %} {{ page_header("Mapping: " ~ column_map.name +page_header %} {% from "components/icons/arrow_right.html" import arrow_right %} +{% block content %} {{ page_header("Mapping: " ~ column_map.name ) }}
    @@ -9,25 +10,17 @@ Edit
    -
    +
    Your Field
    NAD Field
    {% for nad_field, mapped_field in column_map.mapping.items() %} {% if mapped_field %} -
    +
    {{ mapped_field }}
    - - - - + {{ arrow_right() }}
    {{ nad_field }}
    From 82706db5fea9e72469039883cdac7608e45913fb Mon Sep 17 00:00:00 2001 From: Andy Kuny Date: Tue, 19 Mar 2024 07:31:43 -0400 Subject: [PATCH 32/63] Adjust button container --- .../web/templates/column_maps/edit.html | 35 +++++++++---------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/nad_ch/controllers/web/templates/column_maps/edit.html b/nad_ch/controllers/web/templates/column_maps/edit.html index 7cb14fa..1fb82fd 100644 --- a/nad_ch/controllers/web/templates/column_maps/edit.html +++ b/nad_ch/controllers/web/templates/column_maps/edit.html @@ -136,8 +136,9 @@
    -
    - -
    -
    - -
    + +
    From be9d81615cb60d70a550e327c6e43f3d2bd5fcf9 Mon Sep 17 00:00:00 2001 From: Andy Kuny Date: Tue, 19 Mar 2024 08:16:52 -0400 Subject: [PATCH 33/63] Update font of data type in checklist\ --- nad_ch/controllers/web/templates/data-checklist.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nad_ch/controllers/web/templates/data-checklist.html b/nad_ch/controllers/web/templates/data-checklist.html index 03bf0eb..3134e91 100644 --- a/nad_ch/controllers/web/templates/data-checklist.html +++ b/nad_ch/controllers/web/templates/data-checklist.html @@ -66,7 +66,7 @@ {{ field.alias }}
    {{ field.description }} - {{ field.type}} + {{ field.type}} {{ field.length }} From 4e9d14f54e86a6a2f63a74e2fb3c81b09c9907a0 Mon Sep 17 00:00:00 2001 From: Andy Kuny Date: Tue, 19 Mar 2024 08:23:30 -0400 Subject: [PATCH 34/63] Hack to allow for space at bottom of mapping form --- nad_ch/controllers/web/templates/column_maps/edit.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nad_ch/controllers/web/templates/column_maps/edit.html b/nad_ch/controllers/web/templates/column_maps/edit.html index 1fb82fd..7a9f6f4 100644 --- a/nad_ch/controllers/web/templates/column_maps/edit.html +++ b/nad_ch/controllers/web/templates/column_maps/edit.html @@ -106,7 +106,7 @@ -
    +
    @@ -121,7 +121,7 @@
    -
    +
    Date: Tue, 19 Mar 2024 08:36:42 -0400 Subject: [PATCH 35/63] Use card component in data submission show template --- nad_ch/controllers/web/templates/about.html | 2 +- .../web/templates/data_submissions/show.html | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/nad_ch/controllers/web/templates/about.html b/nad_ch/controllers/web/templates/about.html index d70de1e..83ca99e 100644 --- a/nad_ch/controllers/web/templates/about.html +++ b/nad_ch/controllers/web/templates/about.html @@ -25,7 +25,7 @@ (NAD-CH) with the Department of Transportation (DOT) and the U.S. Virgin Island's Geospatial Information Services Division.

    -

    +

    This product will provide a platform for State, Local, Tribal, and Territorial (SLTT) and federal data administrators to connect, collaborate, and share data in an efficient, streamlined manner—enabling a more complete diff --git a/nad_ch/controllers/web/templates/data_submissions/show.html b/nad_ch/controllers/web/templates/data_submissions/show.html index d1faade..a0d073b 100644 --- a/nad_ch/controllers/web/templates/data_submissions/show.html +++ b/nad_ch/controllers/web/templates/data_submissions/show.html @@ -79,11 +79,11 @@

    Yes

    -
    -
    -

    Need support? Contact NAD

    -

    nad@dot.gov

    -
    +
    + {% from "components/card.html" import card %} {% call card("Need + support? Contact NAD") %} +

    nad@dot.gov

    + {% endcall %}
    From 9eae1ed5bb187cbe63b04d0e4acc7c4ff6bad522 Mon Sep 17 00:00:00 2001 From: Andy Kuny Date: Tue, 19 Mar 2024 08:38:16 -0400 Subject: [PATCH 36/63] Pull tweaks into component files --- nad_ch/controllers/web/sass/base/_base.scss | 12 ------------ .../controllers/web/sass/components/_usa-input.scss | 6 ++++++ .../controllers/web/sass/components/_usa-select.scss | 4 ++++ nad_ch/controllers/web/sass/index.scss | 2 ++ 4 files changed, 12 insertions(+), 12 deletions(-) create mode 100644 nad_ch/controllers/web/sass/components/_usa-input.scss create mode 100644 nad_ch/controllers/web/sass/components/_usa-select.scss diff --git a/nad_ch/controllers/web/sass/base/_base.scss b/nad_ch/controllers/web/sass/base/_base.scss index 652511e..2047e6e 100644 --- a/nad_ch/controllers/web/sass/base/_base.scss +++ b/nad_ch/controllers/web/sass/base/_base.scss @@ -9,15 +9,3 @@ html { display: flex; align-items: center; } - -.usa-input--small { - width: 200px; - height: 30px; -} - -.usa-select--small { - width: 200px; - height: 40px; - font-size: 0.875rem; - padding-right: 0px; -} diff --git a/nad_ch/controllers/web/sass/components/_usa-input.scss b/nad_ch/controllers/web/sass/components/_usa-input.scss new file mode 100644 index 0000000..1db6a17 --- /dev/null +++ b/nad_ch/controllers/web/sass/components/_usa-input.scss @@ -0,0 +1,6 @@ +.usa-select--small { + width: 200px; + height: 40px; + font-size: 0.875rem; + padding-right: 0px; +} diff --git a/nad_ch/controllers/web/sass/components/_usa-select.scss b/nad_ch/controllers/web/sass/components/_usa-select.scss new file mode 100644 index 0000000..7a6bd79 --- /dev/null +++ b/nad_ch/controllers/web/sass/components/_usa-select.scss @@ -0,0 +1,4 @@ +.usa-input--small { + width: 200px; + height: 30px; +} diff --git a/nad_ch/controllers/web/sass/index.scss b/nad_ch/controllers/web/sass/index.scss index 39244ca..93525e7 100644 --- a/nad_ch/controllers/web/sass/index.scss +++ b/nad_ch/controllers/web/sass/index.scss @@ -4,6 +4,8 @@ @import 'base/variables'; @import 'components/usa-button'; @import 'components/usa-header'; +@import 'components/usa-input'; @import 'components/usa-nav'; +@import 'components/usa-select'; @import 'components/usa-sidenav'; @import 'components/usa-table'; From 6637a4da22b907796c4062bd7525c0dfd276804f Mon Sep 17 00:00:00 2001 From: Andy Kuny Date: Tue, 19 Mar 2024 09:27:47 -0400 Subject: [PATCH 37/63] Update seed script, fix storage download_temp method --- nad_ch/controllers/cli.py | 5 ++- nad_ch/infrastructure/storage.py | 8 +++- nad_ch/main.py | 13 ++++-- scripts/seed.py | 70 +++++++++++++++++++++++++++++--- 4 files changed, 83 insertions(+), 13 deletions(-) diff --git a/nad_ch/controllers/cli.py b/nad_ch/controllers/cli.py index d8708e1..ad6b293 100644 --- a/nad_ch/controllers/cli.py +++ b/nad_ch/controllers/cli.py @@ -51,6 +51,7 @@ def list_submissions_by_producer(ctx, producer): @cli.command() @click.pass_context @click.argument("filename") -def validate_submission(ctx, filename): +@click.argument("mapping_name") +def validate_submission(ctx, filename, mapping_name): context = ctx.obj - validate_data_submission(context, filename) + validate_data_submission(context, filename, mapping_name) diff --git a/nad_ch/infrastructure/storage.py b/nad_ch/infrastructure/storage.py index df3604c..98ba2a8 100644 --- a/nad_ch/infrastructure/storage.py +++ b/nad_ch/infrastructure/storage.py @@ -1,4 +1,5 @@ import os +import glob import shutil import tempfile from typing import Optional @@ -42,12 +43,15 @@ def download_temp(self, key: str) -> Optional[DownloadResult]: zip_file_path = os.path.join(temp_dir, key) self.client.download_file(self.bucket_name, key, zip_file_path) - extracted_dir = f"{temp_dir}.gdb" + extracted_dir = f"{temp_dir}_extraced" with ZipFile(zip_file_path, "r") as zip_ref: zip_ref.extractall(extracted_dir) - return DownloadResult(temp_dir=temp_dir, extracted_dir=extracted_dir) + gdb_dirs = [d for d in glob.glob(os.path.join(extracted_dir, '*')) if os.path.isdir(d) and d.endswith('.gdb')] + gdb_dir = gdb_dirs[0] if gdb_dirs else None + + return DownloadResult(temp_dir=temp_dir, extracted_dir=gdb_dir) except Exception: return None diff --git a/nad_ch/main.py b/nad_ch/main.py index cb951cc..0dee3b6 100644 --- a/nad_ch/main.py +++ b/nad_ch/main.py @@ -10,8 +10,8 @@ ctx = create_app_context() -def run_cli(): - cli(obj=ctx) +def run_cli(args): + cli.main(args=args, obj=ctx) def serve_flask_app(): @@ -21,11 +21,16 @@ def serve_flask_app(): if __name__ == "__main__": parser = argparse.ArgumentParser(description="Call a specific function.") - parser.add_argument("function", choices=["run_cli", "serve_flask_app"]) + subparsers = parser.add_subparsers(dest='function', required=True) + + parser_run_cli = subparsers.add_parser('run_cli', help='Run the CLI application') + parser_run_cli.add_argument('cli_args', nargs=argparse.REMAINDER, help='Arguments for the CLI application') + + parser_serve_flask_app = subparsers.add_parser('serve_flask_app', help='Serve the Flask application') args = parser.parse_args() if args.function == "run_cli": - run_cli() + run_cli(args.cli_args) elif args.function == "serve_flask_app": serve_flask_app() diff --git a/scripts/seed.py b/scripts/seed.py index ce88032..0eaa00c 100644 --- a/scripts/seed.py +++ b/scripts/seed.py @@ -32,9 +32,70 @@ def main(): ) ctx.users.add(new_user) - # new_column_map = ColumnMap(name="New Jersey Mapping v1", producer=saved_producer) + new_column_map = ColumnMap(name="NewJerseyMapping", producer=saved_producer, version_id=1) # TODO save column map once ApplicationContext can provide a repository - # saved_column_map = ctx.column_maps.add(new_column_map) + new_column_map.mapping = { + "AddNum_Pre": "", + "Add_Number": "address_number", + "AddNum_Suf": "", + "AddNo_Full": "address_number_full", + "St_PreMod": "", + "St_PreDir": "", + "St_PreTyp": "", + "St_PreSep": "", + "St_Name": "street_name", + "St_PosTyp": "", + "St_PosDir": "", + "St_PosMod": "", + "StNam_Full": "street_name_full", + "Building": "", + "Floor": "", + "Unit": "unit", + "Room": "room", + "Seat": "", + "Addtl_Loc": "", + "SubAddress": "", + "LandmkName": "", + "County": "county", + "Inc_Muni": "city", + "Post_City": "", + "Census_Plc": "", + "Uninc_Comm": "", + "Nbrhd_Comm": "", + "NatAmArea": "", + "NatAmSub": "", + "Urbnztn_PR": "", + "PlaceOther": "", + "PlaceNmTyp": "", + "State": "state", + "Zip_Code": "", + "Plus_4": "", + "UUID":"guid", + "AddAuth": "", + "AddrRefSys": "", + "Longitude": "long", + "Latitude": "lat", + "NatGrid": "nat_grid", + 'Elevation': "", + 'Placement': "", + "AddrPoint": "address_point", + 'Related_ID': "", + 'RelateType': "", + "ParcelSrc": "", + "Parcel_ID": "", + "AddrClass": "", + "Lifecycle": "", + "Effective": "", + "Expire": "", + "DateUpdate": "updated", + "AnomStatus": "", + "LocatnDesc": "", + "Addr_Type": "", + "DeliverTyp": "", + "NAD_Source": "source", + "DataSet_ID": "123456", + } + saved_column_map = ctx.column_maps.add(new_column_map) current_script_path = os.path.abspath(__file__) project_root = os.path.dirname(os.path.dirname(current_script_path)) @@ -48,9 +109,8 @@ def main(): filename = DataSubmission.generate_filename(zipped_gdb_path, saved_producer) ctx.storage.upload(zipped_gdb_path, filename) - # TODO save submission once column map has been saved to disk - # new_submission = DataSubmission(filename, saved_producer, saved_column_map) - # ctx.submissions.add(new_submission) + new_submission = DataSubmission(filename, saved_producer, saved_column_map) + ctx.submissions.add(new_submission) os.remove(zipped_gdb_path) From a83ea323254f5ced29a6b55f9f824d02569aac27 Mon Sep 17 00:00:00 2001 From: Andy Kuny Date: Tue, 19 Mar 2024 09:42:17 -0400 Subject: [PATCH 38/63] Format login page --- nad_ch/controllers/web/templates/index.html | 27 ++++++--------------- 1 file changed, 8 insertions(+), 19 deletions(-) diff --git a/nad_ch/controllers/web/templates/index.html b/nad_ch/controllers/web/templates/index.html index 50f4576..c86ba7b 100644 --- a/nad_ch/controllers/web/templates/index.html +++ b/nad_ch/controllers/web/templates/index.html @@ -4,23 +4,12 @@

    Hi, {{ current_user.email }}!

    Thanks for logging in with {{ current_user.login_provider }}.

    {% else %} - +
    +

    Log in to begin.

    + Login with cloud.gov +
    {% endif %} {% endblock %} From 7da96494ae1a28fd7844c1d9122b1cab240f7907 Mon Sep 17 00:00:00 2001 From: Andy Kuny Date: Tue, 19 Mar 2024 09:52:32 -0400 Subject: [PATCH 39/63] Test, lint and format --- nad_ch/application/view_models.py | 8 ++++-- nad_ch/controllers/web/routes/column_maps.py | 30 ++++++++++---------- nad_ch/domain/entities.py | 6 ++-- nad_ch/infrastructure/database.py | 4 ++- nad_ch/infrastructure/storage.py | 6 +++- nad_ch/main.py | 12 +++++--- scripts/seed.py | 14 +++++---- tests/domain/test_entities.py | 2 +- 8 files changed, 49 insertions(+), 33 deletions(-) diff --git a/nad_ch/application/view_models.py b/nad_ch/application/view_models.py index 6cb0df2..6933fff 100644 --- a/nad_ch/application/view_models.py +++ b/nad_ch/application/view_models.py @@ -57,8 +57,10 @@ def create_column_map_view_model(column_map: ColumnMap) -> ColumnMapViewModel: mapping=column_map.mapping, version=column_map.version_id, producer_name=column_map.producer.name, - available_nad_fields = [key for key, value in column_map.mapping.items() if value == ''], - required_nad_fields = [ + available_nad_fields=[ + key for key, value in column_map.mapping.items() if value == "" + ], + required_nad_fields=[ "Add_Number", "AddNo_Full", "St_Name", @@ -74,7 +76,7 @@ def create_column_map_view_model(column_map: ColumnMap) -> ColumnMapViewModel: "DateUpdate", "NAD_Source", "DataSet_ID", - ] + ], ) diff --git a/nad_ch/controllers/web/routes/column_maps.py b/nad_ch/controllers/web/routes/column_maps.py index 5353246..af7a18e 100644 --- a/nad_ch/controllers/web/routes/column_maps.py +++ b/nad_ch/controllers/web/routes/column_maps.py @@ -50,7 +50,7 @@ def create(): @column_maps_bp.route("/column-maps", methods=["POST"]) @login_required def store(): - if request.form.get('_method') == 'PUT': + if request.form.get("_method") == "PUT": return update(request) if "mapping-csv-input" not in request.files: @@ -62,7 +62,7 @@ def store(): flash("No selected file") return redirect(url_for("column_maps.create")) - if not file.filename.endswith('.csv'): + if not file.filename.endswith(".csv"): flash("File is not a CSV") return redirect(url_for("column_maps.create")) @@ -91,21 +91,21 @@ def store(): def update(request): - id = request.form.get('_id') - - if request.form.get('_formType') == 'required_field': - user_field = request.form.get('mappedRequiredField') - nad_field = request.form.get('_nadField') - elif request.form.get('_formType') == 'delete_field': - user_field = request.form.get('_nullField') - nad_field = request.form.get('_nadField') - elif request.form.get('_formType') == 'new_field': - user_field = request.form.get('newField') - nad_field = request.form.get('newNadField') + id = request.form.get("_id") + + if request.form.get("_formType") == "required_field": + user_field = request.form.get("mappedRequiredField") + nad_field = request.form.get("_nadField") + elif request.form.get("_formType") == "delete_field": + user_field = request.form.get("_nullField") + nad_field = request.form.get("_nadField") + elif request.form.get("_formType") == "new_field": + user_field = request.form.get("newField") + nad_field = request.form.get("newNadField") else: abort(404) - print(f'user_field: {user_field}, nad_field: {nad_field}') + print(f"user_field: {user_field}, nad_field: {nad_field}") try: view_model = update_column_mapping(g.ctx, id, user_field, nad_field) @@ -131,5 +131,5 @@ def edit(id): try: view_model = get_column_map(g.ctx, id) return render_template("column_maps/edit.html", column_map=view_model) - except: + except Exception: abort(404) diff --git a/nad_ch/domain/entities.py b/nad_ch/domain/entities.py index 0ea4a43..0844fd3 100644 --- a/nad_ch/domain/entities.py +++ b/nad_ch/domain/entities.py @@ -68,8 +68,10 @@ def is_valid(self) -> bool: if not len(self.mapping): return False - # The mapping must contain all required fields - if not all(field in self.mapping for field in required_fields): + # The mapping must contain all required fields and they must not be empty + if not all( + field in self.mapping and self.mapping[field] for field in required_fields + ): return False return True diff --git a/nad_ch/infrastructure/database.py b/nad_ch/infrastructure/database.py index d271700..6afc785 100644 --- a/nad_ch/infrastructure/database.py +++ b/nad_ch/infrastructure/database.py @@ -432,7 +432,9 @@ def get_by_producer(self, producer: DataProducer) -> List[ColumnMap]: .filter(ColumnMapModel.data_producer_id == producer.id) .all() ) - column_map_entities = [column_map.to_entity() for column_map in column_map_models] + column_map_entities = [ + column_map.to_entity() for column_map in column_map_models + ] return column_map_entities def update(self, column_map: ColumnMap) -> ColumnMap: diff --git a/nad_ch/infrastructure/storage.py b/nad_ch/infrastructure/storage.py index 98ba2a8..384b3e3 100644 --- a/nad_ch/infrastructure/storage.py +++ b/nad_ch/infrastructure/storage.py @@ -48,7 +48,11 @@ def download_temp(self, key: str) -> Optional[DownloadResult]: with ZipFile(zip_file_path, "r") as zip_ref: zip_ref.extractall(extracted_dir) - gdb_dirs = [d for d in glob.glob(os.path.join(extracted_dir, '*')) if os.path.isdir(d) and d.endswith('.gdb')] + gdb_dirs = [ + d + for d in glob.glob(os.path.join(extracted_dir, "*")) + if os.path.isdir(d) and d.endswith(".gdb") + ] gdb_dir = gdb_dirs[0] if gdb_dirs else None return DownloadResult(temp_dir=temp_dir, extracted_dir=gdb_dir) diff --git a/nad_ch/main.py b/nad_ch/main.py index 0dee3b6..0f49e62 100644 --- a/nad_ch/main.py +++ b/nad_ch/main.py @@ -21,12 +21,16 @@ def serve_flask_app(): if __name__ == "__main__": parser = argparse.ArgumentParser(description="Call a specific function.") - subparsers = parser.add_subparsers(dest='function', required=True) + subparsers = parser.add_subparsers(dest="function", required=True) - parser_run_cli = subparsers.add_parser('run_cli', help='Run the CLI application') - parser_run_cli.add_argument('cli_args', nargs=argparse.REMAINDER, help='Arguments for the CLI application') + parser_run_cli = subparsers.add_parser("run_cli", help="Run the CLI application") + parser_run_cli.add_argument( + "cli_args", nargs=argparse.REMAINDER, help="Arguments for the CLI application" + ) - parser_serve_flask_app = subparsers.add_parser('serve_flask_app', help='Serve the Flask application') + parser_serve_flask_app = subparsers.add_parser( + "serve_flask_app", help="Serve the Flask application" + ) args = parser.parse_args() diff --git a/scripts/seed.py b/scripts/seed.py index 0eaa00c..f8dabcd 100644 --- a/scripts/seed.py +++ b/scripts/seed.py @@ -32,7 +32,9 @@ def main(): ) ctx.users.add(new_user) - new_column_map = ColumnMap(name="NewJerseyMapping", producer=saved_producer, version_id=1) + new_column_map = ColumnMap( + name="NewJerseyMapping", producer=saved_producer, version_id=1 + ) # TODO save column map once ApplicationContext can provide a repository new_column_map.mapping = { "AddNum_Pre": "", @@ -70,17 +72,17 @@ def main(): "State": "state", "Zip_Code": "", "Plus_4": "", - "UUID":"guid", + "UUID": "guid", "AddAuth": "", "AddrRefSys": "", "Longitude": "long", "Latitude": "lat", "NatGrid": "nat_grid", - 'Elevation': "", - 'Placement': "", + "Elevation": "", + "Placement": "", "AddrPoint": "address_point", - 'Related_ID': "", - 'RelateType': "", + "Related_ID": "", + "RelateType": "", "ParcelSrc": "", "Parcel_ID": "", "AddrClass": "", diff --git a/tests/domain/test_entities.py b/tests/domain/test_entities.py index 1504885..7eba783 100644 --- a/tests/domain/test_entities.py +++ b/tests/domain/test_entities.py @@ -79,7 +79,7 @@ def test_column_map_is_invalid_if_empty(): assert not column_map.is_valid() -def test_column_map_is_invalid_if_empty_values(): +def test_column_map_is_invalid_if_empty_values_for_required_field(): mapping = { "Add_Number": "address_number", "AddNo_Full": "address_number_full", From b3503360f26be8699a3d89d4f0d42cc014de0d33 Mon Sep 17 00:00:00 2001 From: Andy Kuny Date: Tue, 19 Mar 2024 10:20:45 -0400 Subject: [PATCH 40/63] Show created date in mapping index --- nad_ch/controllers/web/templates/column_maps/index.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nad_ch/controllers/web/templates/column_maps/index.html b/nad_ch/controllers/web/templates/column_maps/index.html index a1f5b97..fc7dc9e 100644 --- a/nad_ch/controllers/web/templates/column_maps/index.html +++ b/nad_ch/controllers/web/templates/column_maps/index.html @@ -89,6 +89,7 @@

    New mapping

    Name + Created Version @@ -99,6 +100,7 @@

    New mapping

    {{ cm.name }} + {{ cm.date_created }} {{ cm.version }} Date: Thu, 21 Mar 2024 10:24:52 -0400 Subject: [PATCH 41/63] Move required fields to static prop of ColumnMap entity --- nad_ch/domain/entities.py | 41 +++++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/nad_ch/domain/entities.py b/nad_ch/domain/entities.py index 0844fd3..d1c76b8 100644 --- a/nad_ch/domain/entities.py +++ b/nad_ch/domain/entities.py @@ -28,6 +28,27 @@ def __repr__(self): class ColumnMap(Entity): + required_fields = [ + "Add_Number", + "AddNo_Full", + "St_Name", + "StNam_Full", + "County", + "Inc_Muni", + "Post_City", + "State", + "UUID", + "AddAuth", + "Longitude", + "Latitude", + "NatGrid", + "Placement", + "AddrPoint", + "DateUpdate", + "NAD_Source", + "DataSet_ID", + ] + def __init__( self, name: str, @@ -46,31 +67,13 @@ def __repr__(self): return f"ColumnMap {self.id}, {self.name})" def is_valid(self) -> bool: - required_fields = [ - "Add_Number", - "AddNo_Full", - "St_Name", - "StNam_Full", - "County", - "Inc_Muni", - "State", - "UUID", - "Longitude", - "Latitude", - "NatGrid", - "AddrPoint", - "DateUpdate", - "NAD_Source", - "DataSet_ID", - ] - # The mapping must not be empty if not len(self.mapping): return False # The mapping must contain all required fields and they must not be empty if not all( - field in self.mapping and self.mapping[field] for field in required_fields + field in self.mapping and self.mapping[field] for field in ColumnMap.required_fields ): return False From 0b9e7ad3addae23b56da24ab06f0e6d50c13c018 Mon Sep 17 00:00:00 2001 From: Andy Kuny Date: Fri, 22 Mar 2024 08:43:34 -0400 Subject: [PATCH 42/63] Shift add form to show.html --- nad_ch/application/use_cases/column_maps.py | 2 +- .../web/templates/column_maps/edit.html | 69 ------------------ .../web/templates/column_maps/show.html | 71 +++++++++++++++++++ nad_ch/domain/entities.py | 15 ++-- tests/domain/test_entities.py | 10 +++ 5 files changed, 86 insertions(+), 81 deletions(-) diff --git a/nad_ch/application/use_cases/column_maps.py b/nad_ch/application/use_cases/column_maps.py index 3b093cd..e7a23eb 100644 --- a/nad_ch/application/use_cases/column_maps.py +++ b/nad_ch/application/use_cases/column_maps.py @@ -59,7 +59,7 @@ def update_column_mapping( raise ValueError("Column map not found") column_map.mapping[nad_field] = user_field - + print(column_map.mapping) if not column_map.is_valid(): raise ValueError("Invalid mapping") diff --git a/nad_ch/controllers/web/templates/column_maps/edit.html b/nad_ch/controllers/web/templates/column_maps/edit.html index 7a9f6f4..6cf83fe 100644 --- a/nad_ch/controllers/web/templates/column_maps/edit.html +++ b/nad_ch/controllers/web/templates/column_maps/edit.html @@ -96,74 +96,5 @@
    {% endif %} {% endfor %} -
    - - - - -
    -
    -
    -
    -
    - -
    -
    - -
    -
    -
    - -
    -
    - {{ arrow_right() }} -
    -
    - -
    -
    - - -
    -
    -
    -
    {% endblock %} diff --git a/nad_ch/controllers/web/templates/column_maps/show.html b/nad_ch/controllers/web/templates/column_maps/show.html index c7aa3ba..f32eafc 100644 --- a/nad_ch/controllers/web/templates/column_maps/show.html +++ b/nad_ch/controllers/web/templates/column_maps/show.html @@ -26,5 +26,76 @@
    {% endif %} {% endfor %}
    + +
    + + + + +
    +
    + +
    +
    + +
    +
    +
    + +
    +
    + {{ arrow_right() }} +
    +
    + +
    +
    +
    +
    + + +
    + +
    +
    +
    +
    {% endblock %} diff --git a/nad_ch/domain/entities.py b/nad_ch/domain/entities.py index d1c76b8..532edfd 100644 --- a/nad_ch/domain/entities.py +++ b/nad_ch/domain/entities.py @@ -67,17 +67,10 @@ def __repr__(self): return f"ColumnMap {self.id}, {self.name})" def is_valid(self) -> bool: - # The mapping must not be empty - if not len(self.mapping): - return False - - # The mapping must contain all required fields and they must not be empty - if not all( - field in self.mapping and self.mapping[field] for field in ColumnMap.required_fields - ): - return False - - return True + return all( + field in self.mapping and self.mapping[field] not in (None, '') + for field in ColumnMap.required_fields + ) class DataSubmission(Entity): diff --git a/tests/domain/test_entities.py b/tests/domain/test_entities.py index 7eba783..4eaaff6 100644 --- a/tests/domain/test_entities.py +++ b/tests/domain/test_entities.py @@ -34,6 +34,7 @@ def test_column_map_is_valid(): "StNam_Full": "street_name_full", "County": "county", "Inc_Muni": "city", + "Post_City": "post_city", "State": "state", "UUID": "guid", "Longitude": "long", @@ -43,6 +44,8 @@ def test_column_map_is_valid(): "DateUpdate": "updated", "NAD_Source": "source", "DataSet_ID": "123456", + "Placement": "placement", + "AddAuth": "auth", } producer = DataProducer("Some producer") @@ -58,6 +61,7 @@ def test_column_map_is_invalid_if_missing_a_required_field(): "StNam_Full": "street_name_full", "County": "county", "Inc_Muni": "city", + "Post_City": "post_city", "State": "state", "UUID": "guid", "Longitude": "long", @@ -66,6 +70,9 @@ def test_column_map_is_invalid_if_missing_a_required_field(): "AddrPoint": "address_point", "DateUpdate": "updated", "NAD_Source": "source", + "DataSet_ID": "123456", + "Placement": "placement", + # "AddAuth": "auth", } producer = DataProducer("Some producer") @@ -87,6 +94,7 @@ def test_column_map_is_invalid_if_empty_values_for_required_field(): "StNam_Full": "street_name_full", "County": "county", "Inc_Muni": "city", + "Post_City": "post_city", "State": "state", "UUID": "guid", "Longitude": "long", @@ -96,6 +104,8 @@ def test_column_map_is_invalid_if_empty_values_for_required_field(): "DateUpdate": "updated", "NAD_Source": "source", "DataSet_ID": "", + "Placement": "placement", + "AddAuth": "", } producer = DataProducer("Some producer") From 4cb870c5b07101e39abf53f3c2ad40d5bc4e7f2c Mon Sep 17 00:00:00 2001 From: Andy Kuny Date: Fri, 22 Mar 2024 09:39:38 -0400 Subject: [PATCH 43/63] Rework edit form --- nad_ch/application/use_cases/column_maps.py | 22 ++- nad_ch/application/view_models.py | 25 +-- nad_ch/controllers/web/routes/column_maps.py | 37 ++-- .../web/templates/column_maps/edit.html | 160 ++++++++---------- .../web/templates/column_maps/show.html | 20 ++- nad_ch/domain/entities.py | 64 ++++++- 6 files changed, 194 insertions(+), 134 deletions(-) diff --git a/nad_ch/application/use_cases/column_maps.py b/nad_ch/application/use_cases/column_maps.py index e7a23eb..2912e1e 100644 --- a/nad_ch/application/use_cases/column_maps.py +++ b/nad_ch/application/use_cases/column_maps.py @@ -44,13 +44,31 @@ def get_column_maps_by_producer( producer = ctx.producers.get_by_name(producer_name) if not producer: raise ValueError("Producer not found") - column_maps = ctx.column_maps.get_by_producer(producer) - + print(get_view_model(column_maps[0])) + print([get_view_model(column_map) for column_map in column_maps]) return [get_view_model(column_map) for column_map in column_maps] def update_column_mapping( + ctx: ApplicationContext, id: int, new_mapping: Dict[str, str] +): + column_map = ctx.column_maps.get_by_id(id) + + if column_map is None: + raise ValueError("Column map not found") + + column_map.mapping = new_mapping + + if not column_map.is_valid(): + raise ValueError("Invalid mapping") + + ctx.column_maps.update(column_map) + + return get_view_model(column_map) + + +def update_column_mapping_field( ctx: ApplicationContext, id: int, user_field: str, nad_field: str ): column_map = ctx.column_maps.get_by_id(id) diff --git a/nad_ch/application/view_models.py b/nad_ch/application/view_models.py index 6933fff..001c8e2 100644 --- a/nad_ch/application/view_models.py +++ b/nad_ch/application/view_models.py @@ -32,6 +32,7 @@ def get_view_model( entity_type = type(entity) if entity_type in entity_to_vm_function_map: mapping_function = entity_to_vm_function_map[entity_type] + print(mapping_function(entity)) return mapping_function(entity) # Call the mapping function for the entity else: raise ValueError(f"No mapping function defined for entity type: {entity_type}") @@ -50,6 +51,8 @@ class ColumnMapViewModel(ViewModel): def create_column_map_view_model(column_map: ColumnMap) -> ColumnMapViewModel: + available_nad_fields = [key for key in ColumnMap.all_fields if key not in column_map.mapping] + return ColumnMapViewModel( id=column_map.id, date_created=present_date(column_map.created_at), @@ -57,26 +60,8 @@ def create_column_map_view_model(column_map: ColumnMap) -> ColumnMapViewModel: mapping=column_map.mapping, version=column_map.version_id, producer_name=column_map.producer.name, - available_nad_fields=[ - key for key, value in column_map.mapping.items() if value == "" - ], - required_nad_fields=[ - "Add_Number", - "AddNo_Full", - "St_Name", - "StNam_Full", - "County", - "Inc_Muni", - "State", - "UUID", - "Longitude", - "Latitude", - "NatGrid", - "AddrPoint", - "DateUpdate", - "NAD_Source", - "DataSet_ID", - ], + available_nad_fields=available_nad_fields, + required_nad_fields=ColumnMap.required_fields, ) diff --git a/nad_ch/controllers/web/routes/column_maps.py b/nad_ch/controllers/web/routes/column_maps.py index af7a18e..a4d6fb4 100644 --- a/nad_ch/controllers/web/routes/column_maps.py +++ b/nad_ch/controllers/web/routes/column_maps.py @@ -16,6 +16,7 @@ get_column_map, get_column_maps_by_producer, update_column_mapping, + update_column_mapping_field, ) @@ -93,27 +94,33 @@ def store(): def update(request): id = request.form.get("_id") - if request.form.get("_formType") == "required_field": - user_field = request.form.get("mappedRequiredField") - nad_field = request.form.get("_nadField") - elif request.form.get("_formType") == "delete_field": - user_field = request.form.get("_nullField") - nad_field = request.form.get("_nadField") + if request.form.get("_formType") == "existing_fields": + excluded_form_keys = ("_method", "_formType", "_id") + + mapping = { + key: value + for key, value in request.form.items() + if key not in excluded_form_keys + } + + try: + view_model = update_column_mapping(g.ctx, id, mapping) + return redirect(url_for("column_maps.show", id=view_model.id)) + except ValueError: + flash("Error: ", str(ValueError)) + return redirect(url_for("column_maps.edit", id=id)) elif request.form.get("_formType") == "new_field": user_field = request.form.get("newField") nad_field = request.form.get("newNadField") + try: + view_model = update_column_mapping_field(g.ctx, id, user_field, nad_field) + return redirect(url_for("column_maps.show", id=view_model.id)) + except ValueError: + flash("Error: ", str(ValueError)) + return redirect(url_for("column_maps.edit", id=id)) else: abort(404) - print(f"user_field: {user_field}, nad_field: {nad_field}") - - try: - view_model = update_column_mapping(g.ctx, id, user_field, nad_field) - return redirect(url_for("column_maps.show", id=view_model.id)) - except ValueError: - flash("Error: ", str(ValueError)) - return redirect(url_for("column_maps.edit", id=id)) - @column_maps_bp.route("/column-maps/") @login_required diff --git a/nad_ch/controllers/web/templates/column_maps/edit.html b/nad_ch/controllers/web/templates/column_maps/edit.html index 6cf83fe..9f17064 100644 --- a/nad_ch/controllers/web/templates/column_maps/edit.html +++ b/nad_ch/controllers/web/templates/column_maps/edit.html @@ -1,100 +1,78 @@ {% extends "_layouts/base.html" %} {% block title %}Mapping {{ -column_map.name}}{% endblock %} {% from "components/page-header.html" import -page_header %} {% from "components/icons/arrow_right.html" import arrow_right %} -{% from "components/icons/edit.html" import edit %} {% block content %} {{ -page_header("Mapping: " ~ column_map.name ~ edit()) }} -
    - -
    -
    Your Field
    -
    -
    NAD Field
    -
    - {% for nad_field, mapped_field in column_map.mapping.items() %} {% if - mapped_field %} -
    -
    -
    -
    {{ mapped_field }}
    -
    {{ arrow_right() }}
    -
    {{ nad_field }}
    -
    - - {% if nad_field not in column_map.required_nad_fields %} -
    - - - - - +column_map.name}}{% endblock %} {% from "components/icons/arrow_right.html" +import arrow_right %} {% from "components/icons/edit.html" import edit %} {% +block content %} - -
    - {% endif %} +
    + + + + +
    +
    +
    + + Cancel +
    -
    - - - - - +
    -
    -
    - -
    -
    {{ arrow_right() }}
    -
    {{ nad_field }}
    -
    - - -
    -
    -
    +
    +
    +
    Your Field
    +
    +
    NAD Field
    +
    + {% for nad_field, mapped_field in column_map.mapping.items() %} {% if + mapped_field %} +
    +
    + +
    +
    {{ arrow_right() }}
    +
    {{ nad_field }}
    +
    + {% if nad_field not in column_map.required_nad_fields %} + + {% endif %} +
    + {% endif %} {% endfor %}
    - {% endif %} {% endfor %} -
    + {% endblock %} diff --git a/nad_ch/controllers/web/templates/column_maps/show.html b/nad_ch/controllers/web/templates/column_maps/show.html index f32eafc..8d783e4 100644 --- a/nad_ch/controllers/web/templates/column_maps/show.html +++ b/nad_ch/controllers/web/templates/column_maps/show.html @@ -1,13 +1,23 @@ {% extends "_layouts/base.html" %} {% block title %}Mapping {{ -column_map.name}}{% endblock %} {% from "components/page-header.html" import -page_header %} {% from "components/icons/arrow_right.html" import arrow_right %} -{% block content %} {{ page_header("Mapping: " ~ column_map.name -) }} +column_map.name}} {% endblock %} {% from "components/icons/arrow_right.html" import arrow_right %} +{% block content %} +
    +
    +
    + + Edit +
    +
    +
    +
    Version number: {{ column_map.version }}
    - Edit
    diff --git a/nad_ch/domain/entities.py b/nad_ch/domain/entities.py index 532edfd..f23b661 100644 --- a/nad_ch/domain/entities.py +++ b/nad_ch/domain/entities.py @@ -28,6 +28,68 @@ def __repr__(self): class ColumnMap(Entity): + all_fields = [ + "AddNum_Pre", + "Add_Number", + "AddNum_Suf", + "AddNo_Full", + "St_PreMod", + "St_PreDir", + "St_PreTyp", + "St_PreSep", + "St_Name", + "St_PosTyp", + "St_PosDir", + "St_PosMod", + "StNam_Full", + "Building", + "Floor", + "Unit", + "Room", + "Seat", + "Addtl_Loc", + "SubAddress", + "LandmkName", + "County", + "Inc_Muni", + "Post_City", + "Census_Plc", + "Uninc_Comm", + "Nbrhd_Comm", + "NatAmArea", + "NatAmSub", + "Urbnztn_PR", + "PlaceOther", + "PlaceNmTyp", + "State", + "Zip_Code", + "Plus_4", + "UUID", + "AddAuth", + "AddrRefSys", + "Longitude", + "Latitude", + "NatGrid", + "Elevation", + "Placement", + "AddrPoint", + "Related_ID", + "RelateType", + "ParcelSrc", + "Parcel_ID", + "AddrClass", + "Lifecycle", + "Effective", + "Expire", + "DateUpdate", + "AnomStatus", + "LocatnDesc", + "Addr_Type", + "DeliverTyp", + "NAD_Source", + "DataSet_ID", + ] + required_fields = [ "Add_Number", "AddNo_Full", @@ -68,7 +130,7 @@ def __repr__(self): def is_valid(self) -> bool: return all( - field in self.mapping and self.mapping[field] not in (None, '') + field in self.mapping and self.mapping[field] not in (None, "") for field in ColumnMap.required_fields ) From 933c366117f9c4ae89ba5a93e51c19f11b614468 Mon Sep 17 00:00:00 2001 From: Andy Kuny Date: Fri, 22 Mar 2024 09:49:10 -0400 Subject: [PATCH 44/63] Testing workflow --- nad_ch/application/use_cases/column_maps.py | 9 +++++---- nad_ch/application/view_models.py | 6 ++++-- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/nad_ch/application/use_cases/column_maps.py b/nad_ch/application/use_cases/column_maps.py index 2912e1e..9ddc900 100644 --- a/nad_ch/application/use_cases/column_maps.py +++ b/nad_ch/application/use_cases/column_maps.py @@ -45,8 +45,7 @@ def get_column_maps_by_producer( if not producer: raise ValueError("Producer not found") column_maps = ctx.column_maps.get_by_producer(producer) - print(get_view_model(column_maps[0])) - print([get_view_model(column_map) for column_map in column_maps]) + return [get_view_model(column_map) for column_map in column_maps] @@ -58,7 +57,7 @@ def update_column_mapping( if column_map is None: raise ValueError("Column map not found") - column_map.mapping = new_mapping + column_map.mapping = {key: new_mapping[key] for key in ColumnMap.all_fields if key in new_mapping} if not column_map.is_valid(): raise ValueError("Invalid mapping") @@ -77,7 +76,9 @@ def update_column_mapping_field( raise ValueError("Column map not found") column_map.mapping[nad_field] = user_field - print(column_map.mapping) + + column_map.mapping = {key: column_map.mapping[key] for key in ColumnMap.all_fields if key in column_map.mapping} + if not column_map.is_valid(): raise ValueError("Invalid mapping") diff --git a/nad_ch/application/view_models.py b/nad_ch/application/view_models.py index 001c8e2..5318098 100644 --- a/nad_ch/application/view_models.py +++ b/nad_ch/application/view_models.py @@ -32,7 +32,6 @@ def get_view_model( entity_type = type(entity) if entity_type in entity_to_vm_function_map: mapping_function = entity_to_vm_function_map[entity_type] - print(mapping_function(entity)) return mapping_function(entity) # Call the mapping function for the entity else: raise ValueError(f"No mapping function defined for entity type: {entity_type}") @@ -51,7 +50,10 @@ class ColumnMapViewModel(ViewModel): def create_column_map_view_model(column_map: ColumnMap) -> ColumnMapViewModel: - available_nad_fields = [key for key in ColumnMap.all_fields if key not in column_map.mapping] + available_nad_fields = [ + key for key in ColumnMap.all_fields + if key not in column_map.mapping or column_map.mapping.get(key) in ["", None] + ] return ColumnMapViewModel( id=column_map.id, From ac04b33db7e0b90dfe6359dd53614ff80c385c80 Mon Sep 17 00:00:00 2001 From: Andy Kuny Date: Fri, 22 Mar 2024 10:04:39 -0400 Subject: [PATCH 45/63] Repositioning buttons --- .../web/templates/column_maps/edit.html | 24 +++--- .../web/templates/column_maps/show.html | 75 +++++++++---------- 2 files changed, 48 insertions(+), 51 deletions(-) diff --git a/nad_ch/controllers/web/templates/column_maps/edit.html b/nad_ch/controllers/web/templates/column_maps/edit.html index 9f17064..813e36a 100644 --- a/nad_ch/controllers/web/templates/column_maps/edit.html +++ b/nad_ch/controllers/web/templates/column_maps/edit.html @@ -11,17 +11,21 @@
    -
    diff --git a/nad_ch/controllers/web/templates/column_maps/show.html b/nad_ch/controllers/web/templates/column_maps/show.html index 8d783e4..38ac512 100644 --- a/nad_ch/controllers/web/templates/column_maps/show.html +++ b/nad_ch/controllers/web/templates/column_maps/show.html @@ -4,41 +4,37 @@
    -
    -
    -
    -
    Version number: {{ column_map.version }}
    -
    -
    -
    +
    Your Field
    -
    NAD Field
    -
    - {% for nad_field, mapped_field in column_map.mapping.items() %} {% if - mapped_field %} -
    -
    {{ mapped_field }}
    -
    - {{ arrow_right() }} +
    NAD Field
    +
    +
    -
    {{ nad_field }}
    - {% endif %} {% endfor %} -
    -
    -
    -
    - -
    -
    -
    @@ -84,9 +69,7 @@ {% endfor %}
    -
    -
    -
    +
    -
    +
    - + {% for nad_field, mapped_field in column_map.mapping.items() %} {% if + mapped_field %} +
    +
    {{ mapped_field }}
    +
    + {{ arrow_right() }} +
    +
    {{ nad_field }}
    +
    + {% endif %} {% endfor %} +
    {% endblock %} From 03d63c25e19aee29e497bbaea1b0d2950c05c408 Mon Sep 17 00:00:00 2001 From: Andy Kuny Date: Fri, 22 Mar 2024 10:05:54 -0400 Subject: [PATCH 46/63] Update type of component --- nad_ch/controllers/web/src/components/MappingForm.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nad_ch/controllers/web/src/components/MappingForm.ts b/nad_ch/controllers/web/src/components/MappingForm.ts index 057ad99..ffa2c2b 100644 --- a/nad_ch/controllers/web/src/components/MappingForm.ts +++ b/nad_ch/controllers/web/src/components/MappingForm.ts @@ -1,3 +1,4 @@ +import { AlpineComponent } from 'alpinejs'; import { BASE_URL } from '../config'; import { getMappingNameValidationError } from '../formValidation'; import { navigateTo } from '../utilities'; @@ -10,7 +11,7 @@ interface MappingFormComponent { closeModal: () => void; } -export default function MappingForm(): MappingFormComponent { +export default function MappingForm(): AlpineComponent { return { hasError: false, errorMessage: '', From 83ec6d6849bbc4646404881018112380bea3b005 Mon Sep 17 00:00:00 2001 From: Andy Kuny Date: Fri, 22 Mar 2024 10:06:54 -0400 Subject: [PATCH 47/63] Lint and format --- nad_ch/application/use_cases/column_maps.py | 10 ++++++++-- nad_ch/application/view_models.py | 3 ++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/nad_ch/application/use_cases/column_maps.py b/nad_ch/application/use_cases/column_maps.py index 9ddc900..3a098da 100644 --- a/nad_ch/application/use_cases/column_maps.py +++ b/nad_ch/application/use_cases/column_maps.py @@ -57,7 +57,9 @@ def update_column_mapping( if column_map is None: raise ValueError("Column map not found") - column_map.mapping = {key: new_mapping[key] for key in ColumnMap.all_fields if key in new_mapping} + column_map.mapping = { + key: new_mapping[key] for key in ColumnMap.all_fields if key in new_mapping + } if not column_map.is_valid(): raise ValueError("Invalid mapping") @@ -77,7 +79,11 @@ def update_column_mapping_field( column_map.mapping[nad_field] = user_field - column_map.mapping = {key: column_map.mapping[key] for key in ColumnMap.all_fields if key in column_map.mapping} + column_map.mapping = { + key: column_map.mapping[key] + for key in ColumnMap.all_fields + if key in column_map.mapping + } if not column_map.is_valid(): raise ValueError("Invalid mapping") diff --git a/nad_ch/application/view_models.py b/nad_ch/application/view_models.py index 5318098..dba7a4a 100644 --- a/nad_ch/application/view_models.py +++ b/nad_ch/application/view_models.py @@ -51,7 +51,8 @@ class ColumnMapViewModel(ViewModel): def create_column_map_view_model(column_map: ColumnMap) -> ColumnMapViewModel: available_nad_fields = [ - key for key in ColumnMap.all_fields + key + for key in ColumnMap.all_fields if key not in column_map.mapping or column_map.mapping.get(key) in ["", None] ] From 30db8186d3377191a6677e553b1e8520f01082b7 Mon Sep 17 00:00:00 2001 From: Andy Kuny Date: Fri, 22 Mar 2024 10:38:29 -0400 Subject: [PATCH 48/63] Test column map use cases --- nad_ch/application/use_cases/column_maps.py | 1 + .../application/use_cases/test_column_maps.py | 206 ++++++++++++++++++ tests/fakes_and_mocks.py | 19 ++ 3 files changed, 226 insertions(+) create mode 100644 tests/application/use_cases/test_column_maps.py diff --git a/nad_ch/application/use_cases/column_maps.py b/nad_ch/application/use_cases/column_maps.py index 3a098da..32f7030 100644 --- a/nad_ch/application/use_cases/column_maps.py +++ b/nad_ch/application/use_cases/column_maps.py @@ -14,6 +14,7 @@ def add_column_map( if user is None: raise ValueError("User not found") + # TODO get the producer name from the user's producer property producer = ctx.producers.get_by_name("New Jersey") if producer is None: raise ValueError("Producer not found") diff --git a/tests/application/use_cases/test_column_maps.py b/tests/application/use_cases/test_column_maps.py new file mode 100644 index 0000000..938e138 --- /dev/null +++ b/tests/application/use_cases/test_column_maps.py @@ -0,0 +1,206 @@ +import pytest +from nad_ch.application.use_cases.column_maps import ( + add_column_map, + get_column_map, + get_column_maps_by_producer, + update_column_mapping, + update_column_mapping_field, +) +from nad_ch.application.view_models import ColumnMapViewModel +from nad_ch.domain.entities import ColumnMap, DataProducer, User +from nad_ch.config import create_app_context + + +@pytest.fixture(scope="function") +def app_context(): + context = create_app_context() + yield context + + +def test_add_column_map_is_valid(app_context): + app_context.producers.add(DataProducer("New Jersey")) + user = app_context.users.add(User("test@test.org", "foo", "bar")) + + mapping = { + "Add_Number": "address_number", + "AddNo_Full": "address_number_full", + "St_Name": "street_name", + "StNam_Full": "street_name_full", + "County": "county", + "Inc_Muni": "city", + "Post_City": "post_city", + "State": "state", + "UUID": "guid", + "AddAuth": "address_authority", + "Longitude": "long", + "Latitude": "lat", + "NatGrid": "nat_grid", + "Placement": "placement", + "AddrPoint": "address_point", + "DateUpdate": "date_updated", + "NAD_Source": "source", + "DataSet_ID": "id", + } + + result = add_column_map(app_context, user.id, "Test", mapping) + + assert isinstance(result, ColumnMapViewModel) + assert result.name == "Test" + + +def test_add_column_map_is_invalid(app_context): + mapping = {"a": "b", "c": "d"} + with pytest.raises(ValueError): + add_column_map(app_context, 1, "Test", mapping) + + +def test_get_column_map(app_context): + app_context.producers.add(DataProducer("New Jersey")) + user = app_context.users.add(User("test@test.org", "foo", "bar")) + + mapping = { + "Add_Number": "address_number", + "AddNo_Full": "address_number_full", + "St_Name": "street_name", + "StNam_Full": "street_name_full", + "County": "county", + "Inc_Muni": "city", + "Post_City": "post_city", + "State": "state", + "UUID": "guid", + "AddAuth": "address_authority", + "Longitude": "long", + "Latitude": "lat", + "NatGrid": "nat_grid", + "Placement": "placement", + "AddrPoint": "address_point", + "DateUpdate": "date_updated", + "NAD_Source": "source", + "DataSet_ID": "id", + } + + saved_column_map = add_column_map(app_context, user.id, "Test", mapping) + result = get_column_map(app_context, saved_column_map.id) + + assert isinstance(result, ColumnMapViewModel) + assert result.name == "Test" + + +def test_get_column_maps_by_producer(app_context): + app_context.producers.add(DataProducer("New Jersey")) + user = app_context.users.add(User("test@test.org", "foo", "bar")) + + mapping = { + "Add_Number": "address_number", + "AddNo_Full": "address_number_full", + "St_Name": "street_name", + "StNam_Full": "street_name_full", + "County": "county", + "Inc_Muni": "city", + "Post_City": "post_city", + "State": "state", + "UUID": "guid", + "AddAuth": "address_authority", + "Longitude": "long", + "Latitude": "lat", + "NatGrid": "nat_grid", + "Placement": "placement", + "AddrPoint": "address_point", + "DateUpdate": "date_updated", + "NAD_Source": "source", + "DataSet_ID": "id", + } + + add_column_map(app_context, user.id, "Test", mapping) + result = get_column_maps_by_producer(app_context, "New Jersey") + + assert isinstance(result, list) + assert len(result) == 1 + assert isinstance(result[0], ColumnMapViewModel) + assert result[0].name == "Test" + + +def test_update_column_mapping(app_context): + app_context.producers.add(DataProducer("New Jersey")) + user = app_context.users.add(User("test@test.org", "foo", "bar")) + + mapping = { + "Add_Number": "address_number", + "AddNo_Full": "address_number_full", + "St_Name": "street_name", + "StNam_Full": "street_name_full", + "County": "county", + "Inc_Muni": "city", + "Post_City": "post_city", + "State": "state", + "UUID": "guid", + "AddAuth": "address_authority", + "Longitude": "long", + "Latitude": "lat", + "NatGrid": "nat_grid", + "Placement": "placement", + "AddrPoint": "address_point", + "DateUpdate": "date_updated", + "NAD_Source": "source", + "DataSet_ID": "id", + } + + cm = add_column_map(app_context, user.id, "Test", mapping) + result = update_column_mapping( + app_context, + cm.id, + { + "Add_Number": "foo", + "AddNo_Full": "address_number_full", + "St_Name": "street_name", + "StNam_Full": "street_name_full", + "County": "county", + "Inc_Muni": "city", + "Post_City": "post_city", + "State": "state", + "UUID": "guid", + "AddAuth": "address_authority", + "Longitude": "long", + "Latitude": "lat", + "NatGrid": "nat_grid", + "Placement": "placement", + "AddrPoint": "address_point", + "DateUpdate": "date_updated", + "NAD_Source": "source", + "DataSet_ID": "id", + }, + ) + assert isinstance(result, ColumnMapViewModel) + assert result.mapping["Add_Number"] == "foo" + + +def test_update_column_mapping_field(app_context): + app_context.producers.add(DataProducer("New Jersey")) + user = app_context.users.add(User("test@test.org", "foo", "bar")) + + mapping = { + "Add_Number": "address_number", + "AddNo_Full": "address_number_full", + "St_Name": "street_name", + "StNam_Full": "street_name_full", + "County": "county", + "Inc_Muni": "city", + "Post_City": "post_city", + "State": "state", + "UUID": "guid", + "AddAuth": "address_authority", + "Longitude": "long", + "Latitude": "lat", + "NatGrid": "nat_grid", + "Placement": "placement", + "AddrPoint": "address_point", + "DateUpdate": "date_updated", + "NAD_Source": "source", + "DataSet_ID": "id", + } + + cm = add_column_map(app_context, user.id, "Test", mapping) + result = update_column_mapping_field(app_context, cm.id, "foo", "Add_Number") + + assert isinstance(result, ColumnMapViewModel) + assert result.mapping["Add_Number"] == "foo" diff --git a/tests/fakes_and_mocks.py b/tests/fakes_and_mocks.py index 70678a9..56f2f89 100644 --- a/tests/fakes_and_mocks.py +++ b/tests/fakes_and_mocks.py @@ -79,6 +79,9 @@ def __init__(self) -> None: self._column_maps = set() self._next_id = 1 + def get_by_id(self, id: int) -> Optional[ColumnMap]: + return next((cm for cm in self._column_maps if cm.id == id), None) + def add(self, column_map: ColumnMap) -> ColumnMap: column_map.id = self._next_id column_map.set_created_at(datetime.now()) @@ -105,6 +108,22 @@ def get_by_name_and_version(self, name: str, version: int) -> Optional[ColumnMap None, ) + def get_by_producer(self, producer: DataProducer) -> Iterable[ColumnMap]: + return [cm for cm in self._column_maps if cm.producer.name == producer.name] + + def update(self, column_map: ColumnMap) -> ColumnMap: + self._column_maps.remove( + next( + (cm for cm in self._column_maps if cm.name == column_map.name), + None, + ) + ) + self._column_maps.add(column_map) + return column_map + + def remove(self, column_map: ColumnMap) -> None: + self._column_maps.remove(column_map) + class FakeStorage: def __init__(self): From a6f274773a14ce2b37b3e804becd4467716c968c Mon Sep 17 00:00:00 2001 From: Andy Kuny Date: Fri, 22 Mar 2024 10:40:19 -0400 Subject: [PATCH 49/63] Rename domain dir core to avoid confusion with gis terminology --- nad_ch/application/interfaces.py | 2 +- nad_ch/application/use_cases/auth.py | 2 +- nad_ch/application/use_cases/column_maps.py | 2 +- nad_ch/application/use_cases/data_producers.py | 2 +- nad_ch/application/use_cases/data_submissions.py | 2 +- nad_ch/application/view_models.py | 2 +- nad_ch/controllers/web/routes/auth.py | 2 +- nad_ch/{domain => core}/entities.py | 0 nad_ch/{domain => core}/repositories.py | 2 +- nad_ch/infrastructure/database.py | 4 ++-- nad_ch/infrastructure/task_queue.py | 2 +- scripts/seed.py | 2 +- tests/application/test_view_models.py | 2 +- tests/application/use_cases/test_auth.py | 2 +- tests/application/use_cases/test_column_maps.py | 2 +- tests/application/use_cases/test_data_submissions.py | 2 +- tests/controllers/test_web.py | 2 +- tests/{domain => core}/__init__.py | 0 tests/{domain => core}/test_entities.py | 2 +- tests/fakes_and_mocks.py | 4 ++-- tests/fixtures.py | 2 +- tests/infrastructure/test_database.py | 2 +- 22 files changed, 22 insertions(+), 22 deletions(-) rename nad_ch/{domain => core}/entities.py (100%) rename nad_ch/{domain => core}/repositories.py (95%) rename tests/{domain => core}/__init__.py (100%) rename tests/{domain => core}/test_entities.py (97%) diff --git a/nad_ch/application/interfaces.py b/nad_ch/application/interfaces.py index 87516a0..94e1a2b 100644 --- a/nad_ch/application/interfaces.py +++ b/nad_ch/application/interfaces.py @@ -1,6 +1,6 @@ from typing import Optional, Protocol, Dict from nad_ch.application.dtos import DownloadResult -from nad_ch.domain.repositories import ( +from nad_ch.core.repositories import ( DataProducerRepository, DataSubmissionRepository, UserRepository, diff --git a/nad_ch/application/use_cases/auth.py b/nad_ch/application/use_cases/auth.py index 192c168..dee4596 100644 --- a/nad_ch/application/use_cases/auth.py +++ b/nad_ch/application/use_cases/auth.py @@ -5,7 +5,7 @@ OAuth2TokenError, ) from nad_ch.application.interfaces import ApplicationContext -from nad_ch.domain.entities import User +from nad_ch.core.entities import User def get_or_create_user(ctx: ApplicationContext, provider_name: str, email: str) -> User: diff --git a/nad_ch/application/use_cases/column_maps.py b/nad_ch/application/use_cases/column_maps.py index 32f7030..3050c1e 100644 --- a/nad_ch/application/use_cases/column_maps.py +++ b/nad_ch/application/use_cases/column_maps.py @@ -4,7 +4,7 @@ get_view_model, ColumnMapViewModel, ) -from nad_ch.domain.entities import ColumnMap +from nad_ch.core.entities import ColumnMap def add_column_map( diff --git a/nad_ch/application/use_cases/data_producers.py b/nad_ch/application/use_cases/data_producers.py index c16da95..421b9fb 100644 --- a/nad_ch/application/use_cases/data_producers.py +++ b/nad_ch/application/use_cases/data_producers.py @@ -4,7 +4,7 @@ get_view_model, DataProducerViewModel, ) -from nad_ch.domain.entities import DataProducer +from nad_ch.core.entities import DataProducer def add_data_producer( diff --git a/nad_ch/application/use_cases/data_submissions.py b/nad_ch/application/use_cases/data_submissions.py index 82229b1..6d97223 100644 --- a/nad_ch/application/use_cases/data_submissions.py +++ b/nad_ch/application/use_cases/data_submissions.py @@ -6,7 +6,7 @@ get_view_model, DataSubmissionViewModel, ) -from nad_ch.domain.entities import DataSubmission, ColumnMap +from nad_ch.core.entities import DataSubmission, ColumnMap def ingest_data_submission( diff --git a/nad_ch/application/view_models.py b/nad_ch/application/view_models.py index dba7a4a..b905839 100644 --- a/nad_ch/application/view_models.py +++ b/nad_ch/application/view_models.py @@ -3,7 +3,7 @@ import json import numpy as np from typing import Union, Dict, List, Tuple, Protocol -from nad_ch.domain.entities import Entity, ColumnMap, DataProducer, DataSubmission +from nad_ch.core.entities import Entity, ColumnMap, DataProducer, DataSubmission class ViewModel(Protocol): diff --git a/nad_ch/controllers/web/routes/auth.py b/nad_ch/controllers/web/routes/auth.py index 805e4cc..88992ea 100644 --- a/nad_ch/controllers/web/routes/auth.py +++ b/nad_ch/controllers/web/routes/auth.py @@ -25,7 +25,7 @@ get_user_email, get_user_email_domain_status, ) -from nad_ch.domain.entities import User +from nad_ch.core.entities import User login_view = "index" diff --git a/nad_ch/domain/entities.py b/nad_ch/core/entities.py similarity index 100% rename from nad_ch/domain/entities.py rename to nad_ch/core/entities.py diff --git a/nad_ch/domain/repositories.py b/nad_ch/core/repositories.py similarity index 95% rename from nad_ch/domain/repositories.py rename to nad_ch/core/repositories.py index 608f74b..0b368f5 100644 --- a/nad_ch/domain/repositories.py +++ b/nad_ch/core/repositories.py @@ -1,6 +1,6 @@ from typing import Optional, Protocol from collections.abc import Iterable -from nad_ch.domain.entities import DataProducer, DataSubmission, User, ColumnMap +from nad_ch.core.entities import DataProducer, DataSubmission, User, ColumnMap class DataProducerRepository(Protocol): diff --git a/nad_ch/infrastructure/database.py b/nad_ch/infrastructure/database.py index 6afc785..4e83710 100644 --- a/nad_ch/infrastructure/database.py +++ b/nad_ch/infrastructure/database.py @@ -2,8 +2,8 @@ import json from typing import List, Optional from flask_login import UserMixin -from nad_ch.domain.entities import DataProducer, DataSubmission, User, ColumnMap -from nad_ch.domain.repositories import ( +from nad_ch.core.entities import DataProducer, DataSubmission, User, ColumnMap +from nad_ch.core.repositories import ( DataProducerRepository, DataSubmissionRepository, UserRepository, diff --git a/nad_ch/infrastructure/task_queue.py b/nad_ch/infrastructure/task_queue.py index 8aac8b1..3a760d5 100644 --- a/nad_ch/infrastructure/task_queue.py +++ b/nad_ch/infrastructure/task_queue.py @@ -13,7 +13,7 @@ finalize_overview_details, ) from nad_ch.config import QUEUE_BROKER_URL, QUEUE_BACKEND_URL -from nad_ch.domain.repositories import DataSubmissionRepository +from nad_ch.core.repositories import DataSubmissionRepository from typing import Dict diff --git a/scripts/seed.py b/scripts/seed.py index f8dabcd..20eb9c6 100644 --- a/scripts/seed.py +++ b/scripts/seed.py @@ -1,7 +1,7 @@ import os import zipfile from nad_ch.config import create_app_context, OAUTH2_CONFIG -from nad_ch.domain.entities import ColumnMap, DataProducer, DataSubmission, User +from nad_ch.core.entities import ColumnMap, DataProducer, DataSubmission, User def zip_directory(folder_path, zip_path): diff --git a/tests/application/test_view_models.py b/tests/application/test_view_models.py index c85db87..d15b62a 100644 --- a/tests/application/test_view_models.py +++ b/tests/application/test_view_models.py @@ -4,7 +4,7 @@ DataProducerViewModel, DataSubmissionViewModel, ) -from nad_ch.domain.entities import DataProducer, DataSubmission, ColumnMap +from nad_ch.core.entities import DataProducer, DataSubmission, ColumnMap def test_get_a_single_data_producer_view_model(): diff --git a/tests/application/use_cases/test_auth.py b/tests/application/use_cases/test_auth.py index dd48493..26a239b 100644 --- a/tests/application/use_cases/test_auth.py +++ b/tests/application/use_cases/test_auth.py @@ -12,7 +12,7 @@ get_user_email, get_user_email_domain_status, ) -from nad_ch.domain.entities import User +from nad_ch.core.entities import User from nad_ch.config import create_app_context diff --git a/tests/application/use_cases/test_column_maps.py b/tests/application/use_cases/test_column_maps.py index 938e138..0852ee8 100644 --- a/tests/application/use_cases/test_column_maps.py +++ b/tests/application/use_cases/test_column_maps.py @@ -7,7 +7,7 @@ update_column_mapping_field, ) from nad_ch.application.view_models import ColumnMapViewModel -from nad_ch.domain.entities import ColumnMap, DataProducer, User +from nad_ch.core.entities import ColumnMap, DataProducer, User from nad_ch.config import create_app_context diff --git a/tests/application/use_cases/test_data_submissions.py b/tests/application/use_cases/test_data_submissions.py index f2e9f72..a08b97f 100644 --- a/tests/application/use_cases/test_data_submissions.py +++ b/tests/application/use_cases/test_data_submissions.py @@ -11,7 +11,7 @@ DataSubmissionViewModel, ) from nad_ch.config import create_app_context -from nad_ch.domain.repositories import DataSubmissionRepository +from nad_ch.core.repositories import DataSubmissionRepository from typing import Dict diff --git a/tests/controllers/test_web.py b/tests/controllers/test_web.py index 0f083e2..c13910f 100644 --- a/tests/controllers/test_web.py +++ b/tests/controllers/test_web.py @@ -2,7 +2,7 @@ import pytest from nad_ch.config import create_app_context from nad_ch.controllers.web.flask import create_flask_application -from nad_ch.domain.entities import User +from nad_ch.core.entities import User @pytest.fixture diff --git a/tests/domain/__init__.py b/tests/core/__init__.py similarity index 100% rename from tests/domain/__init__.py rename to tests/core/__init__.py diff --git a/tests/domain/test_entities.py b/tests/core/test_entities.py similarity index 97% rename from tests/domain/test_entities.py rename to tests/core/test_entities.py index 4eaaff6..a41dfe5 100644 --- a/tests/domain/test_entities.py +++ b/tests/core/test_entities.py @@ -1,5 +1,5 @@ import datetime -from nad_ch.domain.entities import DataProducer, DataSubmission, ColumnMap +from nad_ch.core.entities import DataProducer, DataSubmission, ColumnMap def test_data_submission_generates_filename(): diff --git a/tests/fakes_and_mocks.py b/tests/fakes_and_mocks.py index 56f2f89..15211fc 100644 --- a/tests/fakes_and_mocks.py +++ b/tests/fakes_and_mocks.py @@ -1,8 +1,8 @@ from datetime import datetime from typing import Optional, Iterable from nad_ch.application.dtos import DownloadResult -from nad_ch.domain.entities import DataProducer, DataSubmission, User, ColumnMap -from nad_ch.domain.repositories import ( +from nad_ch.core.entities import DataProducer, DataSubmission, User, ColumnMap +from nad_ch.core.repositories import ( DataProducerRepository, DataSubmissionRepository, UserRepository, diff --git a/tests/fixtures.py b/tests/fixtures.py index 4f5e042..a0864f7 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -5,7 +5,7 @@ import pathlib from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker -from nad_ch.domain.entities import DataProducer, DataSubmission, ColumnMap, User +from nad_ch.core.entities import DataProducer, DataSubmission, ColumnMap, User from nad_ch.infrastructure.database import ( ModelBase, SqlAlchemyDataProducerRepository, diff --git a/tests/infrastructure/test_database.py b/tests/infrastructure/test_database.py index 3c1311e..9456300 100644 --- a/tests/infrastructure/test_database.py +++ b/tests/infrastructure/test_database.py @@ -1,6 +1,6 @@ from conftest import TEST_COLUMN_MAPS_PATH import yaml -from nad_ch.domain.entities import DataProducer, DataSubmission, ColumnMap, User +from nad_ch.core.entities import DataProducer, DataSubmission, ColumnMap, User def test_add_producer(producers): From 5e7a991dbd1f504d28d03878cfbd99b94d592ab4 Mon Sep 17 00:00:00 2001 From: Andy Kuny Date: Fri, 22 Mar 2024 12:08:55 -0400 Subject: [PATCH 50/63] Tweak pending items --- nad_ch/application/use_cases/column_maps.py | 4 ++++ nad_ch/controllers/web/routes/column_maps.py | 5 +++++ scripts/seed.py | 1 - 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/nad_ch/application/use_cases/column_maps.py b/nad_ch/application/use_cases/column_maps.py index 3050c1e..71366ca 100644 --- a/nad_ch/application/use_cases/column_maps.py +++ b/nad_ch/application/use_cases/column_maps.py @@ -19,6 +19,10 @@ def add_column_map( if producer is None: raise ValueError("Producer not found") + # Note: will need to account for admin permissions to update any DataProducer's + # column mapping, and for users associated with the DataProducer to update ONLY + # their own column mapping + column_map = ColumnMap(name, producer, mapping, 1) if not column_map.is_valid(): diff --git a/nad_ch/controllers/web/routes/column_maps.py b/nad_ch/controllers/web/routes/column_maps.py index a4d6fb4..ede58eb 100644 --- a/nad_ch/controllers/web/routes/column_maps.py +++ b/nad_ch/controllers/web/routes/column_maps.py @@ -63,6 +63,8 @@ def store(): flash("No selected file") return redirect(url_for("column_maps.create")) + # TODO instead of validating the file extension, validate the file by trying to open + # it as a CSV and reading in the headers if not file.filename.endswith(".csv"): flash("File is not a CSV") return redirect(url_for("column_maps.create")) @@ -70,6 +72,7 @@ def store(): if file: csv_dict = {} + # TODO: also test with CSV files from a Windows machine (look up the differences) try: file_content = file.read().decode("utf-8").splitlines() csv_reader = csv.reader(file_content) @@ -91,6 +94,8 @@ def store(): return redirect(url_for("column_maps.create")) +# Note: get ID from route, not hidden field in form. +# Make this its own POST route to /column-maps/update/ def update(request): id = request.form.get("_id") diff --git a/scripts/seed.py b/scripts/seed.py index 20eb9c6..16e3e31 100644 --- a/scripts/seed.py +++ b/scripts/seed.py @@ -35,7 +35,6 @@ def main(): new_column_map = ColumnMap( name="NewJerseyMapping", producer=saved_producer, version_id=1 ) - # TODO save column map once ApplicationContext can provide a repository new_column_map.mapping = { "AddNum_Pre": "", "Add_Number": "address_number", From 2e53ebe99c09feb8efba6391ce908dc0c9c7894c Mon Sep 17 00:00:00 2001 From: = <=> Date: Fri, 22 Mar 2024 18:47:46 -0400 Subject: [PATCH 51/63] Add domain frequencies to validation report --- nad_ch/application/dtos.py | 9 ++-- nad_ch/application/validation.py | 31 ++++++++--- tests/application/test_dto.py | 2 + tests/application/test_validation.py | 66 +++++++++++++++++++++++ tests/test_data/baselines.py | 80 ++++++++++++++++++++++++++++ 5 files changed, 175 insertions(+), 13 deletions(-) diff --git a/nad_ch/application/dtos.py b/nad_ch/application/dtos.py index 7defed8..9f9a0d0 100644 --- a/nad_ch/application/dtos.py +++ b/nad_ch/application/dtos.py @@ -29,12 +29,9 @@ class DataSubmissionReportFeature: invalid_domain_count: int = 0 valid_domain_count: int = 0 invalid_domains: List[str] = field(default_factory=list) - # TODO: Add frequency charts for each field and only take the top 10 if - # more than 10 values exist - # invalid_domain_frequencies: Dict[str, int] - # Set to True if invalid_domains & invalid_domain_frequencies doesn't contain - # a full list of unique domains found in source data - # invalid_domain_list_truncated: bool = False + domain_frequency: Dict[str, Dict[str, int]] = field(default_factory=dict) + # Set to true when there is too many unexpected domain values found for a field + high_domain_cardinality: bool = False @dataclass diff --git a/nad_ch/application/validation.py b/nad_ch/application/validation.py index 732bf32..af5ffe9 100644 --- a/nad_ch/application/validation.py +++ b/nad_ch/application/validation.py @@ -8,6 +8,7 @@ import glob from pathlib import Path from nad_ch.domain.entities import ColumnMap +from collections import Counter class DataValidator: @@ -90,7 +91,7 @@ def update_feature_details(self, gdf: GeoDataFrame): feature_submission.populated_count += populated_count feature_submission.null_count += null_count - # Update domain specific metrics + # Update invalid domain metrics column_domain_dict = self.domains["domain"].get(column) column_mapper_dict = self.domains["mapper"].get(column) if column_domain_dict and column_mapper_dict: @@ -124,12 +125,28 @@ def update_feature_details(self, gdf: GeoDataFrame): ) feature_submission.invalid_domain_count += invalid_domain_count feature_submission.valid_domain_count += valid_domain_count - # Can only store up to 10 invalid domains per nad field - invalid_domain_unique_count = len(invalid_domains) - remaining_slots = 10 - len(feature_submission.invalid_domains) - if invalid_domain_unique_count and remaining_slots > 0: - invalid_domains = invalid_domains[:remaining_slots] - feature_submission.invalid_domains.extend(invalid_domains) + # Can only store up to 100 invalid domains per nad field + remaining_slots = 100 - len(feature_submission.invalid_domains) + if invalid_domains and remaining_slots > 0: + feature_submission.invalid_domains.extend( + invalid_domains[:remaining_slots] + ) + + # Generate frequency table of fields that are domain specific only + if column_domain_dict: + domain_freq = gdf[column].value_counts().to_dict() + if feature_submission.domain_frequency: + domain_freq = dict( + Counter(feature_submission.domain_frequency) + + Counter(domain_freq) + ) + # Check if the number of unique domains in frequency dictionary + # is 2x greater than maximum expected unique domains + if len(domain_freq.keys()) > 2 * len(column_domain_dict.keys()): + feature_submission.high_domain_cardinality = True + # Reset domain frequency + domain_freq = {} + feature_submission.domain_frequency = domain_freq def update_overview_details(self, gdf: GeoDataFrame): self.report_overview.records_count += self.get_record_count(gdf) diff --git a/tests/application/test_dto.py b/tests/application/test_dto.py index f8f0d66..a6fef43 100644 --- a/tests/application/test_dto.py +++ b/tests/application/test_dto.py @@ -51,6 +51,8 @@ def test_to_dict_with_numpy_types(): "valid_domain_count": 90, "invalid_domain_count": 10, "invalid_domains": [], + "domain_frequency": {}, + "high_domain_cardinality": False, } assert isinstance(feature_dict["populated_count"], int) assert isinstance(feature_dict["null_count"], float) diff --git a/tests/application/test_validation.py b/tests/application/test_validation.py index 5f82bd9..8ffccff 100644 --- a/tests/application/test_validation.py +++ b/tests/application/test_validation.py @@ -97,6 +97,72 @@ def test_update_feature_details(): assert feature.valid_domain_count == 0 assert feature.invalid_domains == ["Anycounty"] + # Domain frequency assertions + for nad_field in ("St_PreSep", "St_PreTyp", "St_PosDir"): + assert data_validator.report_features.get(nad_field).domain_frequency == {} + assert data_validator.report_features.get("State").domain_frequency == {"IN": 10} + assert data_validator.report_features.get("St_PosTyp").domain_frequency == { + "Street": 10 + } + assert data_validator.report_features.get("St_PreDir").domain_frequency == { + "South": 10 + } + assert data_validator.report_features.get("Placement").domain_frequency == { + "Structure - Rooftop": 10 + } + assert data_validator.report_features.get("County").domain_frequency == { + "Anycounty": 10 + } + assert all( + data_validator.report_features.get(field).high_domain_cardinality is False + for field in data_validator.report_features.keys() + ) + + +def test_update_feature_details_force_high_domain_cardinality(): + gdf = create_fake_geopandas_dataframe(num_rows=200) + gdf["St_PreDir"] = [f"PreDirection{i}" for i in range(len(gdf))] + gdf.loc[[10, 20], "St_PreDir"] = "Northeast" + gdf["Placement"] = [f"Place{i}" for i in range(len(gdf))] + gdf.loc[[10, 20], "Placement"] = "Parcel - Centroid" + column_map = create_fake_column_map_from_gdf(gdf) + data_validator = DataValidator(column_map) + data_validator.initialize_overview_details(gdf, column_map) + data_validator.update_feature_details(gdf) + + # Invalid Domain assertions + for field in ("St_PreDir", "Placement"): + feature = data_validator.report_features.get(field) + assert feature.invalid_domain_count == 198 + assert feature.valid_domain_count == 2 + # The first 100 invalid domains that were saved + assert len(feature.invalid_domains) == 100 + assert all( + domain in feature.invalid_domains + for domain in gdf[field].to_list()[:102] + if domain not in ("Parcel - Centroid", "Northeast") + ) + # Invalid domains that were NOT saved after reaching max of 100 + assert all( + domain not in feature.invalid_domains + for domain in gdf[field].to_list()[102:] + ) + + # High domain cardinality assertions + assert all( + data_validator.report_features.get(field).high_domain_cardinality is False + for field in data_validator.report_features.keys() + if field not in ("St_PreDir", "Placement") + ) + assert all( + data_validator.report_features.get(field).high_domain_cardinality is True + for field in ("St_PreDir", "Placement") + ) + assert all( + data_validator.report_features.get(field).domain_frequency == {} + for field in ("St_PreDir", "Placement") + ) + def test_initialize_overview_details(): gdf = create_fake_geopandas_dataframe(num_rows=1) diff --git a/tests/test_data/baselines.py b/tests/test_data/baselines.py index d3ddfbe..9272c5b 100644 --- a/tests/test_data/baselines.py +++ b/tests/test_data/baselines.py @@ -312,6 +312,8 @@ "invalid_domain_count": 0, "valid_domain_count": 0, "invalid_domains": [], + "domain_frequency": {}, + "high_domain_cardinality": False, }, { "provided_feature_name": "ST", @@ -321,6 +323,8 @@ "invalid_domain_count": 0, "valid_domain_count": 0, "invalid_domains": [], + "domain_frequency": {}, + "high_domain_cardinality": False, }, { "provided_feature_name": "ZIP", @@ -330,6 +334,8 @@ "invalid_domain_count": 0, "valid_domain_count": 0, "invalid_domains": [], + "domain_frequency": {}, + "high_domain_cardinality": False, }, { "provided_feature_name": "RuleID", @@ -339,6 +345,8 @@ "invalid_domain_count": 0, "valid_domain_count": 0, "invalid_domains": [], + "domain_frequency": {}, + "high_domain_cardinality": False, }, { "provided_feature_name": "geometry", @@ -348,6 +356,8 @@ "invalid_domain_count": 0, "valid_domain_count": 0, "invalid_domains": [], + "domain_frequency": {}, + "high_domain_cardinality": False, }, ], } @@ -384,6 +394,8 @@ "invalid_domain_count": 0, "valid_domain_count": 0, "invalid_domains": [], + "domain_frequency": {}, + "high_domain_cardinality": False, }, { "provided_feature_name": "ADD_SUFFIX", @@ -393,6 +405,8 @@ "invalid_domain_count": 0, "valid_domain_count": 0, "invalid_domains": [], + "domain_frequency": {}, + "high_domain_cardinality": False, }, { "provided_feature_name": "STR_DIR", @@ -402,6 +416,15 @@ "invalid_domain_count": 6, "valid_domain_count": 154, "invalid_domains": ["northerns", "southerns"], + "domain_frequency": { + "N": 50, + "W": 36, + "S": 35, + "E": 33, + "northerns": 3, + "southerns": 3, + }, + "high_domain_cardinality": False, }, { "provided_feature_name": "STR_PRETYP", @@ -411,6 +434,13 @@ "invalid_domain_count": 0, "valid_domain_count": 10, "invalid_domains": [], + "domain_frequency": { + "STHY": 5, + "CALLE": 2, + "CAMINO": 2, + "NEW MEXICO HWY": 1, + }, + "high_domain_cardinality": False, }, { "provided_feature_name": "STR_NAME", @@ -420,6 +450,8 @@ "invalid_domain_count": 0, "valid_domain_count": 0, "invalid_domains": [], + "domain_frequency": {}, + "high_domain_cardinality": False, }, { "provided_feature_name": "STR_SUFFIX", @@ -429,6 +461,27 @@ "invalid_domain_count": 8, "valid_domain_count": 831, "invalid_domains": ["Drive Parkway", "Crossings Drive", "Unknown Drive"], + "domain_frequency": { + "RD": 178, + "ST": 167, + "DR": 164, + "AVE": 126, + "LN": 46, + "CT": 36, + "PL": 25, + "BLVD": 22, + "TRL": 18, + "WAY": 18, + "CIR": 14, + "LOOP": 12, + "Crossings Drive": 3, + "Drive Parkway": 3, + "PKWY": 3, + "Unknown Drive": 2, + "HWY": 1, + "RD.": 1, + }, + "high_domain_cardinality": False, }, { "provided_feature_name": "POST_DIR", @@ -438,6 +491,17 @@ "invalid_domain_count": 0, "valid_domain_count": 328, "invalid_domains": [], + "domain_frequency": { + "NE": 159, + "NW": 64, + "SE": 51, + "SW": 48, + "N": 2, + "S": 2, + "E": 1, + "W": 1, + }, + "high_domain_cardinality": False, }, { "provided_feature_name": "ROAD_LABEL", @@ -447,6 +511,8 @@ "invalid_domain_count": 0, "valid_domain_count": 0, "invalid_domains": [], + "domain_frequency": {}, + "high_domain_cardinality": False, }, { "provided_feature_name": "COMNAME", @@ -456,6 +522,8 @@ "invalid_domain_count": 0, "valid_domain_count": 0, "invalid_domains": [], + "domain_frequency": {}, + "high_domain_cardinality": False, }, { "provided_feature_name": "MSAG_COM", @@ -465,6 +533,8 @@ "invalid_domain_count": 0, "valid_domain_count": 0, "invalid_domains": [], + "domain_frequency": {}, + "high_domain_cardinality": False, }, { "provided_feature_name": "ZIPCODE", @@ -474,6 +544,8 @@ "invalid_domain_count": 0, "valid_domain_count": 0, "invalid_domains": [], + "domain_frequency": {}, + "high_domain_cardinality": False, }, { "provided_feature_name": "DPID", @@ -483,6 +555,8 @@ "invalid_domain_count": 0, "valid_domain_count": 0, "invalid_domains": [], + "domain_frequency": {}, + "high_domain_cardinality": False, }, { "provided_feature_name": "DATE_UPD", @@ -492,6 +566,8 @@ "invalid_domain_count": 0, "valid_domain_count": 0, "invalid_domains": [], + "domain_frequency": {}, + "high_domain_cardinality": False, }, { "provided_feature_name": "last_edi_1", @@ -501,6 +577,8 @@ "invalid_domain_count": 0, "valid_domain_count": 0, "invalid_domains": [], + "domain_frequency": {}, + "high_domain_cardinality": False, }, { "provided_feature_name": "EXCEPTION", @@ -510,6 +588,8 @@ "invalid_domain_count": 0, "valid_domain_count": 0, "invalid_domains": [], + "domain_frequency": {}, + "high_domain_cardinality": False, }, ], } From 82118fcd7c410b0ccb07623edee9cd829d754ab2 Mon Sep 17 00:00:00 2001 From: Andy Kuny Date: Mon, 25 Mar 2024 08:46:37 -0400 Subject: [PATCH 52/63] Update column_map route handlers --- nad_ch/controllers/web/routes/column_maps.py | 57 +++++++++---------- .../web/templates/column_maps/edit.html | 6 +- .../web/templates/column_maps/show.html | 3 +- 3 files changed, 34 insertions(+), 32 deletions(-) diff --git a/nad_ch/controllers/web/routes/column_maps.py b/nad_ch/controllers/web/routes/column_maps.py index ede58eb..431092c 100644 --- a/nad_ch/controllers/web/routes/column_maps.py +++ b/nad_ch/controllers/web/routes/column_maps.py @@ -1,4 +1,5 @@ import csv +import io from flask import ( Blueprint, current_app, @@ -63,42 +64,40 @@ def store(): flash("No selected file") return redirect(url_for("column_maps.create")) - # TODO instead of validating the file extension, validate the file by trying to open - # it as a CSV and reading in the headers - if not file.filename.endswith(".csv"): - flash("File is not a CSV") - return redirect(url_for("column_maps.create")) - - if file: - csv_dict = {} + try: + file_content = file.read().decode("utf-8-sig") + stream = io.StringIO(file_content) + csv_reader = csv.reader(stream, dialect="excel") - # TODO: also test with CSV files from a Windows machine (look up the differences) - try: - file_content = file.read().decode("utf-8").splitlines() - csv_reader = csv.reader(file_content) + headers = next(csv_reader) + if not headers: + flash("CSV file seems to be empty or invalid") + return redirect(url_for("column_maps.create")) - for row in csv_reader: - key, value = row - csv_dict[key] = value + csv_dict = {} - except Exception as e: - flash(f"An error occurred while processing the file: {e}") - return redirect(url_for("column_maps.create")) + for row in csv_reader: + if len(row) < 2: + continue + key, value = row[:2] + csv_dict[key] = value - try: - name = request.form.get("name") - view_model = add_column_map(g.ctx, current_user.id, name, csv_dict) - return redirect(url_for("column_maps.show", id=view_model.id)) - except ValueError: - flash("Error: ", str(ValueError)) - return redirect(url_for("column_maps.create")) + except Exception as e: + flash(f"An error occurred while processing the file: {e}") + return redirect(url_for("column_maps.create")) + try: + name = request.form.get("name") + view_model = add_column_map(g.ctx, current_user.id, name, csv_dict) + return redirect(url_for("column_maps.show", id=view_model.id)) + except ValueError as e: + flash(f"Error: {e}") + return redirect(url_for("column_maps.create")) -# Note: get ID from route, not hidden field in form. -# Make this its own POST route to /column-maps/update/ -def update(request): - id = request.form.get("_id") +@column_maps_bp.route("/column-maps/update/", methods=["POST"]) +@login_required +def update(id): if request.form.get("_formType") == "existing_fields": excluded_form_keys = ("_method", "_formType", "_id") diff --git a/nad_ch/controllers/web/templates/column_maps/edit.html b/nad_ch/controllers/web/templates/column_maps/edit.html index 813e36a..53333ec 100644 --- a/nad_ch/controllers/web/templates/column_maps/edit.html +++ b/nad_ch/controllers/web/templates/column_maps/edit.html @@ -3,7 +3,11 @@ import arrow_right %} {% from "components/icons/edit.html" import edit %} {% block content %} -
    + diff --git a/nad_ch/controllers/web/templates/column_maps/show.html b/nad_ch/controllers/web/templates/column_maps/show.html index 38ac512..2ea8648 100644 --- a/nad_ch/controllers/web/templates/column_maps/show.html +++ b/nad_ch/controllers/web/templates/column_maps/show.html @@ -35,13 +35,12 @@
    -
    From 4d14d4c3f907fd8965b83f88a62ab5a4e7401699 Mon Sep 17 00:00:00 2001 From: Andy Kuny Date: Mon, 25 Mar 2024 08:54:22 -0400 Subject: [PATCH 53/63] Remove hidden PUT fields and hook --- nad_ch/controllers/web/routes/column_maps.py | 3 --- nad_ch/controllers/web/templates/column_maps/edit.html | 1 - nad_ch/controllers/web/templates/column_maps/show.html | 1 - 3 files changed, 5 deletions(-) diff --git a/nad_ch/controllers/web/routes/column_maps.py b/nad_ch/controllers/web/routes/column_maps.py index 431092c..e50af5d 100644 --- a/nad_ch/controllers/web/routes/column_maps.py +++ b/nad_ch/controllers/web/routes/column_maps.py @@ -52,9 +52,6 @@ def create(): @column_maps_bp.route("/column-maps", methods=["POST"]) @login_required def store(): - if request.form.get("_method") == "PUT": - return update(request) - if "mapping-csv-input" not in request.files: flash("No file included") return redirect(url_for("column_maps.create")) diff --git a/nad_ch/controllers/web/templates/column_maps/edit.html b/nad_ch/controllers/web/templates/column_maps/edit.html index 53333ec..3b6ab2d 100644 --- a/nad_ch/controllers/web/templates/column_maps/edit.html +++ b/nad_ch/controllers/web/templates/column_maps/edit.html @@ -8,7 +8,6 @@ method="post" enctype="multipart/form-data" > - diff --git a/nad_ch/controllers/web/templates/column_maps/show.html b/nad_ch/controllers/web/templates/column_maps/show.html index 2ea8648..15b3484 100644 --- a/nad_ch/controllers/web/templates/column_maps/show.html +++ b/nad_ch/controllers/web/templates/column_maps/show.html @@ -39,7 +39,6 @@ method="post" enctype="multipart/form-data" > -
    From 30aedadeb45fb04e44bc862fd191ac1b7bafa3e5 Mon Sep 17 00:00:00 2001 From: Andy Kuny Date: Mon, 25 Mar 2024 08:59:41 -0400 Subject: [PATCH 54/63] Display user feedback for invalid mappings --- nad_ch/controllers/web/routes/column_maps.py | 16 ++++++++++------ .../web/templates/column_maps/create.html | 4 ++-- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/nad_ch/controllers/web/routes/column_maps.py b/nad_ch/controllers/web/routes/column_maps.py index e50af5d..c50effa 100644 --- a/nad_ch/controllers/web/routes/column_maps.py +++ b/nad_ch/controllers/web/routes/column_maps.py @@ -52,14 +52,19 @@ def create(): @column_maps_bp.route("/column-maps", methods=["POST"]) @login_required def store(): + name = request.form.get("name") + if not name: + flash("Name is required") + return redirect(url_for("column_maps.create")) + if "mapping-csv-input" not in request.files: flash("No file included") - return redirect(url_for("column_maps.create")) + return redirect(url_for("column_maps.create", name=name)) file = request.files["mapping-csv-input"] if file.filename == "": flash("No selected file") - return redirect(url_for("column_maps.create")) + return redirect(url_for("column_maps.create", name=name)) try: file_content = file.read().decode("utf-8-sig") @@ -69,7 +74,7 @@ def store(): headers = next(csv_reader) if not headers: flash("CSV file seems to be empty or invalid") - return redirect(url_for("column_maps.create")) + return redirect(url_for("column_maps.create", name=name)) csv_dict = {} @@ -81,15 +86,14 @@ def store(): except Exception as e: flash(f"An error occurred while processing the file: {e}") - return redirect(url_for("column_maps.create")) + return redirect(url_for("column_maps.create", name=name)) try: - name = request.form.get("name") view_model = add_column_map(g.ctx, current_user.id, name, csv_dict) return redirect(url_for("column_maps.show", id=view_model.id)) except ValueError as e: flash(f"Error: {e}") - return redirect(url_for("column_maps.create")) + return redirect(url_for("column_maps.create", name=name)) @column_maps_bp.route("/column-maps/update/", methods=["POST"]) diff --git a/nad_ch/controllers/web/templates/column_maps/create.html b/nad_ch/controllers/web/templates/column_maps/create.html index 9f15895..bfaa58e 100644 --- a/nad_ch/controllers/web/templates/column_maps/create.html +++ b/nad_ch/controllers/web/templates/column_maps/create.html @@ -40,9 +40,9 @@

    Upload mapping

    {% with messages = get_flashed_messages() %} {% if messages %} -
    +
    {% for message in messages %} - + {% endfor %}
    {% endif %} {% endwith %} From ded964c90b0d3010d57b0fd803298cc21bc3e4a9 Mon Sep 17 00:00:00 2001 From: Andy Kuny Date: Mon, 25 Mar 2024 09:16:48 -0400 Subject: [PATCH 55/63] Display updated date for mappings --- nad_ch/application/view_models.py | 4 ++++ nad_ch/controllers/web/templates/column_maps/index.html | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/nad_ch/application/view_models.py b/nad_ch/application/view_models.py index b905839..124167f 100644 --- a/nad_ch/application/view_models.py +++ b/nad_ch/application/view_models.py @@ -41,6 +41,7 @@ def get_view_model( class ColumnMapViewModel(ViewModel): id: int date_created: str + date_updated: str name: str mapping: Dict[str, str] version: int @@ -56,9 +57,12 @@ def create_column_map_view_model(column_map: ColumnMap) -> ColumnMapViewModel: if key not in column_map.mapping or column_map.mapping.get(key) in ["", None] ] + date_updated = "-" if column_map.updated_at == column_map.created_at else present_date(column_map.updated_at) + return ColumnMapViewModel( id=column_map.id, date_created=present_date(column_map.created_at), + date_updated=date_updated, name=column_map.name, mapping=column_map.mapping, version=column_map.version_id, diff --git a/nad_ch/controllers/web/templates/column_maps/index.html b/nad_ch/controllers/web/templates/column_maps/index.html index fc7dc9e..492cfcd 100644 --- a/nad_ch/controllers/web/templates/column_maps/index.html +++ b/nad_ch/controllers/web/templates/column_maps/index.html @@ -90,7 +90,7 @@

    New mapping

    Name Created - Version + Updated @@ -101,7 +101,7 @@

    New mapping

    {{ cm.name }} {{ cm.date_created }} - {{ cm.version }} + {{ cm.date_updated }} View Date: Mon, 25 Mar 2024 09:18:35 -0400 Subject: [PATCH 56/63] Add rudimentary required field indicator to edit form --- nad_ch/controllers/web/templates/column_maps/edit.html | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/nad_ch/controllers/web/templates/column_maps/edit.html b/nad_ch/controllers/web/templates/column_maps/edit.html index 3b6ab2d..e464e34 100644 --- a/nad_ch/controllers/web/templates/column_maps/edit.html +++ b/nad_ch/controllers/web/templates/column_maps/edit.html @@ -64,7 +64,10 @@ />
    {{ arrow_right() }}
    -
    {{ nad_field }}
    +
    + {{ nad_field }} {% if nad_field in column_map.required_nad_fields %}*{% + endif %} +
    {% if nad_field not in column_map.required_nad_fields %}