diff --git a/client/package-lock.json b/client/package-lock.json index 9c631e996..05357294f 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -27,7 +27,7 @@ "react-router-dom": "^6.16.0", "react-select": "^5.7.7", "sass": "^1.68.0", - "zod": "^3.22.2" + "zod": "^3.22.3" }, "devDependencies": { "@testing-library/jest-dom": "^6.1.3", @@ -11362,9 +11362,9 @@ } }, "node_modules/zod": { - "version": "3.22.2", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.2.tgz", - "integrity": "sha512-wvWkphh5WQsJbVk1tbx1l1Ly4yg+XecD+Mq280uBGt9wa5BKSWf4Mhp6GmrkPixhMxmabYY7RbzlwVP32pbGCg==", + "version": "3.22.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.3.tgz", + "integrity": "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==", "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/client/package.json b/client/package.json index 42c23ef8a..c284c6036 100644 --- a/client/package.json +++ b/client/package.json @@ -33,7 +33,7 @@ "react-router-dom": "^6.16.0", "react-select": "^5.7.7", "sass": "^1.68.0", - "zod": "^3.22.2" + "zod": "^3.22.3" }, "devDependencies": { "@testing-library/jest-dom": "^6.1.3", diff --git a/client/src/components/Manifest/ManifestForm.tsx b/client/src/components/Manifest/ManifestForm.tsx index a5b559e36..e57e26116 100644 --- a/client/src/components/Manifest/ManifestForm.tsx +++ b/client/src/components/Manifest/ManifestForm.tsx @@ -96,7 +96,7 @@ export function ManifestForm({ const onSubmit: SubmitHandler = (data: Manifest) => { console.log('Manifest Submitted', data); htApi - .post('/rcra/manifest/create', data) + .post('/rcra/manifest', data) .then((response) => { return response; }) diff --git a/client/src/components/Manifest/QuickerSign/QuickerSignForm.tsx b/client/src/components/Manifest/QuickerSign/QuickerSignForm.tsx index 2fac01ac0..a7650f01f 100644 --- a/client/src/components/Manifest/QuickerSign/QuickerSignForm.tsx +++ b/client/src/components/Manifest/QuickerSign/QuickerSignForm.tsx @@ -59,7 +59,7 @@ export function QuickerSignForm({ mtn, mtnHandler, handleClose, siteType }: Quic }; } htApi - .post('/manifest/sign', signature) + .post('rcra/manifest/sign', signature) .then((response: AxiosResponse) => { dispatch( addNotification({ diff --git a/client/src/components/buttons/SyncManifestBtn/SyncManifestBtn.spec.tsx b/client/src/components/buttons/SyncManifestBtn/SyncManifestBtn.spec.tsx index 47e40126e..1b0f69e3f 100644 --- a/client/src/components/buttons/SyncManifestBtn/SyncManifestBtn.spec.tsx +++ b/client/src/components/buttons/SyncManifestBtn/SyncManifestBtn.spec.tsx @@ -11,7 +11,7 @@ const testTaskID = 'testTaskId'; const server = setupServer( ...[ - rest.post(`${API_BASE_URL}/manifest/sync`, (req, res, ctx) => { + rest.post(`${API_BASE_URL}rcra/manifest/sync`, (req, res, ctx) => { // Mock Sync Site Manifests response return res( ctx.status(200), diff --git a/client/src/components/buttons/SyncManifestBtn/SyncManifestBtn.tsx b/client/src/components/buttons/SyncManifestBtn/SyncManifestBtn.tsx index 4e8900269..27b5965bc 100644 --- a/client/src/components/buttons/SyncManifestBtn/SyncManifestBtn.tsx +++ b/client/src/components/buttons/SyncManifestBtn/SyncManifestBtn.tsx @@ -1,8 +1,8 @@ import { faSync } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { RcraApiUserBtn } from 'components/buttons'; import React, { useState } from 'react'; import { htApi } from 'services'; -import { RcraApiUserBtn } from 'components/buttons'; import { addNotification, useAppDispatch } from 'store'; interface SyncManifestProps { @@ -18,8 +18,7 @@ interface SyncManifestProps { export function SyncManifestBtn({ siteId, disabled }: SyncManifestProps) { const [syncingMtn, setSyncingMtn] = useState(false); const dispatch = useAppDispatch(); - let active: boolean = siteId ? true : false; - active = !disabled; + const active = !disabled; return ( { setSyncingMtn(!syncingMtn); htApi - .post('/manifest/sync', { siteId: `${siteId}` }) + .post('rcra/manifest/sync', { siteId: `${siteId}` }) .then((response) => { dispatch( addNotification({ diff --git a/client/src/features/home/Home.spec.tsx b/client/src/features/home/Home.spec.tsx index 73fcbc518..964b6c372 100644 --- a/client/src/features/home/Home.spec.tsx +++ b/client/src/features/home/Home.spec.tsx @@ -1,16 +1,16 @@ import '@testing-library/jest-dom'; -import { Home } from './Home'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import React from 'react'; import { cleanup, renderWithProviders, screen } from 'test-utils'; import { API_BASE_URL, handlers } from 'test-utils/mock/handlers'; import { afterAll, afterEach, beforeAll, describe, expect, test, vi } from 'vitest'; +import { Home } from './Home'; const USERNAME = 'testuser1'; const myAPIHandlers = [ - rest.get(`${API_BASE_URL}/api/user/${USERNAME}/rcra/profile`, (req, res, ctx) => { + rest.get(`${API_BASE_URL}/api/rcra/profile/${USERNAME}`, (req, res, ctx) => { return res( // Respond with a 200 status code ctx.status(200), diff --git a/client/src/features/manifestDetails/ManifestDetails.tsx b/client/src/features/manifestDetails/ManifestDetails.tsx index ddfdd178d..4b8ade587 100644 --- a/client/src/features/manifestDetails/ManifestDetails.tsx +++ b/client/src/features/manifestDetails/ManifestDetails.tsx @@ -8,7 +8,7 @@ import { useParams } from 'react-router-dom'; export function ManifestDetails() { const { mtn, action, siteId } = useParams(); useTitle(`${mtn}`); - const [manifestData, loading, error] = useHtApi(`manifest/${mtn}`); + const [manifestData, loading, error] = useHtApi(`rcra/manifest/${mtn}`); let readOnly = true; if (action === 'edit') { diff --git a/client/src/features/manifestList/ManifestList.tsx b/client/src/features/manifestList/ManifestList.tsx index 3ee5e4e08..68bda4c17 100644 --- a/client/src/features/manifestList/ManifestList.tsx +++ b/client/src/features/manifestList/ManifestList.tsx @@ -16,9 +16,9 @@ export function ManifestList(): ReactElement { useTitle(`${siteId || ''} Manifest`); const [pageSize, setPageSize] = useState(10); - let getUrl = 'mtn'; + let getUrl = 'rcra/mtn'; if (siteId) { - getUrl = `mtn/${siteId}`; + getUrl = `rcra/mtn/${siteId}`; } const [mtnList, loading, error] = useHtApi>(getUrl); diff --git a/client/src/features/notifications/Notifications.tsx b/client/src/features/notifications/Notifications.tsx index aa212ecf6..9c13a7a7e 100644 --- a/client/src/features/notifications/Notifications.tsx +++ b/client/src/features/notifications/Notifications.tsx @@ -1,5 +1,4 @@ -import { faFaceSmile } from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import winkingRobot from '/static/robot-wink.jpg'; import { HtCard } from 'components/Ht'; import { NotificationRow } from 'components/Notification'; import { useTitle } from 'hooks'; @@ -25,8 +24,8 @@ export function Notifications() { {notifications.length === 0 ? (
-

Nothing to see here, you're all caught up!

- +

You're all caught up!

+ happy robot
) : ( diff --git a/client/src/features/profile/RcraProfile.tsx b/client/src/features/profile/RcraProfile.tsx index c2900c5ef..f7ec1cac5 100644 --- a/client/src/features/profile/RcraProfile.tsx +++ b/client/src/features/profile/RcraProfile.tsx @@ -4,11 +4,11 @@ import { HtForm } from 'components/Ht'; import React, { useState } from 'react'; import { Button, Col, Container, Form, Row, Table } from 'react-bootstrap'; import { useForm } from 'react-hook-form'; +import { Link } from 'react-router-dom'; +import { htApi } from 'services'; import { addNotification, useAppDispatch } from 'store'; import { getProfile, updateProfile } from 'store/rcraProfileSlice'; import { RcraProfileState } from 'store/rcraProfileSlice/rcraProfile.slice'; -import { htApi } from 'services'; -import { Link } from 'react-router-dom'; import { z } from 'zod'; interface ProfileViewProps { @@ -48,7 +48,7 @@ export function RcraProfile({ profile }: ProfileViewProps) { setProfileLoading(!profileLoading); setEditable(!editable); htApi - .put(`/user/${profile.user}/rcra/profile`, data) + .put(`/rcra/profile/${profile.user}`, data) .then((r) => { dispatch(updateProfile(r.data)); }) diff --git a/client/src/store/rcraProfileSlice/rcraProfile.slice.ts b/client/src/store/rcraProfileSlice/rcraProfile.slice.ts index a8df7ac49..b2060c928 100644 --- a/client/src/store/rcraProfileSlice/rcraProfile.slice.ts +++ b/client/src/store/rcraProfileSlice/rcraProfile.slice.ts @@ -96,7 +96,7 @@ export const getProfile = createAsyncThunk( async (arg, thunkAPI) => { const state = thunkAPI.getState() as RootState; const username = state.user.user?.username; - const response = await htApi.get(`/user/${username}/rcra/profile`); + const response = await htApi.get(`/rcra/profile/${username}`); const { rcraSites, ...rest } = response.data as RcraProfileResponse; // Convert the array of RcraSite permissions we get from our backend // to an object which each key corresponding to the RcraSite's ID number diff --git a/client/src/store/site.slice.ts b/client/src/store/site.slice.ts index b247ed104..1d14ea8de 100644 --- a/client/src/store/site.slice.ts +++ b/client/src/store/site.slice.ts @@ -14,24 +14,22 @@ interface RcrainfoSiteSearch { export const siteApi = createApi({ reducerPath: 'siteApi', baseQuery: htApiBaseQuery({ - baseUrl: `${import.meta.env.VITE_HT_API_URL}/api/site`, + baseUrl: `${import.meta.env.VITE_HT_API_URL}/api/`, }), endpoints: (build) => ({ searchRcrainfoSites: build.query, RcrainfoSiteSearch>({ query: (data: RcrainfoSiteSearch) => ({ - url: '/rcrainfo/search', + url: 'rcra/handler/search', method: 'post', data: data, }), }), searchRcraSites: build.query, RcrainfoSiteSearch>({ query: (data: RcrainfoSiteSearch) => ({ - url: '/search', + url: 'site/search', method: 'get', params: { epaId: data.siteId, siteType: data.siteType }, }), }), }), }); - -export const { useSearchRcraSitesQuery, useSearchRcrainfoSitesQuery } = siteApi; diff --git a/client/src/store/wasteCode.slice.ts b/client/src/store/wasteCode.slice.ts index 697e2d570..4081b7e8e 100644 --- a/client/src/store/wasteCode.slice.ts +++ b/client/src/store/wasteCode.slice.ts @@ -9,7 +9,7 @@ import { htApiBaseQuery } from 'store/baseQuery'; export const wasteCodeApi = createApi({ reducerPath: 'wasteCodeApi', baseQuery: htApiBaseQuery({ - baseUrl: `${import.meta.env.VITE_HT_API_URL}/api/code/`, + baseUrl: `${import.meta.env.VITE_HT_API_URL}/api/rcra/code/`, }), endpoints: (build) => ({ getFedWasteCodes: build.query, void>({ diff --git a/client/src/test-utils/mock/handlers.ts b/client/src/test-utils/mock/handlers.ts index 40b13db66..50ed760fa 100644 --- a/client/src/test-utils/mock/handlers.ts +++ b/client/src/test-utils/mock/handlers.ts @@ -28,7 +28,7 @@ export const handlers = [ /** * User RcraProfile data */ - rest.get(`${API_BASE_URL}/api/user/${mockUsername}/rcra/profile`, (req, res, ctx) => { + rest.get(`${API_BASE_URL}/api/rcra/profile/${mockUsername}`, (req, res, ctx) => { return res( // Respond with a 200 status code ctx.status(200), @@ -57,13 +57,13 @@ export const handlers = [ /** * mock Manifest */ - rest.get(`${API_BASE_URL}/api/manifest/${mockMTN}`, (req, res, ctx) => { + rest.get(`${API_BASE_URL}/api/rcra/manifest/${mockMTN}`, (req, res, ctx) => { return res(ctx.status(200), ctx.json(createMockManifest())); }), /** * list of manifests ('My Manifests' feature and a site's manifests) */ - rest.get(`${API_BASE_URL}/api/mtn*`, (req, res, ctx) => { + rest.get(`${API_BASE_URL}/api/rcra/mtn*`, (req, res, ctx) => { const mockManifestArray = [ createMockManifest(), createMockManifest({ manifestTrackingNumber: '987654321ELC', status: 'Pending' }), diff --git a/server/apps/core/urls.py b/server/apps/core/urls.py index 3153fa0c4..dd6019fa1 100644 --- a/server/apps/core/urls.py +++ b/server/apps/core/urls.py @@ -1,6 +1,6 @@ -from django.urls import path +from django.urls import include, path -from .views import ( +from .views import ( # type: ignore HaztrakUserView, LaunchExampleTaskView, Login, @@ -11,8 +11,15 @@ urlpatterns = [ # Rcra Profile - path("user//rcra/profile/sync", RcraProfileSyncView.as_view()), - path("user//rcra/profile", RcraProfileView.as_view()), + path( + "rcra/", + include( + [ + path("profile//sync", RcraProfileSyncView.as_view()), + path("profile/", RcraProfileView.as_view()), + ] + ), + ), path("user", HaztrakUserView.as_view()), path("user/login", Login.as_view()), path("task/example", LaunchExampleTaskView.as_view()), diff --git a/server/apps/sites/tests/test_epa_profile_views.py b/server/apps/sites/tests/test_epa_profile_views.py index 6a22149fa..ce29ab6ca 100644 --- a/server/apps/sites/tests/test_epa_profile_views.py +++ b/server/apps/sites/tests/test_epa_profile_views.py @@ -1,9 +1,8 @@ import pytest from rest_framework import status -from rest_framework.response import Response from rest_framework.test import APIRequestFactory, force_authenticate -from apps.core.views import RcraProfileView +from apps.core.views import RcraProfileView # type: ignore class TestRcraProfileView: @@ -11,7 +10,7 @@ class TestRcraProfileView: Tests the for the endpoints related to the user's RcraProfile """ - URL = "/api/user" + URL = "/api/" id_field = "rcraAPIID" key_field = "rcraAPIKey" username_field = "rcraUsername" @@ -28,7 +27,7 @@ def user_and_client(self, rcra_profile_factory, user_factory, api_client_factory def rcra_profile_request(self, user_and_client): factory = APIRequestFactory() request = factory.put( - f"{self.URL}/{self.user.username}/rcra/profile", + f"{self.URL}rcra/profile{self.user.username}", { self.id_field: self.new_api_id, self.username_field: self.new_username, @@ -43,7 +42,7 @@ def test_returns_a_user_profile(self, user_and_client, rcra_profile_factory): # Arrange rcra_profile_factory(user=self.user) # Act - response: Response = self.client.get(f"{self.URL}/{self.user.username}/rcra/profile") + response = self.client.get(f"{self.URL}rcra/profile/{self.user.username}") # Assert assert response.headers["Content-Type"] == "application/json" assert response.status_code == status.HTTP_200_OK diff --git a/server/apps/sites/tests/test_epa_site_views.py b/server/apps/sites/tests/test_epa_site_views.py index 0dcfb9b76..0fdeabc68 100644 --- a/server/apps/sites/tests/test_epa_site_views.py +++ b/server/apps/sites/tests/test_epa_site_views.py @@ -3,8 +3,8 @@ from rest_framework.response import Response from rest_framework.test import APIClient, APIRequestFactory, force_authenticate -from apps.sites.models import RcraSiteType -from apps.sites.views import SiteSearchView +from apps.sites.models import RcraSiteType # type: ignore +from apps.sites.views import SiteSearchView # type: ignore class TestEpaSiteView: @@ -12,7 +12,7 @@ class TestEpaSiteView: Tests the for the endpoints related to the handlers """ - URL = "/api/site/rcra-site" + URL = "/api/rcra/handler" @pytest.fixture def client(self, rcra_site_factory, api_client_factory): diff --git a/server/apps/sites/tests/test_site_views.py b/server/apps/sites/tests/test_site_views.py index ae03ad549..ac67c38d4 100644 --- a/server/apps/sites/tests/test_site_views.py +++ b/server/apps/sites/tests/test_site_views.py @@ -3,12 +3,11 @@ import pytest from django.contrib.auth.models import User from rest_framework import status -from rest_framework.response import Response from rest_framework.test import APIClient, APIRequestFactory, force_authenticate -from apps.core.models import RcraProfile -from apps.sites.models import RcraSite, RcraSitePermission, Site -from apps.sites.views import SiteDetailView, SiteMtnListView +from apps.core.models import RcraProfile # type: ignore +from apps.sites.models import RcraSite, RcraSitePermission, Site # type: ignore +from apps.sites.views import SiteDetailView # type: ignore class TestSiteListView: @@ -96,7 +95,7 @@ def create_site_and_related( return create_site_and_related - def test_returns_site_by_id(self, user_factory, local_site_factory): + def test_returns_site_by_id(self, user_factory, local_site_factory) -> None: # Arrange user = user_factory(username="username1") site = local_site_factory(user=user) @@ -132,28 +131,3 @@ def test_returns_formatted_http_response(self, user_factory, local_site_factory, # Assert assert response.headers["Content-Type"] == "application/json" assert response.status_code == status.HTTP_200_OK - - -class TestSiteManifest: - """ - Tests for the endpoint to retrieve a Site's manifests - """ - - # ToDo fix these false positive tests - - url = "/api/site" - - @pytest.fixture(autouse=True) - def _user(self, user_factory): - self.user = user_factory() - - @pytest.fixture(autouse=True) - def _generator(self, rcra_site_factory): - self.generator = rcra_site_factory() - - def test_returns_200(self): - factory = APIRequestFactory() - request = factory.get(f"{self.url}/{self.generator.epa_id}/manifest") - force_authenticate(request, self.user) - response: Response = SiteMtnListView.as_view()(request, self.generator.epa_id) - print(response.data) diff --git a/server/apps/sites/urls.py b/server/apps/sites/urls.py index 4bd860c61..572b790e1 100644 --- a/server/apps/sites/urls.py +++ b/server/apps/sites/urls.py @@ -1,20 +1,25 @@ -from django.urls import path +from django.urls import include, path -from apps.sites.views import ( +from apps.sites.views import ( # type: ignore + HandlerSearchView, RcraSiteView, SiteDetailView, SiteListView, - SiteMtnListView, SiteSearchView, - rcrainfo_site_search_view, ) urlpatterns = [ + path( + "rcra/", + include( + [ + path("handler/search", HandlerSearchView.as_view()), + path("handler/", RcraSiteView.as_view()), + ] + ), + ), # Site path("site", SiteListView.as_view()), path("site/search", SiteSearchView.as_view()), path("site/", SiteDetailView.as_view()), - path("site//manifest", SiteMtnListView.as_view()), - path("site/rcra-site/", RcraSiteView.as_view()), - path("site/rcrainfo/search", rcrainfo_site_search_view), ] diff --git a/server/apps/sites/views/__init__.py b/server/apps/sites/views/__init__.py index 114b635c8..8db3500e3 100644 --- a/server/apps/sites/views/__init__.py +++ b/server/apps/sites/views/__init__.py @@ -1,8 +1,7 @@ -from .site_views import ( +from .site_views import ( # type: ignore + HandlerSearchView, RcraSiteView, SiteDetailView, SiteListView, - SiteMtnListView, SiteSearchView, - rcrainfo_site_search_view, ) diff --git a/server/apps/sites/views/site_views.py b/server/apps/sites/views/site_views.py index 1e64525c6..1450e08fd 100644 --- a/server/apps/sites/views/site_views.py +++ b/server/apps/sites/views/site_views.py @@ -1,21 +1,19 @@ import logging -from django.core.exceptions import ObjectDoesNotExist, PermissionDenied +from django.db.models import QuerySet from django.utils.decorators import method_decorator from django.views.decorators.cache import cache_page -from drf_spectacular.utils import extend_schema -from rest_framework import permissions, status -from rest_framework.decorators import api_view -from rest_framework.exceptions import APIException, ValidationError -from rest_framework.generics import GenericAPIView, ListAPIView, RetrieveAPIView +from drf_spectacular.utils import extend_schema, inline_serializer +from rest_framework import permissions, serializers, status +from rest_framework.exceptions import ValidationError +from rest_framework.generics import ListAPIView, RetrieveAPIView from rest_framework.request import Request from rest_framework.response import Response +from rest_framework.views import APIView -from apps.sites.models import RcraSite, RcraSiteType, Site -from apps.sites.serializers import RcraSiteSerializer, SiteSerializer -from apps.sites.services import RcraSiteService -from apps.trak.models import Manifest -from apps.trak.serializers import MtnSerializer +from apps.sites.models import RcraSite, RcraSiteType, Site # type: ignore +from apps.sites.serializers import RcraSiteSerializer, SiteSerializer # type: ignore +from apps.sites.services import RcraSiteService # type: ignore logger = logging.getLogger(__name__) @@ -52,50 +50,6 @@ def get_queryset(self): return queryset -class SiteMtnListView(GenericAPIView): - """ - Returns a site's manifest tracking numbers (MTN). Rhe MTN are broken down into three lists; - generator, transporter, designated.Each array contains a list of objects with MTN and select - details such as status - """ - - response = Response - serializer_class = MtnSerializer - - def get(self, request: Request, epa_id: str = None) -> Response: - """GET method rcra_site""" - try: - profile_sites = [ - str(i) for i in Site.objects.filter(rcrasitepermission__profile__user=request.user) - ] - if epa_id not in profile_sites: - raise PermissionDenied - tsdf_manifests = Manifest.objects.filter(tsdf__rcra_site__epa_id=epa_id).values( - "mtn", "status" - ) - gen_manifests = Manifest.objects.filter(generator__rcra_site__epa_id=epa_id).values( - "mtn", "status" - ) - tran_manifests = Manifest.objects.filter( - transporters__rcra_site__epa_id__contains=epa_id - ).values("mtn", "status") - return self.response( - status=status.HTTP_200_OK, - data={ - "tsdf": tsdf_manifests, - "generator": gen_manifests, - "transporter": tran_manifests, - }, - ) - except (APIException, AttributeError) as error: - logger.warning(error) - return self.response(status=status.HTTP_500_INTERNAL_SERVER_ERROR) - except ObjectDoesNotExist: - return self.response( - status=status.HTTP_404_NOT_FOUND, data={"Error": f"{epa_id} not found"} - ) - - @extend_schema( description="Retrieve details on a rcra_site stored in the Haztrak database", ) @@ -109,6 +63,17 @@ class RcraSiteView(RetrieveAPIView): permission_classes = [permissions.IsAuthenticated] +@extend_schema( + responses=RcraSiteSerializer(many=True), + request=inline_serializer( + "handler_search", + fields={ + "epaId": serializers.CharField(), + "siteName": serializers.CharField(), + "siteType": serializers.CharField(), + }, + ), +) class SiteSearchView(ListAPIView): """ Search for locally saved hazardous waste sites ("Generators", "Transporters", "Tsdf's") @@ -117,11 +82,11 @@ class SiteSearchView(ListAPIView): queryset = RcraSite.objects.all() serializer_class = RcraSiteSerializer - def get_queryset(self): + def get_queryset(self: ListAPIView) -> QuerySet[RcraSite]: queryset = RcraSite.objects.all() - epa_id_param = self.request.query_params.get("epaId") - name_param = self.request.query_params.get("siteName") - site_type_param: str = self.request.query_params.get("siteType") + epa_id_param: str | None = self.request.query_params.get("epaId") + name_param: str | None = self.request.query_params.get("siteName") + site_type_param: str | None = self.request.query_params.get("siteType") if epa_id_param is not None: queryset = queryset.filter(epa_id__icontains=epa_id_param) if name_param is not None: @@ -149,18 +114,26 @@ def get_queryset(self): } -@api_view(["POST"]) -def rcrainfo_site_search_view(request: Request): +@extend_schema( + responses=RcraSiteSerializer(many=True), + request=inline_serializer( + "handler_search", + fields={"siteId": serializers.CharField(), "siteType": serializers.CharField()}, + ), +) +class HandlerSearchView(APIView): """ - Search and return a list of sites from RCRAInfo. + Search and return a list of Hazardous waste handlers from RCRAInfo. """ - try: - site_service = RcraSiteService(username=request.user.username) - data = site_service.search_rcra_site( - epaSiteId=request.data["siteId"], siteType=handler_types[request.data["siteType"]] - ) - return Response(status=status.HTTP_200_OK, data=data["sites"]) - except KeyError: - return Response(status=status.HTTP_400_BAD_REQUEST) - except ValidationError: - return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + def post(self, request: Request) -> Response: + try: + site_service = RcraSiteService(username=request.user.username) + data = site_service.search_rcra_site( + epaSiteId=request.data["siteId"], siteType=handler_types[request.data["siteType"]] + ) + return Response(status=status.HTTP_200_OK, data=data["sites"]) + except KeyError: + return Response(status=status.HTTP_400_BAD_REQUEST) + except ValidationError: + return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR) diff --git a/server/apps/trak/tests/views/test_manifest_views.py b/server/apps/trak/tests/views/test_manifest_views.py index 134248bd9..4c30c6756 100644 --- a/server/apps/trak/tests/views/test_manifest_views.py +++ b/server/apps/trak/tests/views/test_manifest_views.py @@ -1,7 +1,6 @@ import pytest from celery.result import AsyncResult from rest_framework import status -from rest_framework.response import Response from rest_framework.test import APIRequestFactory, force_authenticate from apps.trak.views import ManifestView, SignManifestView @@ -10,7 +9,7 @@ class TestManifestCRUD: """Tests the for the Manifest ModelViewSet""" - base_url = "/api/manifest" + base_url = "/api/rcra/manifest" @pytest.fixture def factory(self): @@ -33,22 +32,11 @@ def test_returns_manifest_by_mtn(self, factory, manifest, manifest_json, user): request = factory.get(f"{self.base_url}/{manifest.mtn}") force_authenticate(request, user) # Act - response: Response = ManifestView.as_view({"get": "retrieve"})(request, mtn=manifest.mtn) + response = ManifestView.as_view()(request, mtn=manifest.mtn) # Assert assert response.status_code == status.HTTP_200_OK assert response.data["manifestTrackingNumber"] == manifest.mtn - def test_manifest_from_epa_create_when_posted(self, factory, manifest_json, user): - request = factory.post(f"{self.base_url}", manifest_json, format="json") - force_authenticate(request, user) - - response: Response = ManifestView.as_view({"post": "create"})(request) - - assert response.status_code == status.HTTP_201_CREATED - assert response.data["manifestTrackingNumber"] == manifest_json.get( - "manifestTrackingNumber" - ) - class TestSignManifestVIew: """Quicker Sign endpoint test suite""" @@ -79,5 +67,5 @@ def test_returns_celery_task_id(self): format="json", ) force_authenticate(request, self.user) - response: Response = SignManifestView.as_view()(request) + response = SignManifestView.as_view()(request) assert response.data["task"] == self.mock_task_id diff --git a/server/apps/trak/urls.py b/server/apps/trak/urls.py index 42e34ffc2..e8368996e 100644 --- a/server/apps/trak/urls.py +++ b/server/apps/trak/urls.py @@ -1,31 +1,32 @@ from django.urls import include, path -from rest_framework import routers -from apps.trak.views import ( +from apps.trak.views import ( # type: ignore CreateRcraManifestView, FederalWasteCodesView, ManifestView, MtnList, - PullManifestView, SignManifestView, StateWasteCodesView, SyncSiteManifestView, ) -manifest_router = routers.SimpleRouter(trailing_slash=False) -manifest_router.register(r"manifest", ManifestView) - urlpatterns = [ - # Manifest - path("", include(manifest_router.urls)), - path("rcra/manifest/create", CreateRcraManifestView.as_view()), - path("manifest/pull", PullManifestView.as_view()), - path("manifest/sign", SignManifestView.as_view()), - path("manifest/sync", SyncSiteManifestView.as_view()), - # MTN - path("mtn", MtnList.as_view()), - path("mtn/", MtnList.as_view()), - # Codes - path("code/waste/federal", FederalWasteCodesView.as_view()), - path("code/waste/state/", StateWasteCodesView.as_view()), + path( + "rcra/", + include( + [ + # Manifest + path("manifest", CreateRcraManifestView.as_view()), + path("manifest/", ManifestView.as_view()), + path("manifest/sign", SignManifestView.as_view()), + path("manifest/sync", SyncSiteManifestView.as_view()), + # MTN + path("mtn", MtnList.as_view()), + path("mtn/", MtnList.as_view()), + # Codes + path("code/waste/federal", FederalWasteCodesView.as_view()), + path("code/waste/state/", StateWasteCodesView.as_view()), + ] + ), + ), ] diff --git a/server/apps/trak/views/__init__.py b/server/apps/trak/views/__init__.py index e073299a3..c253fd99a 100644 --- a/server/apps/trak/views/__init__.py +++ b/server/apps/trak/views/__init__.py @@ -1,9 +1,8 @@ -from .lookup_views import FederalWasteCodesView, StateWasteCodesView -from .manifest_view import ( +from .lookup_views import FederalWasteCodesView, StateWasteCodesView # type: ignore +from .manifest_view import ( # type: ignore CreateRcraManifestView, ManifestView, MtnList, - PullManifestView, SignManifestView, SyncSiteManifestView, ) diff --git a/server/apps/trak/views/manifest_view.py b/server/apps/trak/views/manifest_view.py index 7c91bd00e..dc5ff887c 100644 --- a/server/apps/trak/views/manifest_view.py +++ b/server/apps/trak/views/manifest_view.py @@ -1,21 +1,25 @@ -import datetime import logging from celery.exceptions import TaskError from celery.result import AsyncResult from django.db.models import Q -from drf_spectacular.utils import extend_schema -from rest_framework import status, viewsets -from rest_framework.generics import GenericAPIView, ListAPIView +from drf_spectacular.utils import extend_schema, inline_serializer +from rest_framework import serializers, status +from rest_framework.generics import GenericAPIView, ListAPIView, RetrieveAPIView from rest_framework.request import Request from rest_framework.response import Response -from apps.core.services import TaskService -from apps.sites.models import Site -from apps.trak.models import Manifest -from apps.trak.serializers import ManifestSerializer, MtnSerializer -from apps.trak.serializers.signature_ser import QuickerSignSerializer -from apps.trak.tasks import create_rcra_manifest, pull_manifest, sign_manifest, sync_site_manifests +from apps.core.services import TaskService # type: ignore +from apps.sites.models import Site # type: ignore +from apps.trak.models import Manifest # type: ignore +from apps.trak.serializers import ManifestSerializer, MtnSerializer # type: ignore +from apps.trak.serializers.signature_ser import QuickerSignSerializer # type: ignore +from apps.trak.tasks import ( # type: ignore + create_rcra_manifest, + pull_manifest, + sign_manifest, + sync_site_manifests, +) logger = logging.getLogger(__name__) @@ -23,7 +27,7 @@ @extend_schema( responses={201: ManifestSerializer}, ) -class ManifestView(viewsets.ModelViewSet): +class ManifestView(RetrieveAPIView): """ The Uniform hazardous waste manifest by the manifest tracking number (MTN) """ @@ -33,26 +37,6 @@ class ManifestView(viewsets.ModelViewSet): serializer_class = ManifestSerializer -class PullManifestView(GenericAPIView): - """ - This endpoint launches a task to pull a manifest (by MTN) from RCRAInfo. - On success, returns the task queue ID. - """ - - queryset = None - response = Response - - def post(self, request: Request) -> Response: - try: - mtn = request.data["mtn"] - task = pull_manifest.delay(mtn=mtn, username=str(request.user)) - return self.response(data={"task": task.id}, status=status.HTTP_200_OK) - except KeyError: - return self.response( - data={"error": "malformed payload"}, status=status.HTTP_400_BAD_REQUEST - ) - - class MtnList(ListAPIView): """ MtnList returns select details on a user's manifest, @@ -83,7 +67,6 @@ class SignManifestView(GenericAPIView): serializer_class = QuickerSignSerializer queryset = None - response = Response def post(self, request: Request) -> Response: """ @@ -94,7 +77,6 @@ def post(self, request: Request) -> Response: quicker_serializer = self.serializer_class(data=request.data) if quicker_serializer.is_valid(): quicker_serializer.save() - # Use (json serializable) keyword args when handing off to celery task = sign_manifest.delay( username=str(request.user), **quicker_serializer.validated_data ) @@ -104,38 +86,45 @@ def post(self, request: Request) -> Response: return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR, data=exc) +@extend_schema( + request=inline_serializer( + "site_manifest_sync_request", fields={"siteId": serializers.CharField()} + ), + responses=inline_serializer( + "site_manifest_sync_response", fields={"task": serializers.CharField()} + ), +) class SyncSiteManifestView(GenericAPIView): """ - This endpoint launches a task to pull a site's manifests that are out of sync with RCRAInfo + Pull a site's manifests that are out of sync with RCRAInfo. + It returns the task id of the long-running background task which can be used to poll + for status. """ queryset = None - response = Response def post(self, request: Request) -> Response: - """POST method rcra_site""" try: site_id = request.data["siteId"] task = sync_site_manifests.delay(site_id=site_id, username=str(request.user)) - return self.response(data={"task": task.id}, status=status.HTTP_200_OK) + return Response(data={"task": task.id}, status=status.HTTP_200_OK) except KeyError: - return self.response( + return Response( data={"error": "malformed payload"}, status=status.HTTP_400_BAD_REQUEST ) +@extend_schema(request=ManifestSerializer) class CreateRcraManifestView(GenericAPIView): """ - This is a proxy endpoint used to create electronic manifest(s) in RCRAInfo/e-Manifest + A proxy endpoint used to create electronic manifest(s) in RCRAInfo/e-Manifest """ queryset = None - response = Response serializer_class = ManifestSerializer http_method_names = ["post"] def post(self, request: Request) -> Response: - """The Body of the POST request should contain the complete and valid manifest object""" manifest_serializer = self.serializer_class(data=request.data) if manifest_serializer.is_valid(): logger.debug( @@ -145,9 +134,9 @@ def post(self, request: Request) -> Response: manifest=manifest_serializer.data, username=str(request.user) ) TaskService(task_id=task.id, task_name=task.name).update_task_status("PENDING") - return self.response(data={"taskId": task.id}, status=status.HTTP_201_CREATED) + return Response(data={"taskId": task.id}, status=status.HTTP_201_CREATED) else: logger.error("manifest_serializer errors: ", manifest_serializer.errors) - return self.response( + return Response( exception=manifest_serializer.errors, status=status.HTTP_400_BAD_REQUEST ) diff --git a/server/haztrak/settings.py b/server/haztrak/settings.py index 5834685d5..7f90c453a 100644 --- a/server/haztrak/settings.py +++ b/server/haztrak/settings.py @@ -165,6 +165,7 @@ "management software can integrate with EPA's RCRAInfo", "VERSION": HAZTRAK_VERSION, "SERVE_INCLUDE_SCHEMA": False, + "SCHEMA_PATH_PREFIX": r"/api\/(?:rcra)?", "EXTERNAL_DOCS": { "description": "Haztrak Documentation", "url": "https://usepa.github.io/haztrak/",