Skip to content

Commit

Permalink
Merge pull request #29 from GSA-TTS/view-models
Browse files Browse the repository at this point in the history
View models
  • Loading branch information
akuny authored Feb 12, 2024
2 parents bba1eb3 + e691a57 commit a7f4cb0
Show file tree
Hide file tree
Showing 16 changed files with 332 additions and 57 deletions.
36 changes: 30 additions & 6 deletions nad_ch/application/use_cases.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,17 @@
from typing import List
from nad_ch.application.dtos import DownloadResult
from nad_ch.application.interfaces import ApplicationContext
from nad_ch.application.view_models import (
get_view_model,
DataProviderViewModel,
DataSubmissionViewModel,
)
from nad_ch.domain.entities import DataProvider, DataSubmission


def add_data_provider(ctx: ApplicationContext, provider_name: str) -> None:
def add_data_provider(
ctx: ApplicationContext, provider_name: str
) -> DataProviderViewModel:
if not provider_name:
ctx.logger.error("Provider name required")
return
Expand All @@ -19,19 +26,21 @@ def add_data_provider(ctx: ApplicationContext, provider_name: str) -> None:
ctx.providers.add(provider)
ctx.logger.info("Provider added")

return get_view_model(provider)

def list_data_providers(ctx: ApplicationContext) -> List[DataProvider]:

def list_data_providers(ctx: ApplicationContext) -> List[DataProviderViewModel]:
providers = ctx.providers.get_all()
ctx.logger.info("Data Provider Names:")
for p in providers:
ctx.logger.info(p.name)

return providers
return get_view_model(providers)


