Skip to content

Commit

Permalink
EmanifestSearch class (#715)
Browse files Browse the repository at this point in the history
* EManifest search method refactor to use dict instead of long lists of keyword args

* fix login for DRF browsable API, add dev fixture for trak user access to
Generators Organzation llc.

* minor clean up and remove old unit test

thing like removing verbose if-else for more simple implementations

* fix button bubbling event and submitting form when save button not rendered

* EmanifestSearch class - a class for searching using the e-Manifest services that
exposes a builder API

* validate emanifest search user input are values expected by e-Manifest API

* add start, end date, and correction request status to EmanifestSearch builder api

* end_date defaults to datetime.now() (with UTC tzinfo)

* emanifest search class execute requests a search from rcrainfo

* add mock_emanifest_auth_responses, test builds the search

* repalce manifest service search usage with EmanifestSearch class usage
  • Loading branch information
dpgraham4401 authored May 11, 2024
1 parent 91642bc commit 37d0428
Show file tree
Hide file tree
Showing 14 changed files with 332 additions and 146 deletions.
48 changes: 17 additions & 31 deletions client/src/components/RcraProfile/RcraProfile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export function RcraProfile({ profile }: ProfileViewProps) {
});

useEffect(() => {
// ToDo: invalidating tags should be done in the slice
dispatch(userApi.util?.invalidateTags(['rcrainfoProfile']));
}, [inProgress]);