def ingest_data_submission(
ctx: ApplicationContext, file_path: str, provider_name: str
) -> None:
) -> DataSubmissionViewModel:
if not file_path:
ctx.logger.error("File path required")
return
Expand All @@ -53,12 +62,27 @@ def ingest_data_submission(
submission = DataSubmission(filename, provider)
ctx.submissions.add(submission)
ctx.logger.info(f"Submission added: {submission.filename}")

return get_view_model(submission)
except Exception as e:
ctx.storage.delete(filename)
ctx.logger.error(f"Failed to process submission: {e}")


def list_data_submissions_by_provider(ctx: ApplicationContext, provider_name: str):
def get_data_submission(
ctx: ApplicationContext, submission_id: int
) -> DataSubmissionViewModel:
submission = ctx.submissions.get_by_id(submission_id)

if submission is None:
return None

return get_view_model(submission)


def list_data_submissions_by_provider(
ctx: ApplicationContext, provider_name: str
) -> List[DataSubmissionViewModel]:
provider = ctx.providers.get_by_name(provider_name)
if not provider:
ctx.logger.error("Provider with that name does not exist")
Expand All @@ -69,7 +93,7 @@ def list_data_submissions_by_provider(ctx: ApplicationContext, provider_name: st
for s in submissions:
ctx.logger.info(f"{s.provider.name}: {s.filename}")

return submissions
return get_view_model(submissions)


def validate_data_submission(ctx: ApplicationContext, filename: str):
Expand Down
101 changes: 101 additions & 0 deletions nad_ch/application/view_models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
from dataclasses import dataclass
from datetime import datetime
import json
import numpy as np
from typing import Union, List, Tuple
from nad_ch.domain.entities import Entity, DataProvider, DataSubmission


def get_view_model(entity: Union[Entity, List[Entity]]) -> Union[Entity, List[Entity]]:
"""
Provide a single factory function that an application use case can call in order to
get a static view model object that it can return to its caller.
"""
entity_to_vm_function_map = {
DataProvider: create_data_provider_vm,
DataSubmission: create_data_submission_vm,
}

# Check if the input is a list of entities
if isinstance(entity, list):
# Process each entity in the list using a list comprehension
return [get_view_model(single_entity) for single_entity in entity]

# Process a single entity
entity_type = type(entity)
if entity_type in entity_to_vm_function_map:
mapping_function = entity_to_vm_function_map[entity_type]
return mapping_function(entity) # Call the mapping function for the entity
else:
raise ValueError(f"No mapping function defined for entity type: {entity_type}")


@dataclass
class DataProviderViewModel:
id: int
date_created: str
name: str


def create_data_provider_vm(provider: DataProvider) -> DataProviderViewModel:
return DataProviderViewModel(
id=provider.id,
date_created=present_date(provider.created_at),
name=provider.name,
)


@dataclass
class DataSubmissionViewModel:
id: int
date_created: str
filename: str
provider_name: str
report: str


def create_data_submission_vm(submission: DataSubmission) -> DataSubmissionViewModel:
# TODO make this be an empty array so the frontend doesn't have to check for None
report_json = None
if submission.report is not None:
enriched_report = enrich_report(submission.report)
report_json = json.dumps(enriched_report)

return DataSubmissionViewModel(
id=submission.id,
date_created=present_date(submission.created_at),
filename=submission.filename,
provider_name=submission.provider.name,
report=report_json,
)


def enrich_report(report: dict) -> dict:
for feature in report.get("features", []):
percent_populated, percent_empty = calculate_percentages(
feature.get("populated_count"), feature.get("null_count")
)

feature["populated_percentage"] = present_percentage(percent_populated)
feature["null_percentage"] = present_percentage(percent_empty)

return report


def present_date(date: datetime) -> str:
return date.strftime("%B %d, %Y")


def calculate_percentages(populated_count: int, null_count: int) -> Tuple[float, float]:
total_fields = populated_count + null_count
populated_percentage = (populated_count / total_fields) * 100
null_percentage = (null_count / total_fields) * 100
return populated_percentage, null_percentage


def present_percentage(percentage: float) -> str:
rounded_percentage = np.around(percentage, 2)
formatted_string = (
f"{rounded_percentage:05.2f}%" if rounded_percentage != 0 else "00.00%"
)
return formatted_string
14 changes: 13 additions & 1 deletion nad_ch/controllers/web/routes.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
from flask import Blueprint, current_app, render_template, g
from nad_ch.application.use_cases import (
list_data_submissions_by_provider,
get_data_submission,
)


home_bp = Blueprint("home", __name__)
Expand All @@ -16,4 +20,12 @@ def home():

@home_bp.route("/reports")
def reports():
return render_template("reports.html")
# For demo purposes, hard-code the provider name
view_model = list_data_submissions_by_provider(g.ctx, "NJ")
return render_template("reports/index.html", submissions=view_model)


@home_bp.route("/reports/<submission_id>")
def view_report(submission_id):
view_model = get_data_submission(g.ctx, submission_id)
return render_template("reports/show.html", submission=view_model)
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
{% include "layout/head.html" with context %}
{% include "_layouts/head.html" with context %}
<title>{% block title %}{% endblock %} - NAD Collaboration Hub</title>
</head>
<body>
<div class="body-wrapper">
<div class="grid-row minh-viewport">
<div class="grid-col-2">{% include "layout/sidebar.html" %}</div>
<div class="grid-col-2">{% include "_layouts/sidebar.html" %}</div>
<div class="grid-col-10">
<main class="main" id="main-content">
<section class="grid-container usa-section">
Expand Down
2 changes: 1 addition & 1 deletion nad_ch/controllers/web/templates/index.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{% extends "layout/base.html" %} {% block title %}Home Page{% endblock %} {%
{% extends "_layouts/base.html" %} {% block title %}Home Page{% endblock %} {%
block content %}
<h1>Welcome to the NAD Collaboration Hub</h1>
{% endblock %}
23 changes: 0 additions & 23 deletions nad_ch/controllers/web/templates/reports.html

This file was deleted.

30 changes: 30 additions & 0 deletions nad_ch/controllers/web/templates/reports/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{% extends "_layouts/base.html" %} {% block title %}Home Page{% endblock %}
{%block content %}
<h1>Reports</h1>
{% if submissions %}
<table class="usa-table usa-table--borderless">
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">Date Created</th>
<th scope="col">View</th>
</tr>
</thead>
<tbody>
{% for sub in submissions %}
<tr>
<th scope="row">{{ sub.filename }}</th>
<td>{{ sub.date_created }}</td>
<td>
<a
href="{{ url_for('home.view_report', submission_id=sub.id, _external=True) }}"
>View</a
>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p>You haven't uploaded any data submissions yet.</p>
{% endif %} {% endblock %}
9 changes: 9 additions & 0 deletions nad_ch/controllers/web/templates/reports/show.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{% extends "_layouts/base.html" %} {% block title %}Report{% endblock %} {%block
content %}
<h1>Report</h1>
{% if submission %}
<p>{{ submission.filename }}</p>
<p>{{ submission.date_created}}</p>
{% else %}
<p>No such submission exists.</p>
{% endif %} {% endblock %}
8 changes: 4 additions & 4 deletions nad_ch/domain/entities.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@
class Entity:
def __init__(self, id: int = None):
self.id = id
self.created_at = None
self.updated_at = None
self.created_at: datetime = None
self.updated_at: datetime = None

def set_created_at(self, created_at):
def set_created_at(self, created_at: datetime):
self.created_at = created_at

def set_updated_at(self, updated_at):
def set_updated_at(self, updated_at: datetime):
self.updated_at = updated_at


Expand Down
Loading

0 comments on commit a7f4cb0

Please sign in to comment.