Expand All @@ -48,6 +49,7 @@ export function RcraProfile({ profile }: ProfileViewProps) {

/** submitting the RcrainfoProfile form (RCRAInfo API ID, Key, username, etc.)*/
const onSubmit = (data: RcraProfileForm) => {
console.log('submitting form');
setProfileLoading(!profileLoading);
setEditable(!editable);
updateRcrainfoProfile({ username: profile.user, data: data });
Expand Down Expand Up @@ -107,37 +109,21 @@ export function RcraProfile({ profile }: ProfileViewProps) {
</Col>
</Row>
<Row>
<div className="mx-1 d-flex flex-row-reverse">
{!editable ? (
<>
<Button
className="mx-2"
variant="outline-info"
type="button"
onClick={() => {
setEditable(!editable);
}}
>
Edit
</Button>
</>
) : (
<>
<Button className="mx-2" variant="success" type="submit">
Save
</Button>
<Button
className="mx-2"
variant="danger"
onClick={() => {
setEditable(!editable);
reset();
}}
>
Cancel
</Button>
</>
)}
<div className="mx-1 d-flex flex-row justify-content-end">
<Button
className="mx-2"
variant={editable ? 'danger' : 'primary'}
type="button"
onClick={() => {
setEditable(!editable);
reset();
}}
>
{editable ? 'Cancel' : 'Edit'}
</Button>
<Button className="mx-2" disabled={!editable} variant="success" type="submit">
Save
</Button>
</div>
</Row>
</Container>
Expand Down
2 changes: 1 addition & 1 deletion client/src/store/htApi.slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ export const haztrakApi = createApi({
providesTags: ['code'],
}),
getOrgSites: build.query<Array<HaztrakSite>, string>({
query: (id) => ({ url: `org/${id}/site`, method: 'get' }),
query: (id) => ({ url: `org/${id}/sites`, method: 'get' }),
providesTags: ['site'],
}),
getUserHaztrakSites: build.query<Array<HaztrakSite>, void>({
Expand Down
9 changes: 9 additions & 0 deletions server/apps/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,15 @@ def mock_responses():
yield mock_responses


@pytest.fixture()
def mock_emanifest_auth_response(request, mock_responses):
api_id, api_key = request.param
mock_responses.get(
f"https://rcrainfopreprod.epa.gov/rcrainfo/rest/api/v1/auth/{api_id}/{api_key}",
body='{"token": "mocK_token", "expiration": "2021-01-01T00:00:00.000000Z"}',
)


@pytest.fixture
def mocker(mocker: pytest_mock.MockerFixture):
"""
Expand Down
21 changes: 0 additions & 21 deletions server/apps/core/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,27 +44,6 @@ def has_delete_permission(self, request, obj=None):
return False


# @admin.register(HaztrakProfile)
# class HaztrakProfileAdmin(admin.ModelAdmin):
# list_display = ["__str__", "number_of_sites", "rcrainfo_integrated_org"]
# search_fields = ["user__username"]
# inlines = [SitePermissionsInline]
# raw_id_fields = ["user", "rcrainfo_profile"]
# readonly_fields = ["rcrainfo_integrated_org"]
#
# def rcrainfo_integrated_org(self, profile: HaztrakProfile) -> bool:
# if profile.org:
# return profile.rcrainfo_integrated_org
# return False
#
# rcrainfo_integrated_org.boolean = True
#
# @staticmethod
# def number_of_sites(profile: HaztrakProfile) -> str:
# # return ", ".join([str(site) for site in profile.sit])
# return str(profile.site_permissions.all().count())


@admin.register(RcrainfoProfile)
class RcraProfileAdmin(admin.ModelAdmin):
list_display = ["__str__", "related_user", "rcra_username", "api_user"]
Expand Down
26 changes: 0 additions & 26 deletions server/apps/core/services/rcrainfo_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,32 +94,6 @@ def sign_manifest(self, **sign_data):
sign_data = {k: v for k, v in sign_data.items() if v is not None}
return super().sign_manifest(**sign_data)

def search_mtn(
self,
reg: bool = False,
site_id: Optional[str] = None,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
status: Optional[str] = None,
date_type: str = "UpdatedDate",
state_code: Optional[str] = None,
site_type: Optional[str] = None,
) -> RcrainfoResponse:
# map our python friendly keyword arguments to RCRAInfo expected fields
search_params = {
"stateCode": state_code,
"siteId": site_id,
"status": status,
"dateType": date_type,
"siteType": site_type,
"endDate": end_date,
"startDate": start_date,
}
# Remove arguments that are None
filtered_params = {k: v for k, v in search_params.items() if v is not None}
logger.debug(f"rcrainfo manifest search parameters {filtered_params}")
return super().search_mtn(**filtered_params)

def __bool__(self):
"""
This Overrides the RcrainfoClient bool
Expand Down
1 change: 0 additions & 1 deletion server/apps/handler/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ def related_manifest(self, obj: Handler):
return obj.generator.get()
if obj.designated_facility:
return obj.designated_facility.get()
# return obj.manifest.__str__() if obj.manifest else None

related_manifest.short_description = "Manifest"

Expand Down
57 changes: 13 additions & 44 deletions server/apps/manifest/services/emanifest.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import logging
from datetime import UTC, datetime, timedelta, timezone
from datetime import datetime
from typing import List, Literal, NotRequired, Optional, TypedDict

from django.db import transaction
Expand Down Expand Up @@ -28,6 +28,18 @@ class QuickerSignData(TypedDict):
transporter_order: NotRequired[int]


class SearchManifestData(TypedDict, total=False):
"""Type definition for the data required to search for manifests"""

site_id: Optional[str]
start_date: Optional[datetime]
end_date: Optional[datetime]
status: Optional[str]
date_type: Optional[str]
state_code: Optional[str]
site_type: Optional[str]


class TaskResponse(TypedDict):
"""Type definition for the response returned from starting a task"""

Expand Down Expand Up @@ -61,49 +73,6 @@ def is_available(self) -> bool:
"""Check if e-Manifest is available"""
return self.rcrainfo.has_rcrainfo_credentials

def search(
self,
*,
site_id: Optional[str] = None,
start_date: Optional[datetime] = None,
end_date: Optional[datetime] = None,
status: Optional[str] = None,
date_type: str = "UpdatedDate",
state_code: Optional[str] = None,
site_type: Optional[str] = None,
) -> List[str]:
"""Search for manifests from e-Manifest, an abstraction of RcrainfoService's search_mtn"""
date_format = "%Y-%m-%dT%H:%M:%SZ"
if end_date:
end_date_str = end_date.replace(tzinfo=timezone.utc).strftime(date_format)
else:
end_date_str = datetime.now(UTC).strftime(date_format)
if start_date:
start_date_str = start_date.replace(tzinfo=timezone.utc).strftime(date_format)
else:
# If no start date is specified, retrieve for last ~3 years
start_date = datetime.now(UTC) - timedelta(
minutes=60 # 60 seconds/1minutes
* 24 # 24 hours/1day
* 30 # 30 days/1month
* 36 # 36 months/3years = 3/years
)
start_date_str = start_date.strftime(date_format)

response = self.rcrainfo.search_mtn(
site_id=site_id,
site_type=site_type,
state_code=state_code,
start_date=start_date_str,
end_date=end_date_str,
status=status,
date_type=date_type,
)

if response.ok:
return response.json()
return []

def pull(self, tracking_numbers: List[str]) -> PullManifestsResult:
"""Retrieve manifests from e-Manifest and save to database"""
results: PullManifestsResult = {"success": [], "error": []}
Expand Down
154 changes: 154 additions & 0 deletions server/apps/manifest/services/emanifest_search.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
from datetime import UTC, datetime, timedelta, timezone
from typing import Literal, Optional, get_args

from apps.core.services import RcrainfoService

EmanifestStatus = Literal[
"Pending",
"Scheduled",
"InTransit",
"ReadyForSignature",
"Signed",
"SignedComplete",
"UnderCorrection",
"Corrected",
]

SiteType = Literal["Generator", "Tsdf", "Transporter", "RejectionInfo_AlternateTsdf"]

CorrectionRequestStatus = Literal["NotSent", "Sent", "IndustryResponded", "Cancelled"]

DateType = Literal["CertifiedDate", "ReceivedDate", "ShippedDate", "UpdatedDate"]


class EmanifestSearch:
def __init__(self, rcra_client: Optional[RcrainfoService] = None):
self._rcra_client = rcra_client or RcrainfoService()
self.state_code: Optional[str] = None
self.site_id: Optional[str] = None
self.status: Optional[EmanifestStatus] = None
self.site_type: Optional[SiteType] = None
self.date_type: Optional[DateType] = None
self.start_date: Optional[datetime] = None
self.end_date: Optional[datetime] = None
self.correction_request_status: Optional[CorrectionRequestStatus] = None

@property
def rcra_client(self):
if not self._rcra_client:
self._rcra_client = RcrainfoService()
return self._rcra_client

@rcra_client.setter
def rcra_client(self, value):
self._rcra_client = value

@staticmethod
def __validate_literal(value, literal) -> bool:
return value in get_args(literal)

@staticmethod
def _valid_state_code(state_code) -> bool:
return len(state_code) == 2 and state_code.isalpha()

@staticmethod
def _valid_site_id(site_id: str) -> bool:
return len(site_id) > 2 and site_id.isalnum()

@classmethod
def _emanifest_status(cls, status) -> bool:
return cls.__validate_literal(status, EmanifestStatus)

@classmethod
def _emanifest_site_type(cls, site_type) -> bool:
return cls.__validate_literal(site_type, SiteType)

@classmethod
def _emanifest_date_type(cls, date_type) -> bool:
return cls.__validate_literal(date_type, DateType)

@classmethod
def _emanifest_correction_request_status(cls, correction_request_status) -> bool:
return cls.__validate_literal(correction_request_status, CorrectionRequestStatus)

def _date_or_three_years_past(self, start_date: Optional[datetime]) -> str:
return (
start_date.replace(tzinfo=timezone.utc).strftime(self.rcra_client.datetime_format)
if start_date
else (
datetime.now(UTC)
- timedelta(
minutes=60 # 60 seconds/1minutes
* 24 # 24 hours/1day
* 30 # 30 days/1month
* 36 # 36 months/3years = 3/years
)
).strftime(self.rcra_client.datetime_format)
)

def _date_or_now(self, end_date: Optional[datetime]) -> str:
return (
end_date.replace(tzinfo=timezone.utc).strftime(self.rcra_client.datetime_format)
if end_date
else datetime.now(UTC).strftime(self.rcra_client.datetime_format)
)

def build_search_args(self):
search_params = {
"stateCode": self.state_code,
"siteId": self.site_id,
"status": self.status,
"dateType": self.date_type,
"siteType": self.site_type,
"endDate": self.end_date,
"startDate": self.start_date,
}
return {k: v for k, v in search_params.items() if v is not None}

def add_state_code(self, state: str):
if not self._valid_state_code(state):
raise ValueError("Invalid State code")
self.state_code = state
return self

def add_site_id(self, site_id: str):
if not self._valid_site_id(site_id):
raise ValueError("Invalid Site ID")
self.site_id = site_id
return self

def add_status(self, status: EmanifestStatus):
if not self._emanifest_status(status):
raise ValueError("Invalid Status")
self.status = status
return self

def add_site_type(self, site_type: SiteType):
if not self._emanifest_site_type(site_type):
raise ValueError("Invalid Site Type")
self.site_type = site_type
return self

def add_date_type(self, date_type: DateType):
if not self._emanifest_date_type(date_type):
raise ValueError("Invalid Date Type")
self.date_type = date_type
return self

def add_correction_request_status(self, correction_request_status: CorrectionRequestStatus):
if not self._emanifest_correction_request_status(correction_request_status):
raise ValueError("Invalid Correction Request Status")
self.correction_request_status = correction_request_status
return self

def add_start_date(self, start_date: datetime = None):
self.start_date = self._date_or_three_years_past(start_date)
return self

def add_end_date(self, end_date: datetime = None):
self.end_date = self._date_or_now(end_date)
return self

def execute(self):
search_args = self.build_search_args()
return self._rcra_client.search_mtn(**search_args)
Loading

0 comments on commit 37d0428

Please sign in to comment.