diff --git a/client/src/App.tsx b/client/src/App.tsx index c821e71d9..b880ed2e2 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,36 +1,21 @@ import { ErrorBoundary } from 'components/Error'; import { Notifications } from 'components/Notifications/Notifications'; import { HtSpinner } from 'components/UI'; -import React, { ReactElement, Suspense, useEffect } from 'react'; +import React, { ReactElement, Suspense } from 'react'; import { Container } from 'react-bootstrap'; import { RouterProvider } from 'react-router-dom'; import { ToastContainer } from 'react-toastify'; import 'react-toastify/dist/ReactToastify.css'; import { router } from 'routes'; -import { - getHaztrakProfile, - getHaztrakUser, - getRcraProfile, - selectHaztrakProfile, - selectUserName, - useAppDispatch, - useAppSelector, -} from 'store'; import './App.scss'; -function App(): ReactElement { - const userName = useAppSelector(selectUserName); - const profile = useAppSelector(selectHaztrakProfile); - const dispatch = useAppDispatch(); - - useEffect(() => { - if (userName) { - dispatch(getRcraProfile()); - dispatch(getHaztrakUser()); - dispatch(getHaztrakProfile()); - } - }, [profile.user]); +const GlobalSpinner = () => ( + + + +); +function App(): ReactElement { return ( - - - - } - > + }> diff --git a/client/src/components/Auth/LoginForm.tsx b/client/src/components/Auth/LoginForm.tsx index 31ee6f988..9e23e77a9 100644 --- a/client/src/components/Auth/LoginForm.tsx +++ b/client/src/components/Auth/LoginForm.tsx @@ -1,10 +1,16 @@ import { zodResolver } from '@hookform/resolvers/zod'; -import { HtForm } from 'components/UI'; -import React, { useEffect } from 'react'; +import { HtForm, HtSpinner } from 'components/UI'; +import React, { useEffect, useState } from 'react'; import { FloatingLabel, Form } from 'react-bootstrap'; import { useForm } from 'react-hook-form'; import { useNavigate } from 'react-router-dom'; -import { login, selectUserState, useAppDispatch, useAppSelector } from 'store'; +import { + selectAuthenticated, + setCredentials, + useAppDispatch, + useAppSelector, + useLoginMutation, +} from 'store'; import { z } from 'zod'; const loginSchema = z.object({ @@ -15,7 +21,9 @@ const loginSchema = z.object({ type LoginSchema = z.infer; export function LoginForm() { - const userState = useAppSelector(selectUserState); + const isAuthenticated = useAppSelector(selectAuthenticated); + const [login, { error, isLoading }] = useLoginMutation(); + const [loginError, setLoginError] = useState(undefined); const navigation = useNavigate(); const dispatch = useAppDispatch(); const { @@ -25,16 +33,30 @@ export function LoginForm() { } = useForm({ resolver: zodResolver(loginSchema) }); useEffect(() => { - // redirect to home if already logged in - if (userState.user?.username) { + if (isAuthenticated) { navigation('/'); } - }, [userState.user?.username]); + }, [isAuthenticated]); - function onSubmit({ username, password }: LoginSchema) { - return dispatch(login({ username, password })); + async function onSubmit({ username, password }: LoginSchema) { + try { + const response = await login({ username, password }).unwrap(); + if (response) { + dispatch(setCredentials({ token: response.key })); + } else { + setLoginError('something went wrong'); + } + } catch (error) { + setLoginError('unable to log in with the provided credentials'); + } } + useEffect(() => { + if (error) { + setLoginError('Error logging in'); + } + }, [error]); + return ( @@ -60,12 +82,10 @@ export function LoginForm() {
{errors.password?.message}
- {userState.error && ( -
{String(userState.error)}
- )} + {loginError &&
{loginError}
}
); } diff --git a/client/src/components/Layout/PrivateRoute.tsx b/client/src/components/Layout/PrivateRoute.tsx index dba03cea5..06e388012 100644 --- a/client/src/components/Layout/PrivateRoute.tsx +++ b/client/src/components/Layout/PrivateRoute.tsx @@ -1,22 +1,16 @@ import React, { ReactElement } from 'react'; import { Navigate } from 'react-router-dom'; -import { selectUserName, useAppSelector } from 'store'; +import { selectAuthenticated, useAppSelector } from 'store'; interface Props { children: any; } -/** - * Wraps around Route component to redirect to og in if not authenticated user - * @param { children } Route to wrap around - * @constructor - */ +/** Redirect to the login if user is not authenticated*/ export function PrivateRoute({ children }: Props): ReactElement { - const authUser = useAppSelector(selectUserName); + const authUser = useAppSelector(selectAuthenticated); if (!authUser) { - // not logged in so redirect to login page with the return url return ; } - // authorized so return child components return children; } diff --git a/client/src/components/Layout/TopNav/TopNav.tsx b/client/src/components/Layout/TopNav/TopNav.tsx index 0e44bb38e..4dc7cf30b 100644 --- a/client/src/components/Layout/TopNav/TopNav.tsx +++ b/client/src/components/Layout/TopNav/TopNav.tsx @@ -1,24 +1,25 @@ +import logo from '/assets/img/haztrak-logos/haztrak-logo-zip-file/svg/logo-no-background.svg'; import { faArrowRightFromBracket, faBars, faGear, faUser } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import logo from '/assets/img/haztrak-logos/haztrak-logo-zip-file/svg/logo-no-background.svg'; import { NavContext, NavContextProps } from 'components/Layout/Root'; import React, { useContext } from 'react'; import { Button, Dropdown } from 'react-bootstrap'; import { useDispatch, useSelector } from 'react-redux'; import { Link, useNavigate } from 'react-router-dom'; -import { RootState } from 'store'; -import { logout } from 'store/authSlice/auth.slice'; +import { removeCredentials, selectAuthenticated } from 'store'; export function TopNav() { const { showSidebar, setShowSidebar } = useContext(NavContext); - const authUser = useSelector((state: RootState) => state.auth.user); + const isAuthenticated = useSelector(selectAuthenticated); const dispatch = useDispatch(); const navigation = useNavigate(); + + if (!isAuthenticated) return null; + const handleLogout = () => { - dispatch(logout()); + dispatch(removeCredentials()); navigation('/login'); }; - if (!authUser) return null; const toggleSidebar = () => { setShowSidebar(!showSidebar); diff --git a/client/src/components/Manifest/Buttons/ManifestCancelBtn.tsx b/client/src/components/Manifest/Actions/ManifestCancelBtn.tsx similarity index 100% rename from client/src/components/Manifest/Buttons/ManifestCancelBtn.tsx rename to client/src/components/Manifest/Actions/ManifestCancelBtn.tsx diff --git a/client/src/components/Manifest/Buttons/ManifestEditBtn.tsx b/client/src/components/Manifest/Actions/ManifestEditBtn.tsx similarity index 100% rename from client/src/components/Manifest/Buttons/ManifestEditBtn.tsx rename to client/src/components/Manifest/Actions/ManifestEditBtn.tsx diff --git a/client/src/components/Manifest/Buttons/ManifestFABs.spec.tsx b/client/src/components/Manifest/Actions/ManifestFABs.spec.tsx similarity index 97% rename from client/src/components/Manifest/Buttons/ManifestFABs.spec.tsx rename to client/src/components/Manifest/Actions/ManifestFABs.spec.tsx index 048a339a5..de67ff2bf 100644 --- a/client/src/components/Manifest/Buttons/ManifestFABs.spec.tsx +++ b/client/src/components/Manifest/Actions/ManifestFABs.spec.tsx @@ -1,10 +1,10 @@ import '@testing-library/jest-dom'; -import { ManifestFABs } from 'components/Manifest/Buttons/ManifestFABs'; +import { ManifestFABs } from 'components/Manifest/Actions/ManifestFABs'; +import { ManifestContext } from 'components/Manifest/ManifestForm'; +import { ManifestStatus } from 'components/Manifest/manifestSchema'; import React from 'react'; import { cleanup, renderWithProviders, screen } from 'test-utils'; import { afterEach, describe, expect, test } from 'vitest'; -import { ManifestContext } from 'components/Manifest/ManifestForm'; -import { ManifestStatus } from 'components/Manifest/manifestSchema'; const TestComponent = ({ status, diff --git a/client/src/components/Manifest/Buttons/ManifestFABs.tsx b/client/src/components/Manifest/Actions/ManifestFABs.tsx similarity index 88% rename from client/src/components/Manifest/Buttons/ManifestFABs.tsx rename to client/src/components/Manifest/Actions/ManifestFABs.tsx index 593f27d0e..58fcb1dab 100644 --- a/client/src/components/Manifest/Buttons/ManifestFABs.tsx +++ b/client/src/components/Manifest/Actions/ManifestFABs.tsx @@ -1,5 +1,5 @@ -import { ManifestEditBtn } from 'components/Manifest/Buttons/ManifestEditBtn'; -import { ManifestSaveBtn } from 'components/Manifest/Buttons/ManifestSaveBtn'; +import { ManifestEditBtn } from 'components/Manifest/Actions/ManifestEditBtn'; +import { ManifestSaveBtn } from 'components/Manifest/Actions/ManifestSaveBtn'; import { ManifestContext } from 'components/Manifest/ManifestForm'; import { QuickSignBtn } from 'components/Manifest/QuickerSign'; import { FloatingActionBtn } from 'components/UI'; diff --git a/client/src/components/Manifest/Buttons/ManifestSaveBtn.tsx b/client/src/components/Manifest/Actions/ManifestSaveBtn.tsx similarity index 100% rename from client/src/components/Manifest/Buttons/ManifestSaveBtn.tsx rename to client/src/components/Manifest/Actions/ManifestSaveBtn.tsx diff --git a/client/src/components/Manifest/Handler/Search/HandlerSearchForm.spec.tsx b/client/src/components/Manifest/Handler/Search/HandlerSearchForm.spec.tsx index a26b7d658..b8967e5e9 100644 --- a/client/src/components/Manifest/Handler/Search/HandlerSearchForm.spec.tsx +++ b/client/src/components/Manifest/Handler/Search/HandlerSearchForm.spec.tsx @@ -1,36 +1,51 @@ +import userEvent from '@testing-library/user-event'; +import { http, HttpResponse } from 'msw'; +import { setupServer } from 'msw/node'; import React from 'react'; import '@testing-library/jest-dom'; -import { HandlerSearchForm } from './HandlerSearchForm'; +import { HaztrakProfileResponse } from 'store/userSlice/user.slice'; import { cleanup, renderWithProviders, screen } from 'test-utils'; -import { afterAll, afterEach, beforeAll, describe, expect, test } from 'vitest'; import { createMockRcrainfoSite } from 'test-utils/fixtures'; -import { setupServer } from 'msw/node'; -import { http, HttpResponse } from 'msw'; -import { API_BASE_URL } from 'test-utils/mock/handlers'; -import userEvent from '@testing-library/user-event'; +import { userApiMocks } from 'test-utils/mock'; +import { API_BASE_URL } from 'test-utils/mock/htApiMocks'; +import { afterAll, afterEach, beforeAll, describe, expect, test } from 'vitest'; +import { HandlerSearchForm } from './HandlerSearchForm'; const mockRcraSite1Id = 'VATEST111111111'; const mockRcraSite2Id = 'VATEST222222222'; const mockRcrainfoSite1Id = 'VATEST333333333'; const mockRcrainfoSite2Id = 'VATEST444444444'; +const mockProfile: HaztrakProfileResponse = { + user: 'testuser1', + sites: [], + org: { + name: 'my org', + rcrainfoIntegrated: true, + id: '1234', + }, +}; + const mockRcrainfoSite1 = createMockRcrainfoSite({ epaSiteId: mockRcrainfoSite1Id }); const mockRcrainfoSite2 = createMockRcrainfoSite({ epaSiteId: mockRcrainfoSite2Id }); const mockRcraSite1 = createMockRcrainfoSite({ epaSiteId: mockRcraSite1Id }); const mockRcraSite2 = createMockRcrainfoSite({ epaSiteId: mockRcraSite2Id }); -export const testURL = [ - http.get(`${API_BASE_URL}/api/site/search`, (info) => { +export const mockHandlerSearches = [ + http.get(`${API_BASE_URL}/api/site/search`, () => { return HttpResponse.json([mockRcraSite1, mockRcraSite2], { status: 200 }); }), - http.get(`${API_BASE_URL}/api/rcra/handler/search`, (info) => { + http.get(`${API_BASE_URL}/api/rcra/handler/search`, () => { return HttpResponse.json([mockRcrainfoSite1, mockRcrainfoSite2], { status: 200 }); }), - http.post(`${API_BASE_URL}/api/rcra/handler/search`, (info) => { + http.get(`${API_BASE_URL}/api/user/profile`, () => { + return HttpResponse.json({ ...mockProfile }, { status: 200 }); + }), + http.post(`${API_BASE_URL}/api/rcra/handler/search`, () => { return HttpResponse.json([mockRcrainfoSite1, mockRcrainfoSite2], { status: 200 }); }), ]; -const server = setupServer(...testURL); +const server = setupServer(...userApiMocks, ...mockHandlerSearches); afterEach(() => { cleanup(); }); @@ -46,19 +61,7 @@ describe('HandlerSearchForm', () => { }); test('retrieves rcra sites from haztrak and RCRAInfo', async () => { renderWithProviders( - undefined} handlerType="generator" />, - { - preloadedState: { - profile: { - user: 'testuser1', - org: { - name: 'my org', - rcrainfoIntegrated: true, - id: '1234', - }, - }, - }, - } + undefined} handlerType="generator" /> ); const epaId = screen.getByRole('combobox'); await userEvent.type(epaId, 'VATEST'); @@ -68,20 +71,19 @@ describe('HandlerSearchForm', () => { expect(await screen.findByText(new RegExp(mockRcrainfoSite2Id, 'i'))).toBeInTheDocument(); }); test('retrieves rcra sites from haztrak if org not rcrainfo integrated', async () => { - renderWithProviders( - undefined} handlerType="generator" />, - { - preloadedState: { - profile: { - user: 'testuser1', - org: { - name: 'my org', - rcrainfoIntegrated: false, - id: '1234', - }, + server.use( + http.get(`${API_BASE_URL}/api/user/profile`, () => { + return HttpResponse.json( + { + ...mockProfile, + org: { rcrainfoIntegrated: false }, }, - }, - } + { status: 200 } + ); + }) + ); + renderWithProviders( + undefined} handlerType="generator" /> ); const epaId = screen.getByRole('combobox'); await userEvent.type(epaId, 'VATEST'); diff --git a/client/src/components/Manifest/Handler/Search/HandlerSearchForm.tsx b/client/src/components/Manifest/Handler/Search/HandlerSearchForm.tsx index 35b37dd7c..0f5381d52 100644 --- a/client/src/components/Manifest/Handler/Search/HandlerSearchForm.tsx +++ b/client/src/components/Manifest/Handler/Search/HandlerSearchForm.tsx @@ -1,3 +1,4 @@ +import { RcrainfoSiteSearchBadge } from 'components/Manifest/Handler/Search/RcrainfoSiteSearchBadge'; import { ManifestContext, ManifestContextType } from 'components/Manifest/ManifestForm'; import { Manifest, SiteType, Transporter } from 'components/Manifest/manifestSchema'; import { RcraSite } from 'components/RcraSite'; @@ -12,13 +13,7 @@ import { useFormContext, } from 'react-hook-form'; import Select from 'react-select'; -import { - selectHaztrakProfile, - useAppSelector, - useSearchRcrainfoSitesQuery, - useSearchRcraSitesQuery, -} from 'store'; -import { RcrainfoSiteSearchBadge } from 'components/Manifest/Handler/Search/RcrainfoSiteSearchBadge'; +import { useGetProfileQuery, useSearchRcrainfoSitesQuery, useSearchRcraSitesQuery } from 'store'; interface Props { handleClose: () => void; @@ -42,9 +37,13 @@ export function HandlerSearchForm({ const manifestMethods = useFormContext(); const [inputValue, setInputValue] = useState(''); const [selectedHandler, setSelectedHandler] = useState(null); - const { org } = useAppSelector(selectHaztrakProfile); + const { org } = useGetProfileQuery(undefined, { + selectFromResult: ({ data }) => { + return { org: data?.org }; + }, + }); const [skip, setSkip] = useState(true); - const { data, error, isLoading } = useSearchRcraSitesQuery( + const { data } = useSearchRcraSitesQuery( { siteType: handlerType, siteId: inputValue, diff --git a/client/src/components/Manifest/ManifestForm.tsx b/client/src/components/Manifest/ManifestForm.tsx index 9861bbe71..6529f4fe5 100644 --- a/client/src/components/Manifest/ManifestForm.tsx +++ b/client/src/components/Manifest/ManifestForm.tsx @@ -1,24 +1,25 @@ import { ErrorMessage } from '@hookform/error-message'; import { zodResolver } from '@hookform/resolvers/zod'; +import { createSelector } from '@reduxjs/toolkit'; +import { ManifestCancelBtn } from 'components/Manifest/Actions/ManifestCancelBtn'; +import { ManifestEditBtn } from 'components/Manifest/Actions/ManifestEditBtn'; +import { ManifestFABs } from 'components/Manifest/Actions/ManifestFABs'; +import { ManifestSaveBtn } from 'components/Manifest/Actions/ManifestSaveBtn'; import { AdditionalInfoForm } from 'components/Manifest/AdditionalInfo'; -import { ManifestCancelBtn } from 'components/Manifest/Buttons/ManifestCancelBtn'; -import { ManifestEditBtn } from 'components/Manifest/Buttons/ManifestEditBtn'; -import { ManifestFABs } from 'components/Manifest/Buttons/ManifestFABs'; -import { ManifestSaveBtn } from 'components/Manifest/Buttons/ManifestSaveBtn'; import { UpdateRcra } from 'components/Manifest/UpdateRcra/UpdateRcra'; import { WasteLine } from 'components/Manifest/WasteLine/wasteLineSchema'; import { RcraSiteDetails } from 'components/RcraSite'; import { HtButton, HtCard, HtForm, InfoIconTooltip } from 'components/UI'; -import React, { createContext, useEffect, useState } from 'react'; +import React, { createContext, useEffect, useMemo, useState } from 'react'; import { Alert, Button, Col, Container, Form, Row, Stack } from 'react-bootstrap'; import { FormProvider, SubmitHandler, useFieldArray, useForm } from 'react-hook-form'; import { useNavigate } from 'react-router-dom'; import { toast } from 'react-toastify'; import { manifest } from 'services'; import { - selectHaztrakSiteEpaIds, - useAppSelector, + ProfileSlice, useCreateManifestMutation, + useGetProfileQuery, useSaveEManifestMutation, useUpdateManifestMutation, } from 'store'; @@ -239,8 +240,25 @@ export function ManifestForm({ manifestData?.status ); + const selectUserSiteIds = useMemo( + () => + createSelector( + (res) => res.data, + (data: ProfileSlice) => + !data ?? !data.sites + ? [] + : Object.values(data.sites).map((site) => site.handler.epaSiteId) + ), + [] + ); + const nextSigner = manifest.getNextSigner(manifestData); - const userSiteIds = useAppSelector(selectHaztrakSiteEpaIds); + const { userSiteIds } = useGetProfileQuery(undefined, { + selectFromResult: (result) => ({ + ...result, + userSiteIds: selectUserSiteIds(result), + }), + }); // Whether the user has permissions and manifest is in a state to be signed const signAble = userSiteIds.includes(nextSigner?.epaSiteId ?? ''); diff --git a/client/src/components/Manifest/QuickerSign/SignBtn/QuickSignBtn.spec.tsx b/client/src/components/Manifest/QuickerSign/SignBtn/QuickSignBtn.spec.tsx index 532c57d50..c878b7f7a 100644 --- a/client/src/components/Manifest/QuickerSign/SignBtn/QuickSignBtn.spec.tsx +++ b/client/src/components/Manifest/QuickerSign/SignBtn/QuickSignBtn.spec.tsx @@ -2,12 +2,31 @@ import '@testing-library/jest-dom'; import { ManifestContext } from 'components/Manifest/ManifestForm'; import { Handler, RcraSiteType } from 'components/Manifest/manifestSchema'; import { QuickSignBtn } from 'components/Manifest/QuickerSign/index'; +import { setupServer } from 'msw/node'; import React from 'react'; +import { HaztrakProfileResponse } from 'store/userSlice/user.slice'; import { cleanup, renderWithProviders, screen } from 'test-utils'; import { createMockMTNHandler } from 'test-utils/fixtures'; -import { afterEach, describe, expect, test } from 'vitest'; +import { userApiMocks } from 'test-utils/mock'; +import { afterAll, afterEach, beforeAll, describe, expect, test } from 'vitest'; import { undefined } from 'zod'; +const mockProfile: HaztrakProfileResponse = { + user: 'testuser1', + sites: [], + org: { + name: 'my org', + rcrainfoIntegrated: true, + id: '1234', + }, +}; + +const server = setupServer(...userApiMocks); +afterEach(() => { + cleanup(); +}); +beforeAll(() => server.listen()); +afterAll(() => server.close()); // Disable API mocking after the tests are done. afterEach(() => { cleanup(); }); @@ -40,60 +59,12 @@ function TestComponent({ } describe('QuickSignBtn', () => { - test('renders', () => { - const handlerId = 'TXD987654321'; - const handler = createMockMTNHandler({ siteType: 'Generator', epaSiteId: handlerId }); - renderWithProviders( - , - { - preloadedState: { - profile: { - user: 'testuser1', - org: { - rcrainfoIntegrated: true, - id: '123', - name: 'Test Org', - }, - sites: { - TXD987654321: { - name: 'Test Site', - handler: handler, - permissions: { eManifest: 'signer' }, - }, - }, - }, - }, - } - ); - expect(screen.getByRole('button')).toBeInTheDocument(); - }); test('is not disabled when user org is rcrainfo integrated', () => { const unsigned_handler = createMockMTNHandler({ signed: false, electronicSignaturesInfo: [], }); - renderWithProviders(, { - // Redux store state with an API user is required for this button to be active - preloadedState: { - profile: { - org: { - name: 'Test Org', - id: '123', - rcrainfoIntegrated: true, - }, - user: 'username', - rcrainfoProfile: { - user: 'username', - phoneNumber: '1231231234', - apiUser: true, - }, - }, - }, - }); + renderWithProviders(); expect(screen.queryByRole('button')).not.toBeInTheDocument(); }); test('is disabled when API user but already signed', () => { @@ -108,20 +79,7 @@ describe('QuickSignBtn', () => { siteType={'Tsdf'} signingSite={{ epaSiteId: 'other_site', siteType: 'transporter' }} handler={unsigned_handler} - />, - // Redux store state with an API user is required for this button to be active - { - preloadedState: { - profile: { - user: 'username', - rcrainfoProfile: { - user: 'username', - phoneNumber: '1231231234', - apiUser: false, - }, - }, - }, - } + /> ); expect(screen.queryByRole('button')).not.toBeInTheDocument(); }); diff --git a/client/src/components/Manifest/QuickerSign/SignBtn/QuickSignBtn.tsx b/client/src/components/Manifest/QuickerSign/SignBtn/QuickSignBtn.tsx index 9a958ca08..3f582c5e0 100644 --- a/client/src/components/Manifest/QuickerSign/SignBtn/QuickSignBtn.tsx +++ b/client/src/components/Manifest/QuickerSign/SignBtn/QuickSignBtn.tsx @@ -1,11 +1,12 @@ import { faFeather } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { createSelector } from '@reduxjs/toolkit'; import { ManifestContext } from 'components/Manifest/ManifestForm'; import { Handler, RcraSiteType } from 'components/Manifest/manifestSchema'; import { RcraApiUserBtn } from 'components/Rcrainfo'; -import React, { useContext } from 'react'; +import React, { useContext, useMemo } from 'react'; import { ButtonProps } from 'react-bootstrap'; -import { siteByEpaIdSelector, useAppSelector } from 'store'; +import { ProfileSlice, useGetProfileQuery } from 'store'; interface QuickSignBtnProps extends ButtonProps { siteType?: RcraSiteType; @@ -25,8 +26,24 @@ export function QuickSignBtn({ ...props }: QuickSignBtnProps) { const { nextSigningSite } = useContext(ManifestContext); - // if next site to sign is not one of the user's sites, don't show the button - if (!useAppSelector(siteByEpaIdSelector(nextSigningSite?.epaSiteId))) return <>; + + const selectBySiteId = useMemo(() => { + return createSelector( + (res) => res.data, + (res, siteId) => siteId, + (data: ProfileSlice, siteId) => { + return data && data.sites ? data.sites[siteId] : undefined; + } + ); + }, []); + + const { userSite } = useGetProfileQuery(undefined, { + selectFromResult: (res) => ({ + ...res, + userSite: selectBySiteId(res, nextSigningSite?.epaSiteId), + }), + }); + if (!userSite) return <>; if (mtnHandler && mtnHandler?.epaSiteId !== nextSigningSite?.epaSiteId) return <>; diff --git a/client/src/components/Manifest/SiteSelect/SiteSelect.spec.tsx b/client/src/components/Manifest/SiteSelect/SiteSelect.spec.tsx index d433b84fa..38b05eb59 100644 --- a/client/src/components/Manifest/SiteSelect/SiteSelect.spec.tsx +++ b/client/src/components/Manifest/SiteSelect/SiteSelect.spec.tsx @@ -3,8 +3,6 @@ import { SiteSelect } from 'components/Manifest/SiteSelect/SiteSelect'; import React, { useState } from 'react'; import { useForm } from 'react-hook-form'; import { renderWithProviders } from 'test-utils'; -import { createMockSite } from 'test-utils/fixtures'; -import { createMockRcrainfoPermissions } from 'test-utils/fixtures/mockHandler'; import { describe, expect, test } from 'vitest'; function TestComponent() { @@ -16,29 +14,7 @@ function TestComponent() { describe('SiteSelect', () => { test('renders', () => { - const mySite = createMockSite(); - renderWithProviders(, { - preloadedState: { - profile: { - user: 'username', - rcrainfoProfile: { - user: 'username', - phoneNumber: '1231231234', - apiUser: false, - rcraSites: { - VATESTGEN001: { - epaSiteId: 'VATESTGEN001', - permissions: createMockRcrainfoPermissions(), - }, - VATEST00001: { - epaSiteId: 'VATEST00001', - permissions: createMockRcrainfoPermissions(), - }, - }, - }, - }, - }, - }); + renderWithProviders(); expect(screen.queryByTestId('siteSelect')).toBeDefined(); }); }); diff --git a/client/src/components/Manifest/SiteSelect/SiteSelect.tsx b/client/src/components/Manifest/SiteSelect/SiteSelect.tsx index 2b381f798..3fa1efb95 100644 --- a/client/src/components/Manifest/SiteSelect/SiteSelect.tsx +++ b/client/src/components/Manifest/SiteSelect/SiteSelect.tsx @@ -1,9 +1,10 @@ +import { createSelector } from '@reduxjs/toolkit'; import { RcraSite } from 'components/RcraSite'; import { HtForm } from 'components/UI'; -import React from 'react'; +import React, { useMemo } from 'react'; import { Control, Controller } from 'react-hook-form'; import Select from 'react-select'; -import { selectHaztrakSites, useAppSelector } from 'store'; +import { ProfileSlice, useGetProfileQuery } from 'store'; interface SiteSelectProps { control: Control; @@ -16,8 +17,21 @@ export function SiteSelect({ value, handleChange, }: SiteSelectProps) { - const userSites = useAppSelector(selectHaztrakSites); - const siteOptions = userSites?.map((site) => site.handler); + const selectUserSites = useMemo(() => { + return createSelector( + (res) => res?.data, + (data: ProfileSlice) => + !data || !data.sites ? [] : Object.values(data.sites).map((site) => site.handler) + ); + }, []); + + const { siteOptions } = useGetProfileQuery(undefined, { + selectFromResult: (result) => ({ + ...result, + siteOptions: selectUserSites(result), + }), + }); + return ( <> Site diff --git a/client/src/components/Org/OrgSitesTable.tsx b/client/src/components/Org/OrgSitesTable.tsx index e11a6bb40..6a932e15b 100644 --- a/client/src/components/Org/OrgSitesTable.tsx +++ b/client/src/components/Org/OrgSitesTable.tsx @@ -1,8 +1,7 @@ import { HaztrakSite } from 'components/HaztrakSite'; import React from 'react'; import { Row, Table } from 'react-bootstrap'; -import { useGetOrgSitesQuery } from 'store'; -import { HaztrakProfileOrg } from 'store/profileSlice/profile.slice'; +import { HaztrakProfileOrg, useGetOrgSitesQuery } from 'store'; interface OrgSitesProps { org: HaztrakProfileOrg; diff --git a/client/src/components/RcraProfile/RcraProfile.tsx b/client/src/components/RcraProfile/RcraProfile.tsx index a143c5e08..abcc14703 100644 --- a/client/src/components/RcraProfile/RcraProfile.tsx +++ b/client/src/components/RcraProfile/RcraProfile.tsx @@ -1,21 +1,12 @@ import { zodResolver } from '@hookform/resolvers/zod'; -import { AxiosError } from 'axios'; -import { RcraApiUserBtn } from 'components/Rcrainfo'; +import { SyncRcrainfoProfileBtn } from 'components/RcraProfile/SyncRcrainfoProfileBtn'; import { HtForm, HtSpinner } from 'components/UI'; import { useProgressTracker } from 'hooks'; -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { Button, Col, Container, Form, Row, Table } from 'react-bootstrap'; import { useForm } from 'react-hook-form'; -import { toast } from 'react-toastify'; -import { UserApi } from 'services'; -import { - addAlert, - addTask, - getRcraProfile, - RcrainfoProfileState, - updateProfile, - useAppDispatch, -} from 'store'; +import { RcrainfoProfileState, useAppDispatch, useUpdateRcrainfoProfileMutation } from 'store'; +import { userApi } from 'store/userSlice/user.slice'; import { z } from 'zod'; interface ProfileViewProps { @@ -23,24 +14,28 @@ interface ProfileViewProps { } const rcraProfileForm = z.object({ - rcraAPIID: z.string().min(36).optional(), - rcraAPIKey: z.string().min(20).optional(), + rcraAPIID: z.string().min(36).optional().or(z.string().nullable()), + rcraAPIKey: z.string().min(20).optional().or(z.string().nullable()), rcraUsername: z.string().min(8).optional(), }); type RcraProfileForm = z.infer; export function RcraProfile({ profile }: ProfileViewProps) { + const dispatch = useAppDispatch(); const [editable, setEditable] = useState(false); const [profileLoading, setProfileLoading] = useState(false); const [taskId, setTaskId] = useState(); + const [updateRcrainfoProfile] = useUpdateRcrainfoProfileMutation(); const { rcraSites, isLoading, ...formValues } = profile; - const dispatch = useAppDispatch(); const { inProgress } = useProgressTracker({ taskId: taskId, - reduxAction: getRcraProfile(), }); + useEffect(() => { + dispatch(userApi.util?.invalidateTags(['rcrainfoProfile'])); + }, [inProgress]); + const { register, reset, @@ -51,20 +46,11 @@ export function RcraProfile({ profile }: ProfileViewProps) { resolver: zodResolver(rcraProfileForm), }); - /** - * submitting the RcraProfile form (RCRAInfo API ID, Key, username, etc.) - * @param data {ProfileSlice} - */ + /** submitting the RcrainfoProfile form (RCRAInfo API ID, Key, username, etc.)*/ const onSubmit = (data: RcraProfileForm) => { setProfileLoading(!profileLoading); setEditable(!editable); - UserApi.updateRcrainfoProfile({ username: profile.user, data: data }) - .then((r) => { - dispatch(updateProfile(r.data)); - }) - .then(() => dispatch(getRcraProfile())) - .then(() => setProfileLoading(!profileLoading)) - .catch((error: AxiosError) => toast.error(error.message)); + updateRcrainfoProfile({ username: profile.user, data: data }); }; if (profile.isLoading) return ; @@ -126,10 +112,9 @@ export function RcraProfile({ profile }: ProfileViewProps) { <> + + + + ); +}; + +describe('auth slice', () => { + test('our localStorage mocks are working', () => { + const value = 'test'; + localStorage.setItem('token', value); + expect(setItemSpy).toHaveBeenCalled(); + const foo = localStorage.getItem('token'); + expect(setItemSpy).toHaveBeenCalled(); + expect(foo).toBe(value); + }); + test('initial state is unauthenticated', () => { + renderWithProviders(); + expect(screen.queryByText('unauthenticated')).toBeInTheDocument(); + expect(screen.queryByText('authenticated')).not.toBeInTheDocument(); + }); + test('setCredentials stores the token in local storage', async () => { + renderWithProviders(); + await userEvent.click(screen.getByText('Log in')); + expect(setItemSpy).toHaveBeenCalled(); + }); + test('selectAuthenticated is true after credentials are set', async () => { + renderWithProviders(); + await userEvent.click(screen.getByText('Log in')); + expect(screen.queryByText('authenticated')).toBeInTheDocument(); + }); + test('selectAuthenticated is false removeCredentials is dispatched', async () => { + renderWithProviders(, { preloadedState: { auth: { token: 'mockToken' } } }); + localStorage.setItem('token', 'mockToken'); + await userEvent.click(screen.getByText('Log out')); + expect(screen.queryByText('unauthenticated')).toBeInTheDocument(); + }); + test('tokens are removed from localStorage on logout', async () => { + renderWithProviders(, { preloadedState: { auth: { token: 'mockToken' } } }); + localStorage.setItem('token', 'mockToken'); + await userEvent.click(screen.getByText('Log out')); + expect(removeItemSpy).toHaveBeenCalled(); + }); +}); diff --git a/client/src/store/authSlice/auth.slice.ts b/client/src/store/authSlice/auth.slice.ts index be76a59ef..7dc0ddd9f 100644 --- a/client/src/store/authSlice/auth.slice.ts +++ b/client/src/store/authSlice/auth.slice.ts @@ -1,7 +1,4 @@ -import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit'; -import axios from 'axios'; -import { UserApi } from 'services'; -import { RootState } from 'store/rootStore'; +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; export interface HaztrakUser { username: string; @@ -15,122 +12,43 @@ export interface HaztrakUser { /** * The Redux stored information on the current haztrak user */ -export interface UserState { +export interface AuthSlice { user?: HaztrakUser; token?: string; loading?: boolean; error?: string; } -const initialState: UserState = { - // Retrieve the user's username and token from local storage. For convenience +const initialState: AuthSlice = { user: { username: JSON.parse(localStorage.getItem('user') || 'null') || null, isLoading: false }, token: JSON.parse(localStorage.getItem('token') || 'null') || null, loading: false, error: undefined, }; - -export const login = createAsyncThunk( - 'auth/login', - async ({ username, password }: { username: string; password: string }) => { - const response = await axios.post(`${import.meta.env.VITE_HT_API_URL}/api/user/login`, { - username, - password, - }); - // return response.data as UserState; - return { - user: { username: response.data.user }, - token: response.data.token, - } as UserState; - } -); - -/** Fetch a Haztrak User's information and store in global state */ -export const getHaztrakUser = createAsyncThunk('auth/getHaztrakUser', async (arg, thunkAPI) => { - try { - const { data } = await UserApi.getUser(); - return data; - } catch (err) { - return thunkAPI.rejectWithValue(err); - } -}); - -const authSlice = createSlice({ +export const authSlice = createSlice({ name: 'auth', initialState, + selectors: { + selectAuthenticated: (state: AuthSlice): boolean => !!state.token, + selectUser: (state: AuthSlice): HaztrakUser | undefined => state.user, + selectUserName: (state: AuthSlice): string | undefined => state.user?.username, + }, reducers: { - logout(state: UserState): UserState { - localStorage.removeItem('user'); - localStorage.removeItem('token'); - return { ...initialState, user: undefined, token: undefined }; - }, - updateUserProfile(state: UserState, action: PayloadAction) { + setCredentials(state: AuthSlice, action: PayloadAction<{ token: string }>) { + const { token } = action.payload; + localStorage.setItem('token', JSON.stringify(token)); return { ...state, - user: action.payload, - }; + token, + } as AuthSlice; + }, + removeCredentials(): AuthSlice { + localStorage.removeItem('token'); + return { ...initialState, user: undefined, token: undefined }; }, - }, - extraReducers: (builder) => { - builder - .addCase(login.pending, (state) => { - return { - ...state, - error: undefined, - loading: true, - }; - }) - .addCase(login.fulfilled, (state, action) => { - const authResponse = action.payload; - localStorage.setItem('user', JSON.stringify(authResponse.user?.username)); - localStorage.setItem('token', JSON.stringify(authResponse.token)); - return { - loading: false, - error: undefined, - ...authResponse, - }; - }) - .addCase(login.rejected, (state, action) => { - return { - ...state, - // @ts-ignore - error: action.payload.error, - loading: false, - }; - }) - .addCase(getHaztrakUser.pending, (state) => { - return { - ...state, - error: undefined, - loading: true, - }; - }) - .addCase(getHaztrakUser.rejected, (state, action) => { - return { - ...state, - error: `Error: ${action.payload}`, - loading: true, - }; - }) - .addCase(getHaztrakUser.fulfilled, (state, action) => { - return { - ...state, - user: action.payload, - error: undefined, - loading: true, - }; - }); }, }); -/** Get the current user's username from the Redux store*/ -export const selectUserName = (state: RootState): string | undefined => state.auth.user?.username; - -/** Select the current user*/ -export const selectUser = (state: RootState): HaztrakUser | undefined => state.auth.user; - -/** Select the current User State*/ -export const selectUserState = (state: RootState): UserState => state.auth; - export default authSlice.reducer; -export const { updateUserProfile, logout } = authSlice.actions; +export const { removeCredentials, setCredentials } = authSlice.actions; +export const { selectAuthenticated, selectUser, selectUserName } = authSlice.selectors; diff --git a/client/src/store/haztrakApiSlice.ts b/client/src/store/htApi.slice.ts similarity index 86% rename from client/src/store/haztrakApiSlice.ts rename to client/src/store/htApi.slice.ts index 3da5f872a..f2a8dac6c 100644 --- a/client/src/store/haztrakApiSlice.ts +++ b/client/src/store/htApi.slice.ts @@ -2,11 +2,11 @@ import { BaseQueryFn, createApi } from '@reduxjs/toolkit/query/react'; import { AxiosError, AxiosRequestConfig, AxiosResponse } from 'axios'; import { HaztrakSite } from 'components/HaztrakSite'; import { Manifest } from 'components/Manifest'; +import { QuickerSignature } from 'components/Manifest/QuickerSign'; import { Code } from 'components/Manifest/WasteLine/wasteLineSchema'; import { MtnDetails } from 'components/Mtn'; import { RcraSite } from 'components/RcraSite'; import { htApi } from 'services'; -import { QuickerSignature } from 'components/Manifest/QuickerSign'; export interface TaskResponse { taskId: string; @@ -19,10 +19,8 @@ export interface HtApiQueryArgs { params?: AxiosRequestConfig['params']; } -export interface HtApiError { - status?: number; +export interface HtApiError extends AxiosError { data?: AxiosResponse['data']; - code?: string; statusText?: string; } @@ -50,10 +48,10 @@ export const htApiBaseQuery = return { data: response.data }; } catch (axiosError) { let err = axiosError as AxiosError; + console.log(err); return { error: { - code: err.code, - status: err.response?.status, + ...err, statusText: err.response?.statusText, data: err.response?.data || err.message, } as HtApiError, @@ -76,6 +74,7 @@ interface RcrainfoSiteSearch { } export const haztrakApi = createApi({ + tagTypes: ['user', 'auth', 'profile', 'rcrainfoProfile', 'site', 'code', 'manifest'], reducerPath: 'haztrakApi', baseQuery: htApiBaseQuery({ baseUrl: `${import.meta.env.VITE_HT_API_URL}/api/`, @@ -101,24 +100,35 @@ export const haztrakApi = createApi({ }), getFedWasteCodes: build.query, void>({ query: () => ({ url: 'rcra/waste/code/federal', method: 'get' }), + providesTags: ['code'], }), getStateWasteCodes: build.query, string>({ query: (state) => ({ url: `rcra/waste/code/state/${state}`, method: 'get' }), + providesTags: ['code'], }), getDotIdNumbers: build.query, string>({ query: (id) => ({ url: 'rcra/waste/dot/id', method: 'get', params: { q: id } }), + providesTags: ['code'], }), getOrgSites: build.query, string>({ query: (id) => ({ url: `org/${id}/site`, method: 'get' }), + providesTags: ['site'], }), getUserHaztrakSites: build.query, void>({ query: () => ({ url: 'site', method: 'get' }), + providesTags: ['site'], }), getUserHaztrakSite: build.query({ query: (epaId) => ({ url: `site/${epaId}`, method: 'get' }), + providesTags: ['site'], }), getMTN: build.query, string | undefined>({ query: (siteId) => ({ url: siteId ? `rcra/mtn/${siteId}` : 'rcra/mtn', method: 'get' }), + providesTags: ['manifest'], + }), + getManifest: build.query({ + query: (mtn) => ({ url: `rcra/manifest/${mtn}`, method: 'get' }), + providesTags: ['manifest'], }), createManifest: build.mutation({ query: (data) => ({ @@ -126,6 +136,7 @@ export const haztrakApi = createApi({ method: 'POST', data, }), + invalidatesTags: ['manifest'], }), updateManifest: build.mutation({ query: ({ mtn, manifest }) => ({ @@ -133,6 +144,7 @@ export const haztrakApi = createApi({ method: 'PUT', data: manifest, }), + invalidatesTags: ['manifest'], }), saveEManifest: build.mutation({ query: (data) => ({ @@ -140,6 +152,7 @@ export const haztrakApi = createApi({ method: 'POST', data, }), + invalidatesTags: ['manifest'], }), syncEManifest: build.mutation({ query: (siteId) => ({ @@ -147,6 +160,7 @@ export const haztrakApi = createApi({ method: 'POST', data: { siteId: siteId }, }), + invalidatesTags: ['manifest'], }), signEManifest: build.mutation({ query: (signature) => ({ @@ -154,6 +168,7 @@ export const haztrakApi = createApi({ method: 'POST', data: signature, }), + invalidatesTags: ['manifest'], }), }), }); diff --git a/client/src/store/index.ts b/client/src/store/index.ts index b7b5e8251..aab921280 100644 --- a/client/src/store/index.ts +++ b/client/src/store/index.ts @@ -1,6 +1,7 @@ -import type { AppDispatch, AppStore, RootState } from './rootStore'; // Haztrak API - RTK Query -import { haztrakApi } from './haztrakApiSlice'; +import { haztrakApi } from 'store/htApi.slice'; +import { userApi } from 'store/userSlice/user.slice'; +import type { AppDispatch, AppStore, RootState } from './rootStore'; // Root Store export { rootStore, setupStore, useAppDispatch, useAppSelector } from './rootStore'; @@ -22,30 +23,28 @@ export const { useSyncEManifestMutation, useSignEManifestMutation, useUpdateManifestMutation, + useGetManifestQuery, } = haztrakApi; +export const { + useLoginMutation, + useGetUserQuery, + useGetProfileQuery, + useGetRcrainfoProfileQuery, + useUpdateUserMutation, + useUpdateRcrainfoProfileMutation, + useSyncRcrainfoProfileMutation, +} = userApi; + // Authentication Slice export { - getHaztrakUser, - login, selectUser, selectUserName, - selectUserState, - updateUserProfile, -} from './authSlice/auth.slice'; - -// Profile Slice -export { - getHaztrakProfile, - getRcraProfile, - selectRcrainfoSites, - selectRcraProfile, - siteByEpaIdSelector, - updateProfile, - selectHaztrakSites, - selectHaztrakSiteEpaIds, - selectHaztrakProfile, -} from './profileSlice/profile.slice'; + setCredentials, + selectAuthenticated, + removeCredentials, +} from 'store/authSlice/auth.slice'; +export type { HaztrakUser } from 'store/authSlice/auth.slice'; // Notification Slice export { @@ -64,9 +63,8 @@ export { export { addError, selectAllErrors } from './errorSlice/error.slice'; // Types -export type { HaztrakUser } from './authSlice/auth.slice'; export type { HaztrakError } from './errorSlice/error.slice'; -export type { TaskStatus } from './haztrakApiSlice'; +export type { TaskStatus } from 'store/htApi.slice'; export type { LongRunningTask, HaztrakAlert } from './notificationSlice/notification.slice'; export type { ProfileSlice, @@ -75,6 +73,7 @@ export type { HaztrakSitePermissions, RcrainfoSitePermissions, HaztrakModulePermissions, + HaztrakProfileOrg, RcrainfoProfile, RcrainfoProfileSite, -} from './profileSlice/profile.slice'; +} from './userSlice/user.slice'; diff --git a/client/src/store/profileSlice/profile.slice.spec.tsx b/client/src/store/profileSlice/profile.slice.spec.tsx deleted file mode 100644 index bcc217b63..000000000 --- a/client/src/store/profileSlice/profile.slice.spec.tsx +++ /dev/null @@ -1,91 +0,0 @@ -/** - * RcraProfile tests - */ -import '@testing-library/jest-dom'; -import { screen } from '@testing-library/react'; -import React from 'react'; -import { useAppSelector } from 'store'; -import { renderWithProviders } from 'test-utils'; -import { createMockSite } from 'test-utils/fixtures'; -import { createMockRcrainfoPermissions } from 'test-utils/fixtures/mockHandler'; -import { describe, expect, test } from 'vitest'; -import { selectRcrainfoSites, siteByEpaIdSelector } from './profile.slice'; - -interface TestComponentProps { - siteId: string; -} - -function TestComponent({ siteId }: TestComponentProps) { - const rcraSite = useAppSelector(siteByEpaIdSelector(siteId)); - return ( - <> -

{rcraSite?.epaSiteId}

- - ); -} - -describe('RcraProfileSlice selectors', () => { - test('Retrieve RcraProfileSite by EPA ID', () => { - const mySite = createMockSite(); - renderWithProviders(, { - preloadedState: { - profile: { - user: 'testuser1', - sites: { VATESTGEN001: { ...mySite, permissions: { eManifest: 'viewer' } } }, - rcrainfoProfile: { - user: 'username', - phoneNumber: '1231231234', - apiUser: false, - rcraSites: { - VATESTGEN001: { - epaSiteId: mySite.handler.epaSiteId, - permissions: createMockRcrainfoPermissions(), - }, - }, - }, - }, - }, - }); - expect(screen.getByText(mySite.handler.epaSiteId)).toBeInTheDocument(); - }); - test('retrieve all RcraProfileSites', () => { - const mySite = createMockSite(); - const TestComp = () => { - const myRcraSite = useAppSelector(selectRcrainfoSites); - return ( - <> -

{myRcraSite ? Object.keys(myRcraSite).length : 'not defined'}

- {myRcraSite - ? myRcraSite.map((site, index) => ( -

{site.epaSiteId}

- )) - : 'not defined'} - - ); - }; - renderWithProviders(, { - preloadedState: { - profile: { - user: 'testuser1', - rcrainfoProfile: { - user: 'username', - phoneNumber: '1231231234', - apiUser: false, - rcraSites: { - VATESTGEN001: { - epaSiteId: mySite.handler.epaSiteId, - permissions: createMockRcrainfoPermissions(), - }, - VATEST00001: { - epaSiteId: mySite.handler.epaSiteId, - permissions: createMockRcrainfoPermissions(), - }, - }, - }, - }, - }, - }); - expect(screen.getByText('2')).toBeInTheDocument(); - expect(screen.queryByText(/Not Defined/i)).not.toBeInTheDocument(); - }); -}); diff --git a/client/src/store/profileSlice/profile.slice.ts b/client/src/store/profileSlice/profile.slice.ts deleted file mode 100644 index b7594b012..000000000 --- a/client/src/store/profileSlice/profile.slice.ts +++ /dev/null @@ -1,238 +0,0 @@ -/** - * A user's RcraProfile slice encapsulates our logic related what actions and data a user - * has access to for each EPA site ID. - */ -import { createAsyncThunk, createSelector, createSlice, PayloadAction } from '@reduxjs/toolkit'; -import { HaztrakSite } from 'components/HaztrakSite'; -import { RcraSite } from 'components/RcraSite'; -import { UserApi } from 'services'; -import { RootState } from 'store'; - -/**The user's RCRAInfo account data stored in the Redux store*/ -export interface ProfileSlice { - user: string | undefined; - rcrainfoProfile?: RcrainfoProfile>; - sites?: Record; - org?: HaztrakProfileOrg | null; - loading?: boolean; - error?: string; -} - -export interface HaztrakProfileOrg { - id: string; - name: string; - rcrainfoIntegrated: boolean; -} - -/** A site a user has access to in RCRAInfo and their module permissions */ -export interface RcrainfoProfileSite { - epaSiteId: string; - permissions: RcrainfoSitePermissions; -} - -export interface HaztrakProfileSite extends HaztrakSite { - permissions: HaztrakSitePermissions; -} - -export type HaztrakModulePermissions = 'viewer' | 'editor' | 'signer'; - -export interface HaztrakSitePermissions { - eManifest: HaztrakModulePermissions; -} - -export interface RcrainfoProfileState - extends RcrainfoProfile> {} - -export interface RcrainfoProfile { - user: string; - rcraAPIID?: string; - rcraUsername?: string; - rcraAPIKey?: string; - apiUser?: boolean; - rcraSites?: T; - phoneNumber?: string; - isLoading?: boolean; - error?: string; -} - -export interface RcrainfoSitePermissions { - siteManagement: boolean; - annualReport: string; - biennialReport: string; - eManifest: string; - WIETS: string; - myRCRAid: string; -} - -/**initial, state of a user's RcraProfile.*/ -const initialState: ProfileSlice = { - user: undefined, - rcrainfoProfile: undefined, - sites: undefined, - loading: false, - error: undefined, -}; - -/**Retrieves a user's profile from the server.*/ -export const getHaztrakProfile = createAsyncThunk('profile/getHaztrakProfile', async () => { - const { data } = await UserApi.getUserProfile(); - const sites = data.sites.reduce((obj, site) => { - return { - ...obj, - [site.site.handler.epaSiteId]: { - ...site.site, - permissions: { eManifest: site.eManifest }, - }, - }; - }, {}); - return { - user: data.user, - org: data.org, - sites: sites, - } as ProfileSlice; -}); - -/**Retrieves a user's RcrainfoProfile, if it exists, from the server.*/ -export const getRcraProfile = createAsyncThunk( - 'profile/getRcrainfoProfile', - async (arg, thunkAPI) => { - const state = thunkAPI.getState() as RootState; - const username = state.auth.user?.username; - if (!username) { - throw new Error('User is not logged in'); - } - const { data } = await UserApi.getRcrainfoProfile(username); - const { rcraSites, ...rest } = data; - return { - rcrainfoProfile: { - ...rest, - rcraSites: rcraSites?.reduce((obj, site) => { - return { - ...obj, - [site.epaSiteId]: { epaSiteId: site.epaSiteId, permissions: site.permissions }, - }; - }, {}), - }, - } as ProfileSlice; - } -); - -const profileSlice = createSlice({ - name: 'profile', - initialState, - reducers: { - updateProfile: (state: ProfileSlice, action: PayloadAction) => { - return { - ...state, - ...action.payload, - }; - }, - }, - extraReducers: (builder) => { - builder - .addCase(getHaztrakProfile.pending, (state) => { - return { - ...state, - loading: true, - error: undefined, - }; - }) - .addCase(getHaztrakProfile.fulfilled, (state, action) => { - return { - ...state, - ...action.payload, - loading: false, - error: undefined, - }; - }) - .addCase(getHaztrakProfile.rejected, (state) => { - state.loading = false; - state.error = 'error'; - return state; - }) - .addCase(getRcraProfile.pending, (state) => { - return { - ...state, - loading: true, - error: undefined, - }; - }) - .addCase(getRcraProfile.fulfilled, (state, action) => { - return { - ...state, - ...action.payload, - loading: false, - error: undefined, - }; - }) - .addCase(getRcraProfile.rejected, (state, action) => { - state.loading = false; - // @ts-ignore - state.error = action.payload.error; - return state; - }); - }, -}); - -/** Retrieve a Haztrak site from the users Profile by the site's EPA ID number */ -export const siteByEpaIdSelector = ( - epaId: string | undefined -): ((state: RootState) => RcraSite | undefined) => - createSelector( - (state: { profile: ProfileSlice }) => state.profile.sites, - (sites: Record | undefined) => { - if (!sites) return undefined; - - const siteId = Object.keys(sites).find((key) => sites[key]?.handler.epaSiteId === epaId); - if (!siteId) return undefined; - - const sitePermissions = sites[siteId]; - if (!sitePermissions) return undefined; - - return sitePermissions.handler; - } - ); - -/** Get all sites a user has access to their Haztrak Profile*/ -export const selectHaztrakSites = createSelector( - (state: { profile: ProfileSlice }) => state.profile.sites, - (sites: Record | undefined) => { - if (!sites) return undefined; - - return Object.values(sites).map((site) => site); - } -); - -/** Get all sites a user has access to their Haztrak Profile*/ -export const selectHaztrakSiteEpaIds = createSelector( - (state: { profile: ProfileSlice }) => state.profile.sites, - (sites: Record | undefined) => { - if (!sites) return []; - return Object.values(sites).map((site) => site.handler.epaSiteId); - } -); - -/** select all RCRAInfo sites a user has access to from their RCRAInfo Profile if they're updated it*/ -export const selectRcrainfoSites = createSelector( - (state: { profile: ProfileSlice }) => state.profile.rcrainfoProfile?.rcraSites, - (rcraSites: Record | undefined) => { - if (!rcraSites) return undefined; - - return Object.values(rcraSites).map((site) => site); - } -); - -/** Retrieve a user's RcraProfile from the Redux store. */ -export const selectRcraProfile = createSelector( - (state: RootState) => state, - (state: RootState) => state.profile.rcrainfoProfile -); - -/** Retrieve a user's HaztrakProfile from the Redux store. */ -export const selectHaztrakProfile = createSelector( - (state: RootState) => state, - (state: RootState) => state.profile -); - -export default profileSlice.reducer; -export const { updateProfile } = profileSlice.actions; diff --git a/client/src/store/rootStore.ts b/client/src/store/rootStore.ts index e531ba5e5..8a80e7aeb 100644 --- a/client/src/store/rootStore.ts +++ b/client/src/store/rootStore.ts @@ -1,14 +1,12 @@ import { combineReducers, configureStore } from '@reduxjs/toolkit'; -import userReducers, { login } from './authSlice/auth.slice'; +import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'; +import authReducers from 'store/authSlice/auth.slice'; +import { haztrakApi } from 'store/htApi.slice'; import errorReducers from './errorSlice/error.slice'; -import { haztrakApi } from './haztrakApiSlice'; import notificationReducers from './notificationSlice/notification.slice'; -import profileReducers from './profileSlice/profile.slice'; -import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'; const rootReducer = combineReducers({ - auth: userReducers, - profile: profileReducers, + auth: authReducers, error: errorReducers, notifications: notificationReducers, [haztrakApi.reducerPath]: haztrakApi.reducer, @@ -30,4 +28,4 @@ export const useAppSelector: TypedUseSelectorHook = useSelector; export type AppDispatch = typeof rootStore.dispatch; export type RootState = ReturnType; export type AppStore = ReturnType; -export { rootStore, login, setupStore }; +export { rootStore, setupStore }; diff --git a/client/src/store/userSlice/user.slice.spec.tsx b/client/src/store/userSlice/user.slice.spec.tsx new file mode 100644 index 000000000..40fd90705 --- /dev/null +++ b/client/src/store/userSlice/user.slice.spec.tsx @@ -0,0 +1,60 @@ +import '@testing-library/jest-dom'; +import { waitFor } from '@testing-library/react'; +import { setupServer } from 'msw/node'; +import { useEffect, useState } from 'react'; +import { useGetUserQuery, useUpdateUserMutation } from 'store'; +import { cleanup, renderWithProviders, screen } from 'test-utils'; +import { userApiMocks } from 'test-utils/mock'; +import { afterAll, afterEach, beforeAll, describe, expect, test } from 'vitest'; + +const server = setupServer(...userApiMocks); +afterEach(() => { + cleanup(); +}); +beforeAll(() => server.listen()); +afterAll(() => server.close()); + +const UserQueryComponent = () => { + const [fetchCount, setFetchCount] = useState(0); + const { data, error, isLoading, isFetching } = useGetUserQuery(); + const [updateUser, results] = useUpdateUserMutation(); + + useEffect(() => { + if (isFetching) setFetchCount(fetchCount + 1); + }, [isFetching]); + + if (isLoading || isFetching) { + return
Loading
; + } + if (error) { + return
Error
; + } + if (data) { + return ( +
+

Data

+

username: {data.username}

+

email: {data.email}

+

fetchCount: {fetchCount}

+
+ +
+
+ ); + } +}; + +describe('userApi', () => { + test('get user query', async () => { + renderWithProviders(); + await waitFor(() => screen.getByText('Data')); + expect(screen.queryByText(/username:/i)).toBeInTheDocument(); + }); +}); diff --git a/client/src/store/userSlice/user.slice.ts b/client/src/store/userSlice/user.slice.ts new file mode 100644 index 000000000..3d84ebd4a --- /dev/null +++ b/client/src/store/userSlice/user.slice.ts @@ -0,0 +1,171 @@ +import { HaztrakSite } from 'components/HaztrakSite'; +import { HaztrakUser } from 'store/authSlice/auth.slice'; +import { haztrakApi, TaskResponse } from 'store/htApi.slice'; + +/**The user's RCRAInfo account data stored in the Redux store*/ +export interface ProfileSlice { + user: string | undefined; + rcrainfoProfile?: RcrainfoProfile>; + sites?: Record; + org?: HaztrakProfileOrg | null; + loading?: boolean; + error?: string; +} + +export interface HaztrakProfileOrg { + id: string; + name: string; + rcrainfoIntegrated: boolean; +} + +/** A site a user has access to in RCRAInfo and their module permissions */ +export interface RcrainfoProfileSite { + epaSiteId: string; + permissions: RcrainfoSitePermissions; +} + +export interface HaztrakProfileSite extends HaztrakSite { + permissions: HaztrakSitePermissions; +} + +export type HaztrakModulePermissions = 'viewer' | 'editor' | 'signer'; + +export interface HaztrakSitePermissions { + eManifest: HaztrakModulePermissions; +} + +export interface RcrainfoProfileState + extends RcrainfoProfile> {} + +export interface RcrainfoProfile { + user: string; + rcraAPIID?: string; + rcraUsername?: string; + rcraAPIKey?: string; + apiUser?: boolean; + rcraSites?: T; + phoneNumber?: string; + isLoading?: boolean; + error?: string; +} + +export interface RcrainfoSitePermissions { + siteManagement: boolean; + annualReport: string; + biennialReport: string; + eManifest: string; + WIETS: string; + myRCRAid: string; +} + +export interface LoginRequest { + username: string; + password: string; +} + +export interface LoginResponse { + key: string; +} + +interface HaztrakOrgResponse { + id: string; + name: string; + rcrainfoIntegrated: boolean; +} + +export interface HaztrakProfileResponse { + user: string; + sites: Array<{ + site: HaztrakSite; + eManifest: HaztrakModulePermissions; + }>; + org?: HaztrakOrgResponse; +} + +interface RcrainfoProfileResponse extends RcrainfoProfile> {} + +export const userApi = haztrakApi.injectEndpoints({ + endpoints: (build) => ({ + // Note: build.query + login: build.mutation({ + query: (data) => ({ + url: 'login', + method: 'POST', + data: data, + }), + invalidatesTags: ['auth'], + }), + getUser: build.query({ + query: () => ({ + url: 'user', + method: 'GET', + }), + providesTags: ['user'], + }), + updateUser: build.mutation({ + query: (data) => ({ + url: 'user', + method: 'PUT', + data: data, + }), + invalidatesTags: ['user'], + }), + getProfile: build.query({ + query: () => ({ + url: 'user/profile', + method: 'GET', + }), + providesTags: ['profile'], + transformResponse: (response: HaztrakProfileResponse) => { + const sites = response.sites.reduce((obj, site) => { + return { + ...obj, + [site.site.handler.epaSiteId]: { + ...site.site, + permissions: { eManifest: site.eManifest }, + }, + }; + }, {}); + return { + user: response.user, + org: response.org, + sites: sites, + }; + }, + }), + getRcrainfoProfile: build.query({ + query: (username) => ({ + url: `user/rcrainfo-profile/${username}`, + method: 'GET', + }), + providesTags: ['rcrainfoProfile'], + transformResponse: (response: RcrainfoProfileResponse) => { + const rcraSites = response?.rcraSites; + return { + ...response, + rcraSites: rcraSites?.reduce((obj, site) => { + return { + ...obj, + [site.epaSiteId]: { epaSiteId: site.epaSiteId, permissions: site.permissions }, + }; + }, {}), + }; + }, + }), + updateRcrainfoProfile: build.mutation({ + query: (data) => ({ + url: `user/rcrainfo-profile/${data.username}`, + method: 'PUT', + data: data.data, + }), + invalidatesTags: ['rcrainfoProfile'], + }), + syncRcrainfoProfile: build.mutation({ + query: () => ({ + url: `user/rcrainfo-profile/sync`, + method: 'POST', + }), + invalidatesTags: ['rcrainfoProfile'], + }), + }), +}); diff --git a/client/src/test-utils/fixtures/index.ts b/client/src/test-utils/fixtures/index.ts index 687b20c00..3938bc145 100644 --- a/client/src/test-utils/fixtures/index.ts +++ b/client/src/test-utils/fixtures/index.ts @@ -9,3 +9,4 @@ export { createMockRcrainfoPermissions, } from './mockHandler'; export { createMockManifest } from './mockManifest'; +export { createMockHaztrakUser } from './mockUser'; diff --git a/client/src/test-utils/fixtures/mockUser.ts b/client/src/test-utils/fixtures/mockUser.ts new file mode 100644 index 000000000..ad145e19e --- /dev/null +++ b/client/src/test-utils/fixtures/mockUser.ts @@ -0,0 +1,57 @@ +import { HaztrakProfileOrg, HaztrakUser, RcrainfoProfile, RcrainfoProfileSite } from 'store'; +import { HaztrakProfileResponse } from 'store/userSlice/user.slice'; +import { createMockSite } from 'test-utils/fixtures/mockHandler'; + +export const DEFAULT_HAZTRAK_USER: HaztrakUser = { + username: 'testuser1', + firstName: 'john', + lastName: 'smith', + email: 'test@mail.com', +}; + +export function createMockHaztrakUser(overWrites?: Partial): HaztrakUser { + return { + ...DEFAULT_HAZTRAK_USER, + ...overWrites, + }; +} + +interface RcrainfoProfileResponse extends RcrainfoProfile> {} + +const DEFAULT_RCRAINFO_PROFILE_RESPONSE: RcrainfoProfileResponse = { + user: 'testuser1', + rcraAPIID: 'mockRcraAPIID', + rcraUsername: undefined, + rcraSites: [], + phoneNumber: undefined, + apiUser: true, +}; + +export function createMockRcrainfoProfileResponse( + overWrites?: Partial +): RcrainfoProfileResponse { + return { + ...DEFAULT_RCRAINFO_PROFILE_RESPONSE, + ...overWrites, + }; +} + +export function createMockOrg(overWrites?: Partial): HaztrakProfileOrg { + return { + name: 'mockOrg', + id: 'mockOrgId', + rcrainfoIntegrated: true, + ...overWrites, + }; +} + +export function createMockProfileResponse( + overWrites?: Partial +): HaztrakProfileResponse { + return { + user: DEFAULT_HAZTRAK_USER.username, + org: createMockOrg(), + sites: [{ site: createMockSite(), eManifest: 'signer' }], + ...overWrites, + }; +} diff --git a/client/src/test-utils/index.tsx b/client/src/test-utils/index.ts similarity index 100% rename from client/src/test-utils/index.tsx rename to client/src/test-utils/index.ts diff --git a/client/src/test-utils/mock/browser.js b/client/src/test-utils/mock/browser.js deleted file mode 100644 index b3e2bd2f8..000000000 --- a/client/src/test-utils/mock/browser.js +++ /dev/null @@ -1,4 +0,0 @@ -// src/test/browser.js - -// This configures a Service Worker with the given request handlers. -export const worker = setupWorker(...handlers); diff --git a/client/src/test-utils/mock/handlers.ts b/client/src/test-utils/mock/htApiMocks.ts similarity index 58% rename from client/src/test-utils/mock/handlers.ts rename to client/src/test-utils/mock/htApiMocks.ts index ae638dc95..a17dcfe9b 100644 --- a/client/src/test-utils/mock/handlers.ts +++ b/client/src/test-utils/mock/htApiMocks.ts @@ -4,39 +4,9 @@ import { createMockHandler, createMockManifest, createMockSite } from '../fixtur export const API_BASE_URL = import.meta.env.VITE_HT_API_URL; const mockMTN = createMockManifest().manifestTrackingNumber; const mockEpaId = createMockHandler().epaSiteId; -const mockUsername = 'testuser1'; const mockSites = [createMockSite(), createMockSite()]; -export const handlers = [ - /** Login endpoint*/ - http.post(`${API_BASE_URL}/api/user/login`, (info) => { - // Persist user's authentication in the session - sessionStorage.setItem('token', 'this_is_a_fake_token'); - sessionStorage.setItem('user', mockUsername); - - // Mock response from haztrak API - return HttpResponse.json( - { - token: 'fake_token', - user: mockUsername, - }, - { status: 200 } - ); - }), - /** User RcraProfile data*/ - http.get(`${API_BASE_URL}/api/rcra/profile/${mockUsername}`, (info) => { - return HttpResponse.json( - { - user: mockUsername, - rcraAPIID: 'mockRcraAPIID', - rcraUsername: undefined, - epaSites: [], - phoneNumber: undefined, - apiUser: true, - }, - { status: 200 } - ); - }), +export const htApiMocks = [ /** List user sites*/ http.get(`${API_BASE_URL}/api/site`, (info) => { return HttpResponse.json(mockSites, { status: 200 }); diff --git a/client/src/test-utils/mock/index.ts b/client/src/test-utils/mock/index.ts new file mode 100644 index 000000000..720f3980a --- /dev/null +++ b/client/src/test-utils/mock/index.ts @@ -0,0 +1,2 @@ +export { userApiMocks } from './userApiMocks'; +export { htApiMocks } from './htApiMocks'; diff --git a/client/src/test-utils/mock/userApiMocks.ts b/client/src/test-utils/mock/userApiMocks.ts new file mode 100644 index 000000000..eaf852d3e --- /dev/null +++ b/client/src/test-utils/mock/userApiMocks.ts @@ -0,0 +1,48 @@ +import { http, HttpResponse } from 'msw'; +import { HaztrakUser } from 'store/authSlice/auth.slice'; +import { LoginResponse } from 'store/userSlice/user.slice'; +import { createMockHaztrakUser } from 'test-utils/fixtures'; +import { + createMockProfileResponse, + createMockRcrainfoProfileResponse, +} from 'test-utils/fixtures/mockUser'; + +/** mock Rest API*/ +const API_BASE_URL = import.meta.env.VITE_HT_API_URL; +export const userApiMocks = [ + /** GET User */ + http.get(`${API_BASE_URL}/api/user`, () => { + return HttpResponse.json({ ...createMockHaztrakUser() }, { status: 200 }); + }), + /** Update User */ + http.put(`${API_BASE_URL}/api/user`, (info) => { + const user: HaztrakUser = { ...createMockHaztrakUser() }; + return HttpResponse.json({ ...user, ...info.request.body }, { status: 200 }); + }), + /** GET Profile */ + http.get(`${API_BASE_URL}/api/user/profile`, () => { + return HttpResponse.json({ ...createMockProfileResponse() }, { status: 200 }); + }), + /** Login */ + http.post(`${API_BASE_URL}/api/user/login`, (info) => { + const body: LoginResponse = { key: 'mockToken' }; + return HttpResponse.json( + { + ...body, + }, + { status: 200 } + ); + }), + /** GET RCRAInfo profile */ + http.get(`${API_BASE_URL}/api/user/rcrainfo-profile/:username`, (info) => { + const { username } = info.params; + // @ts-ignore + const rcrainfoProfile = createMockRcrainfoProfileResponse({ user: username ?? '' }); + return HttpResponse.json( + { + ...rcrainfoProfile, + }, + { status: 200 } + ); + }), +]; diff --git a/server/.gitignore b/server/.gitignore index c6203673c..57b196ba5 100644 --- a/server/.gitignore +++ b/server/.gitignore @@ -3,3 +3,4 @@ test_db static/ .mypy_cache test_db-journal +**/*coverage.json diff --git a/server/Dockerfile b/server/Dockerfile index 46a9f7ffa..09762873c 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -1,9 +1,10 @@ # Builder -FROM python:3.12.0-alpine3.18 as builder +FROM python:3.12.1-alpine3.18 as builder LABEL maintainer="graham.david@epa.gov" ENV APP_DIRECTORY=/app/ ENV PYTHONDONTWRITEBYTECODE 1 ENV PYTHONUNBUFFERED 1 +RUN apk add libffi-dev gcc libc-dev WORKDIR $APP_DIRECTORY COPY requirements.txt ./ RUN mkdir $APP_DIRECTORY/static diff --git a/server/apps/conftest.py b/server/apps/conftest.py index 92de5d7dc..ff01160b9 100644 --- a/server/apps/conftest.py +++ b/server/apps/conftest.py @@ -13,15 +13,15 @@ from faker.providers import BaseProvider from rest_framework.test import APIClient -from apps.core.models import HaztrakProfile, HaztrakUser, RcraProfile -from apps.sites.models import ( +from apps.core.models import HaztrakProfile, HaztrakUser, RcrainfoProfile +from apps.site.models import ( Address, Contact, HaztrakSite, RcraPhone, RcraSite, ) -from apps.sites.models.site_models import HaztrakOrg, SitePermissions +from apps.site.models.site_models import HaztrakOrg, SitePermissions from apps.trak.models import ManifestPhone @@ -80,14 +80,14 @@ def create_user( @pytest.fixture def rcra_profile_factory(db, user_factory, faker: Faker): - """Abstract factory for Haztrak RcraProfile model""" + """Abstract factory for Haztrak RcrainfoProfile model""" def create_profile( rcra_api_id: Optional[str] = str(faker.uuid4()), rcra_api_key: Optional[str] = faker.pystr(min_chars=15), rcra_username: Optional[str] = faker.pystr(min_chars=12), - ) -> RcraProfile: - return RcraProfile.objects.create( + ) -> RcrainfoProfile: + return RcrainfoProfile.objects.create( rcra_api_id=rcra_api_id, rcra_api_key=rcra_api_key, rcra_username=rcra_username, @@ -98,11 +98,11 @@ def create_profile( @pytest.fixture def haztrak_profile_factory(db, user_factory, rcra_profile_factory, haztrak_org_factory): - """Abstract factory for Haztrak RcraProfile model""" + """Abstract factory for Haztrak RcrainfoProfile model""" def create_profile( user: Optional[User] = None, - rcrainfo_profile: Optional[RcraProfile] = rcra_profile_factory(), + rcrainfo_profile: Optional[RcrainfoProfile] = rcra_profile_factory(), org: Optional[HaztrakOrg] = haztrak_org_factory(), ) -> HaztrakProfile: return HaztrakProfile.objects.create( @@ -329,7 +329,7 @@ def user_with_org_factory( def create_fixtures( user: Optional[User] = None, org: Optional[HaztrakOrg] = None, - admin_rcrainfo_profile: Optional[RcraProfile] = None, + admin_rcrainfo_profile: Optional[RcrainfoProfile] = None, is_rcrainfo_enabled: Optional[bool] = True, ): if is_rcrainfo_enabled: diff --git a/server/apps/core/admin.py b/server/apps/core/admin.py index ed604ba09..a9dd05d7b 100644 --- a/server/apps/core/admin.py +++ b/server/apps/core/admin.py @@ -3,8 +3,8 @@ from django.urls import reverse from django.utils.html import format_html, urlencode -from ..sites.models import RcraSitePermissions, SitePermissions -from .models import HaztrakProfile, HaztrakUser, RcraProfile +from ..site.models import RcraSitePermissions, SitePermissions +from .models import HaztrakProfile, HaztrakUser, RcrainfoProfile class HiddenListView(admin.ModelAdmin): @@ -25,7 +25,7 @@ class HaztrakUserAdmin(UserAdmin): @admin.display(description="Profile") def related_profile(self, user: HaztrakUser) -> str: url = ( - reverse("admin:core_haztrakprofile_changelist") + "?" + urlencode({"user": str(user)}) # noqa E501 + reverse("admin:core_haztrakprofile_changelist") + "?" + urlencode({"user": str(user)}) # noqa E501 ) return format_html("{}", url, user.haztrak_profile) @@ -70,7 +70,7 @@ def number_of_sites(profile: HaztrakProfile) -> str: return str(profile.site_permissions.all().count()) -@admin.register(RcraProfile) +@admin.register(RcrainfoProfile) class RcraProfileAdmin(admin.ModelAdmin): list_display = ["__str__", "related_user", "rcra_username", "api_user"] search_fields = ["haztrak_profile__user__username", "rcra_username"] @@ -84,7 +84,7 @@ def related_user(self, user): url = reverse("admin:core_haztrakuser_changelist") + "?" + urlencode({"q": str(user.id)}) return format_html("{}", url, user) - def api_user(self, profile: RcraProfile) -> bool: + def api_user(self, profile: RcrainfoProfile) -> bool: return profile.has_rcrainfo_api_id_key api_user.boolean = True diff --git a/server/apps/core/exceptions.py b/server/apps/core/exceptions.py index f669eda8e..e9cf21679 100644 --- a/server/apps/core/exceptions.py +++ b/server/apps/core/exceptions.py @@ -6,7 +6,7 @@ from rest_framework.serializers import as_serializer_error from rest_framework.views import exception_handler -from apps.sites.services.site_services import HaztrakSiteServiceError +from apps.site.services.site_services import HaztrakSiteServiceError class InternalServer500(APIException): diff --git a/server/apps/core/migrations/0001_initial.py b/server/apps/core/migrations/0001_initial.py index b7af7d12b..78da9c7db 100644 --- a/server/apps/core/migrations/0001_initial.py +++ b/server/apps/core/migrations/0001_initial.py @@ -1,67 +1,129 @@ # Generated by Django 4.2.7 on 2023-11-20 16:47 +import uuid + import django.contrib.auth.models import django.contrib.auth.validators -from django.db import migrations, models import django.utils.timezone -import uuid +from django.db import migrations, models class Migration(migrations.Migration): - initial = True - dependencies = [ - ] + dependencies = [] operations = [ migrations.CreateModel( - name='HaztrakUser', + name="HaztrakUser", fields=[ - ('password', models.CharField(max_length=128, verbose_name='password')), - ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), - ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), - ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), - ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), - ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), - ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), - ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), - ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), - ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ("password", models.CharField(max_length=128, verbose_name="password")), + ( + "last_login", + models.DateTimeField(blank=True, null=True, verbose_name="last login"), + ), + ( + "is_superuser", + models.BooleanField( + default=False, + help_text="Designates that this user has all permissions without explicitly assigning them.", + verbose_name="superuser status", + ), + ), + ( + "username", + models.CharField( + error_messages={"unique": "A user with that username already exists."}, + help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.", + max_length=150, + unique=True, + validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], + verbose_name="username", + ), + ), + ( + "first_name", + models.CharField(blank=True, max_length=150, verbose_name="first name"), + ), + ( + "last_name", + models.CharField(blank=True, max_length=150, verbose_name="last name"), + ), + ( + "email", + models.EmailField(blank=True, max_length=254, verbose_name="email address"), + ), + ( + "is_staff", + models.BooleanField( + default=False, + help_text="Designates whether the user can log into this admin site.", + verbose_name="staff status", + ), + ), + ( + "is_active", + models.BooleanField( + default=True, + help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.", + verbose_name="active", + ), + ), + ( + "date_joined", + models.DateTimeField( + default=django.utils.timezone.now, verbose_name="date joined" + ), + ), + ( + "id", + models.UUIDField( + default=uuid.uuid4, editable=False, primary_key=True, serialize=False + ), + ), ], options={ - 'verbose_name': 'User', - 'verbose_name_plural': 'Users', - 'ordering': ['username'], + "verbose_name": "User", + "verbose_name_plural": "Users", + "ordering": ["username"], }, managers=[ - ('objects', django.contrib.auth.models.UserManager()), + ("objects", django.contrib.auth.models.UserManager()), ], ), migrations.CreateModel( - name='HaztrakProfile', + name="HaztrakProfile", fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ( + "id", + models.UUIDField( + default=uuid.uuid4, editable=False, primary_key=True, serialize=False + ), + ), ], options={ - 'verbose_name': 'Haztrak Profile', - 'ordering': ['user__username'], - 'default_related_name': 'haztrak_profile', + "verbose_name": "Haztrak Profile", + "ordering": ["user__username"], + "default_related_name": "haztrak_profile", }, ), migrations.CreateModel( - name='RcraProfile', + name="RcrainfoProfile", fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('rcra_api_key', models.CharField(blank=True, max_length=128, null=True)), - ('rcra_api_id', models.CharField(blank=True, max_length=128, null=True)), - ('rcra_username', models.CharField(blank=True, max_length=128, null=True)), - ('phone_number', models.CharField(blank=True, max_length=15, null=True)), - ('email', models.EmailField(max_length=254)), + ( + "id", + models.UUIDField( + default=uuid.uuid4, editable=False, primary_key=True, serialize=False + ), + ), + ("rcra_api_key", models.CharField(blank=True, max_length=128, null=True)), + ("rcra_api_id", models.CharField(blank=True, max_length=128, null=True)), + ("rcra_username", models.CharField(blank=True, max_length=128, null=True)), + ("phone_number", models.CharField(blank=True, max_length=15, null=True)), + ("email", models.EmailField(max_length=254)), ], options={ - 'ordering': ['rcra_username'], + "ordering": ["rcra_username"], }, ), ] diff --git a/server/apps/core/migrations/0002_initial.py b/server/apps/core/migrations/0002_initial.py index 75eedc2d8..a320f9249 100644 --- a/server/apps/core/migrations/0002_initial.py +++ b/server/apps/core/migrations/0002_initial.py @@ -1,44 +1,73 @@ # Generated by Django 4.2.7 on 2023-11-20 16:47 +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion class Migration(migrations.Migration): - initial = True dependencies = [ - ('sites', '0001_initial'), - ('core', '0001_initial'), - ('auth', '0012_alter_user_first_name_max_length'), + ("site", "0001_initial"), + ("core", "0001_initial"), + ("auth", "0012_alter_user_first_name_max_length"), ] operations = [ migrations.AddField( - model_name='haztrakprofile', - name='org', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='haztrak_profiles', to='sites.haztrakorg'), + model_name="haztrakprofile", + name="org", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="haztrak_profiles", + to="site.haztrakorg", + ), ), migrations.AddField( - model_name='haztrakprofile', - name='rcrainfo_profile', - field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='haztrak_profile', to='core.rcraprofile'), + model_name="haztrakprofile", + name="rcrainfo_profile", + field=models.OneToOneField( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="haztrak_profile", + to="core.rcrainfoprofile", + ), ), migrations.AddField( - model_name='haztrakprofile', - name='user', - field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='haztrak_profile', to=settings.AUTH_USER_MODEL), + model_name="haztrakprofile", + name="user", + field=models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="haztrak_profile", + to=settings.AUTH_USER_MODEL, + ), ), migrations.AddField( - model_name='haztrakuser', - name='groups', - field=models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups'), + model_name="haztrakuser", + name="groups", + field=models.ManyToManyField( + blank=True, + help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.", + related_name="user_set", + related_query_name="user", + to="auth.group", + verbose_name="groups", + ), ), migrations.AddField( - model_name='haztrakuser', - name='user_permissions', - field=models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions'), + model_name="haztrakuser", + name="user_permissions", + field=models.ManyToManyField( + blank=True, + help_text="Specific permissions for this user.", + related_name="user_set", + related_query_name="user", + to="auth.permission", + verbose_name="user permissions", + ), ), ] diff --git a/server/apps/core/models.py b/server/apps/core/models.py index bc40b8109..2f4502df3 100644 --- a/server/apps/core/models.py +++ b/server/apps/core/models.py @@ -38,14 +38,14 @@ class Meta: related_name="haztrak_profile", ) rcrainfo_profile = models.OneToOneField( - "RcraProfile", + "core.RcrainfoProfile", on_delete=models.SET_NULL, related_name="haztrak_profile", null=True, blank=True, ) org = models.ForeignKey( - "sites.HaztrakOrg", + "site.HaztrakOrg", on_delete=models.SET_NULL, related_name="haztrak_profiles", null=True, @@ -61,9 +61,9 @@ def __str__(self): return f"{self.user.username}" -class RcraProfile(models.Model): +class RcrainfoProfile(models.Model): """ - Contains a user's RcraProfile information, such as username, and API credentials. + Contains a user's RcrainfoProfile information, such as username, and API credentials. Has a one-to-one relationship with the User model. """ diff --git a/server/apps/core/serializers.py b/server/apps/core/serializers.py index 91da83b89..34d9f9f53 100644 --- a/server/apps/core/serializers.py +++ b/server/apps/core/serializers.py @@ -4,9 +4,9 @@ from rest_framework import serializers from rest_framework.serializers import ModelSerializer -from apps.core.models import HaztrakProfile, HaztrakUser, RcraProfile -from apps.sites.serializers import HaztrakOrgSerializer, RcraSitePermissionSerializer -from apps.sites.serializers.profile_serializer import SitePermissionSerializer +from apps.core.models import HaztrakProfile, HaztrakUser, RcrainfoProfile +from apps.site.serializers import HaztrakOrgSerializer, RcraSitePermissionSerializer +from apps.site.serializers.profile_serializer import SitePermissionSerializer class HaztrakUserSerializer(ModelSerializer): @@ -14,6 +14,7 @@ class HaztrakUserSerializer(ModelSerializer): Model serializer for marshalling/unmarshalling a user's HaztrakUser """ + id = serializers.CharField(source="pk") username = serializers.CharField( required=False, ) @@ -29,6 +30,7 @@ class HaztrakUserSerializer(ModelSerializer): class Meta: model = HaztrakUser fields = [ + "id", "username", "firstName", "lastName", @@ -57,8 +59,8 @@ class Meta: ] -class RcraProfileSerializer(ModelSerializer): - """Model serializer for marshalling/unmarshalling a user's RcraProfile""" +class RcrainfoProfileSerializer(ModelSerializer): + """Model serializer for marshalling/unmarshalling a user's RcrainfoProfile""" user = serializers.StringRelatedField( source="haztrak_profile", @@ -75,11 +77,15 @@ class RcraProfileSerializer(ModelSerializer): rcraAPIID = serializers.CharField( source="rcra_api_id", required=False, + allow_null=True, + allow_blank=True, ) rcraAPIKey = serializers.CharField( source="rcra_api_key", required=False, write_only=True, + allow_blank=True, + allow_null=True, ) rcraUsername = serializers.CharField( source="rcra_username", @@ -92,7 +98,7 @@ class RcraProfileSerializer(ModelSerializer): ) class Meta: - model = RcraProfile + model = RcrainfoProfile fields = [ "user", "rcraAPIID", diff --git a/server/apps/core/services/profile_service.py b/server/apps/core/services/profile_service.py index f74c88951..a6eff5e43 100644 --- a/server/apps/core/services/profile_service.py +++ b/server/apps/core/services/profile_service.py @@ -1,4 +1,4 @@ -"""business logic related to a user's Haztrak profile (note: not their RcraProfile)""" +"""business logic related to a user's Haztrak profile (note: not their RcrainfoProfile)""" from django.db import transaction from apps.core.models import HaztrakProfile, HaztrakUser diff --git a/server/apps/core/services/rcrainfo_service.py b/server/apps/core/services/rcrainfo_service.py index 92d90ca30..393e9bcc9 100644 --- a/server/apps/core/services/rcrainfo_service.py +++ b/server/apps/core/services/rcrainfo_service.py @@ -5,8 +5,8 @@ from django.db import IntegrityError from emanifest import RcrainfoClient, RcrainfoResponse # type: ignore -from apps.core.models import RcraProfile # type: ignore -from apps.sites.models.site_models import HaztrakOrg +from apps.core.models import RcrainfoProfile # type: ignore +from apps.site.models.site_models import HaztrakOrg from apps.trak.models import WasteCode # type: ignore logger = logging.getLogger(__name__) @@ -23,13 +23,13 @@ class RcrainfoService(RcrainfoClient): def __init__( self, *, - rcra_profile: Optional[RcraProfile] = None, + rcra_profile: Optional[RcrainfoProfile] = None, api_id: Optional[str] = None, api_key: Optional[str] = None, rcrainfo_env: Optional[Literal["preprod"] | Literal["prod"]] = None, **kwargs, ): - self.profile: RcraProfile | None = rcra_profile + self.profile: RcrainfoProfile | None = rcra_profile self.api_id: str | None = api_id self.api_key: str | None = api_key self.rcrainfo_env: str = rcrainfo_env or "preprod" @@ -67,7 +67,7 @@ def get_user_rcrainfo_profile( """ Retrieve a user's site permissions from RCRAInfo, It expects the haztrak user to have their unique RCRAInfo user and API credentials in their - RcraProfile + RcrainfoProfile """ return self.search_users(userId=rcrainfo_username) diff --git a/server/apps/core/tests/conftest.py b/server/apps/core/tests/conftest.py index 17c2c236a..b05c34202 100644 --- a/server/apps/core/tests/conftest.py +++ b/server/apps/core/tests/conftest.py @@ -4,7 +4,7 @@ import pytest from faker import Faker -from apps.sites.models import RcraSiteType +from apps.site.models import RcraSiteType @pytest.fixture diff --git a/server/apps/core/tests/test_models.py b/server/apps/core/tests/test_models.py index d71df4997..4d15a1b37 100644 --- a/server/apps/core/tests/test_models.py +++ b/server/apps/core/tests/test_models.py @@ -1,16 +1,16 @@ import pytest -from apps.core.models import HaztrakProfile, RcraProfile +from apps.core.models import HaztrakProfile, RcrainfoProfile @pytest.mark.django_db -class TestRcraProfileModel: - """Test related to the RcraProfile model and its API""" +class TestRcrainfoProfileModel: + """Test related to the RcrainfoProfile model and its API""" def test_rcra_profile_factory(self, rcra_profile_factory): """simply check the model saves given our factory's defaults""" rcra_profile = rcra_profile_factory() - assert isinstance(rcra_profile, RcraProfile) + assert isinstance(rcra_profile, RcrainfoProfile) @pytest.mark.parametrize("rcra_api_id", ["id", None]) @pytest.mark.parametrize("rcra_api_key", ["key", None]) diff --git a/server/apps/core/tests/test_rcra_profile_views.py b/server/apps/core/tests/test_rcrainfo_profile_views.py similarity index 81% rename from server/apps/core/tests/test_rcra_profile_views.py rename to server/apps/core/tests/test_rcrainfo_profile_views.py index 59da1fb6f..a3d3d3d95 100644 --- a/server/apps/core/tests/test_rcra_profile_views.py +++ b/server/apps/core/tests/test_rcrainfo_profile_views.py @@ -1,15 +1,15 @@ from rest_framework import status from rest_framework.test import APIRequestFactory, force_authenticate -from apps.core.views import RcraProfileView # type: ignore +from apps.core.views import RcrainfoProfileView # type: ignore -class TestRcraProfileView: +class TestRcrainfoProfileView: """ - Tests the for the endpoints related to the user's RcraProfile + Tests the for the endpoints related to the user's RcrainfoProfile """ - URL = "/api/" + URL = "/api/user/" id_field = "rcraAPIID" key_field = "rcraAPIKey" username_field = "rcraUsername" @@ -26,7 +26,7 @@ def test_returns_a_user_profile( rcra_profile = rcra_profile_factory() haztrak_profile_factory(user=user, rcrainfo_profile=rcra_profile) # Act - response = client.get(f"{self.URL}rcra/profile/{user.username}") + response = client.get(f"{self.URL}rcrainfo-profile/{user.username}") # Assert assert response.headers["Content-Type"] == "application/json" assert response.status_code == status.HTTP_200_OK @@ -40,7 +40,7 @@ def test_rcra_profile_updates( haztrak_profile_factory(user=user, rcrainfo_profile=rcra_profile) factory = APIRequestFactory() request = factory.put( - f"{self.URL}rcra/profile/{user.username}", + f"{self.URL}rcrainfo-profile/{user.username}", { self.id_field: rcra_profile.rcra_api_id, self.username_field: user.username, @@ -50,7 +50,7 @@ def test_rcra_profile_updates( ) force_authenticate(request, user) # Act - response = RcraProfileView.as_view()(request, username=user.username) + response = RcrainfoProfileView.as_view()(request, username=user.username) assert response.data[self.id_field] == rcra_profile.rcra_api_id assert response.data[self.username_field] == user.username @@ -63,7 +63,7 @@ def test_update_does_not_return_api_key( profile = haztrak_profile_factory(user=user, rcrainfo_profile=rcra_profile) factory = APIRequestFactory() request = factory.put( - f"{self.URL}rcra/profile/{user.username}", + f"{self.URL}rcrainfo-profile/{user.username}", { self.id_field: rcra_profile.rcra_api_id, self.username_field: user.username, @@ -73,7 +73,7 @@ def test_update_does_not_return_api_key( ) force_authenticate(request, user) # Act - response = RcraProfileView.as_view()(request, username=profile.user.username) + response = RcrainfoProfileView.as_view()(request, username=profile.user.username) # Assert assert self.key_field not in response.data assert self.id_field in response.data diff --git a/server/apps/core/tests/test_rcrainfo_service.py b/server/apps/core/tests/test_rcrainfo_service.py index df8958114..1e97ca7ad 100644 --- a/server/apps/core/tests/test_rcrainfo_service.py +++ b/server/apps/core/tests/test_rcrainfo_service.py @@ -1,7 +1,6 @@ -from datetime import datetime, timezone +from datetime import UTC, datetime import emanifest -import pytest from responses import matchers from rest_framework import status @@ -72,7 +71,7 @@ class TestQuickerSign: printed_name = "David Graham" site_id = "VATESTGEN001" site_type = "Generator" - sign_date = datetime.utcnow().replace(tzinfo=timezone.utc) + sign_date = datetime.now(UTC) def test_maps_keywords( self, diff --git a/server/apps/core/urls.py b/server/apps/core/urls.py index fb24210d6..c5932c574 100644 --- a/server/apps/core/urls.py +++ b/server/apps/core/urls.py @@ -1,29 +1,28 @@ +from dj_rest_auth.views import LoginView, LogoutView, UserDetailsView from django.urls import include, path from .views import ( # type: ignore HaztrakProfileView, - HaztrakUserView, LaunchExampleTaskView, - Login, - RcraProfileView, - SyncRcraProfileView, + RcrainfoProfileView, + SyncRcrainfoProfileView, TaskStatusView, ) urlpatterns = [ - # Rcra Profile + path("task/example", LaunchExampleTaskView.as_view()), + path("task/", TaskStatusView.as_view()), path( - "rcra/", + "user", include( [ - path("profile/sync", SyncRcraProfileView.as_view()), - path("profile/", RcraProfileView.as_view()), + path("", UserDetailsView.as_view(), name="rest_user_details"), + path("/login", LoginView.as_view(), name="rest_login"), + path("/logout", LogoutView.as_view(), name="rest_logout"), + path("/profile", HaztrakProfileView.as_view()), + path("/rcrainfo-profile/sync", SyncRcrainfoProfileView.as_view()), + path("/rcrainfo-profile/", RcrainfoProfileView.as_view()), ] ), ), - path("profile", HaztrakProfileView.as_view()), - path("user", HaztrakUserView.as_view()), - path("user/login", Login.as_view()), - path("task/example", LaunchExampleTaskView.as_view()), - path("task/", TaskStatusView.as_view()), ] diff --git a/server/apps/core/views/__init__.py b/server/apps/core/views/__init__.py index 10e446c99..b7da1e2d0 100644 --- a/server/apps/core/views/__init__.py +++ b/server/apps/core/views/__init__.py @@ -1,8 +1,6 @@ -from .auth_view import Login from .profile_views import ( HaztrakProfileView, - HaztrakUserView, - RcraProfileView, - SyncRcraProfileView, + RcrainfoProfileView, + SyncRcrainfoProfileView, ) from .task_views import LaunchExampleTaskView, TaskStatusView diff --git a/server/apps/core/views/auth_view.py b/server/apps/core/views/auth_view.py deleted file mode 100644 index bb9dccb43..000000000 --- a/server/apps/core/views/auth_view.py +++ /dev/null @@ -1,12 +0,0 @@ -from rest_framework.authtoken.models import Token -from rest_framework.authtoken.views import ObtainAuthToken -from rest_framework.response import Response - - -class Login(ObtainAuthToken): - def post(self, request, *args, **kwargs): - serializer = self.serializer_class(data=request.data, context={"request": request}) - serializer.is_valid(raise_exception=True) - user = serializer.validated_data["user"] - token, created = Token.objects.get_or_create(user=user) - return Response({"token": token.key, "user": str(user)}) diff --git a/server/apps/core/views/profile_views.py b/server/apps/core/views/profile_views.py index 1d8350675..ef1768297 100644 --- a/server/apps/core/views/profile_views.py +++ b/server/apps/core/views/profile_views.py @@ -5,23 +5,12 @@ from rest_framework.request import Request from rest_framework.response import Response -from apps.core.models import HaztrakProfile, HaztrakUser, RcraProfile +from apps.core.models import HaztrakProfile, RcrainfoProfile from apps.core.serializers import ( HaztrakProfileSerializer, - HaztrakUserSerializer, - RcraProfileSerializer, + RcrainfoProfileSerializer, ) -from apps.sites.tasks import sync_user_rcrainfo_sites - - -class HaztrakUserView(RetrieveUpdateAPIView): - """Retrieve the current user's base information""" - - queryset = HaztrakUser.objects.all() - serializer_class = HaztrakUserSerializer - - def get_object(self): - return self.request.user +from apps.site.tasks import sync_user_rcrainfo_sites class HaztrakProfileView(RetrieveAPIView): @@ -35,21 +24,21 @@ def get_object(self): return HaztrakProfile.objects.get(user=self.request.user) -class RcraProfileView(RetrieveUpdateAPIView): +class RcrainfoProfileView(RetrieveUpdateAPIView): """ - Responsible for Create/Update operations related to the user RcraProfile, + Responsible for Create/Update operations related to the user RcrainfoProfile, which maintains a user's RCRAInfo profile data. This info is necessary for actions that interface with RCRAInfo. """ - queryset = RcraProfile.objects.all() - serializer_class = RcraProfileSerializer + queryset = RcrainfoProfile.objects.all() + serializer_class = RcrainfoProfileSerializer response = Response lookup_field = "haztrak_profile__user__username" lookup_url_kwarg = "username" -class SyncRcraProfileView(GenericAPIView): +class SyncRcrainfoProfileView(GenericAPIView): """ This endpoint launches a task to sync the logged-in user's RCRAInfo profile with their haztrak (Rcra)profile. @@ -58,11 +47,11 @@ class SyncRcraProfileView(GenericAPIView): queryset = None response = Response - def get(self, request: Request) -> Response: + def post(self, request: Request) -> Response: try: task: CeleryTask = sync_user_rcrainfo_sites.delay(str(self.request.user)) return self.response({"taskId": task.id}) - except RcraProfile.DoesNotExist as exc: + except RcrainfoProfile.DoesNotExist as exc: return self.response(data={"error": str(exc)}, status=status.HTTP_404_NOT_FOUND) except CeleryError as exc: return self.response( diff --git a/server/apps/sites/README.md b/server/apps/site/README.md similarity index 100% rename from server/apps/sites/README.md rename to server/apps/site/README.md diff --git a/server/apps/sites/__init__.py b/server/apps/site/__init__.py similarity index 100% rename from server/apps/sites/__init__.py rename to server/apps/site/__init__.py diff --git a/server/apps/sites/admin.py b/server/apps/site/admin.py similarity index 96% rename from server/apps/sites/admin.py rename to server/apps/site/admin.py index f64a82f4e..64a7a1526 100644 --- a/server/apps/sites/admin.py +++ b/server/apps/site/admin.py @@ -4,13 +4,13 @@ from apps.core.admin import HiddenListView from apps.core.models import HaztrakProfile -from apps.sites.models import ( +from apps.site.models import ( Address, Contact, HaztrakSite, RcraSite, ) -from apps.sites.models.site_models import HaztrakOrg +from apps.site.models.site_models import HaztrakOrg @admin.register(RcraSite) diff --git a/server/apps/sites/apps.py b/server/apps/site/apps.py similarity index 83% rename from server/apps/sites/apps.py rename to server/apps/site/apps.py index 2fe0c831b..263ff00dd 100644 --- a/server/apps/sites/apps.py +++ b/server/apps/site/apps.py @@ -3,4 +3,4 @@ class SitesConfig(AppConfig): default_auto_field = "django.db.models.BigAutoField" - name = "apps.sites" + name = "apps.site" diff --git a/server/apps/site/migrations/0001_initial.py b/server/apps/site/migrations/0001_initial.py new file mode 100644 index 000000000..f2a3f85c1 --- /dev/null +++ b/server/apps/site/migrations/0001_initial.py @@ -0,0 +1,479 @@ +# Generated by Django 4.2.7 on 2023-11-20 16:47 + +import uuid + +import django.core.validators +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + +import apps.site.models.contact_models + + +class Migration(migrations.Migration): + initial = True + + dependencies = [ + ("core", "0001_initial"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="Address", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ("street_number", models.CharField(blank=True, max_length=12, null=True)), + ("address1", models.CharField(max_length=50, verbose_name="address 1")), + ( + "address2", + models.CharField( + blank=True, + default=None, + max_length=50, + null=True, + verbose_name="address 2", + ), + ), + ("city", models.CharField(blank=True, max_length=25, null=True)), + ( + "state", + models.CharField( + blank=True, + choices=[ + ("AK", "Alaska"), + ("AL", "Alabama"), + ("AP", "Armed Forces Pacific"), + ("AR", "Arkansas"), + ("AZ", "Arizona"), + ("CA", "California"), + ("CO", "Colorado"), + ("CT", "Connecticut"), + ("DC", "Washington DC"), + ("DE", "Delaware"), + ("FL", "Florida"), + ("GA", "Georgia"), + ("GU", "Guam"), + ("HI", "Hawaii"), + ("IA", "Iowa"), + ("ID", "Idaho"), + ("IL", "Illinois"), + ("IN", "Indiana"), + ("KS", "Kansas"), + ("KY", "Kentucky"), + ("LA", "Louisiana"), + ("MA", "Massachusetts"), + ("MD", "Maryland"), + ("ME", "Maine"), + ("MI", "Michigan"), + ("MN", "Minnesota"), + ("MO", "Missouri"), + ("MS", "Mississippi"), + ("MT", "Montana"), + ("NC", "North Carolina"), + ("ND", "North Dakota"), + ("NE", "Nebraska"), + ("NH", "New Hampshire"), + ("NJ", "New Jersey"), + ("NM", "New Mexico"), + ("NV", "Nevada"), + ("NY", "New York"), + ("OH", "Ohio"), + ("OK", "Oklahoma"), + ("OR", "Oregon"), + ("PA", "Pennsylvania"), + ("PR", "Puerto Rico"), + ("RI", "Rhode Island"), + ("SC", "South Carolina"), + ("SD", "South Dakota"), + ("TN", "Tennessee"), + ("TX", "Texas"), + ("UT", "Utah"), + ("VA", "Virginia"), + ("VI", "Virgin Islands"), + ("VT", "Vermont"), + ("WA", "Washington"), + ("WI", "Wisconsin"), + ("WV", "West Virginia"), + ("WY", "Wyoming"), + ("XA", "REGION 01 PURVIEW"), + ("XB", "REGION 02 PURVIEW"), + ("XC", "REGION 03 PURVIEW"), + ("XD", "REGION 04 PURVIEW"), + ("XE", "REGION 05 PURVIEW"), + ("XF", "REGION 06 PURVIEW"), + ("XG", "REGION 07 PURVIEW"), + ("XH", "REGION 08 PURVIEW"), + ("XI", "REGION 09 PURVIEW"), + ("XJ", "REGION 10 PURVIEW"), + ], + max_length=3, + null=True, + ), + ), + ( + "country", + models.CharField( + blank=True, + choices=[("US", "United States"), ("MX", "Mexico"), ("CA", "Canada")], + max_length=3, + null=True, + ), + ), + ("zip", models.CharField(blank=True, max_length=5, null=True)), + ], + options={ + "ordering": ["address1"], + }, + ), + migrations.CreateModel( + name="Contact", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ("first_name", models.CharField(blank=True, max_length=38, null=True)), + ("middle_initial", models.CharField(blank=True, max_length=1, null=True)), + ("last_name", models.CharField(blank=True, max_length=38, null=True)), + ("email", models.EmailField(blank=True, max_length=254, null=True)), + ("company_name", models.CharField(blank=True, max_length=80, null=True)), + ], + options={ + "ordering": ["first_name"], + }, + ), + migrations.CreateModel( + name="HaztrakOrg", + fields=[ + ("name", models.CharField(max_length=200, unique=True)), + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ( + "admin", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "verbose_name": "Organization", + "verbose_name_plural": "Organizations", + "ordering": ["name"], + }, + ), + migrations.CreateModel( + name="HaztrakSite", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ( + "name", + models.CharField( + max_length=200, + validators=[ + django.core.validators.MinLengthValidator( + 2, "site aliases must be longer than 2 characters" + ) + ], + verbose_name="site alias", + ), + ), + ( + "last_rcrainfo_manifest_sync", + models.DateTimeField( + blank=True, null=True, verbose_name="last RCRAInfo manifest sync date" + ), + ), + ( + "org", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="site.haztrakorg" + ), + ), + ], + options={ + "verbose_name": "Haztrak Site", + "verbose_name_plural": "Haztrak Sites", + "ordering": ["rcra_site__epa_id"], + }, + ), + migrations.CreateModel( + name="RcraPhone", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ("number", apps.site.models.contact_models.RcraPhoneNumber(max_length=12)), + ("extension", models.CharField(blank=True, max_length=6, null=True)), + ], + options={ + "ordering": ["number"], + }, + ), + migrations.CreateModel( + name="RcraSite", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ( + "site_type", + models.CharField( + blank=True, + choices=[ + ("Generator", "Generator"), + ("Transporter", "Transporter"), + ("Tsdf", "Tsdf"), + ("Broker", "Broker"), + ], + max_length=20, + null=True, + ), + ), + ( + "epa_id", + models.CharField(max_length=25, unique=True, verbose_name="EPA ID number"), + ), + ("name", models.CharField(max_length=200)), + ("modified", models.BooleanField(blank=True, null=True)), + ("registered", models.BooleanField(blank=True, null=True)), + ( + "gis_primary", + models.BooleanField( + blank=True, default=False, null=True, verbose_name="GIS primary" + ), + ), + ( + "can_esign", + models.BooleanField( + blank=True, null=True, verbose_name="can electronically sign" + ), + ), + ( + "limited_esign", + models.BooleanField( + blank=True, null=True, verbose_name="limited electronic signing ability" + ), + ), + ( + "registered_emanifest_user", + models.BooleanField( + blank=True, + default=False, + null=True, + verbose_name="has registered e-manifest user", + ), + ), + ( + "contact", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="site.contact", + verbose_name="contact information", + ), + ), + ( + "emergency_phone", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="site.rcraphone", + ), + ), + ( + "mail_address", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="mail_address", + to="site.address", + ), + ), + ( + "site_address", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="site_address", + to="site.address", + ), + ), + ], + options={ + "verbose_name": "RCRAInfo Site", + "verbose_name_plural": "RCRAInfo Sites", + "ordering": ["epa_id"], + }, + ), + migrations.CreateModel( + name="SitePermissions", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ( + "emanifest", + models.CharField( + choices=[("viewer", "view"), ("editor", "edit"), ("signer", "sign")], + default="view", + max_length=6, + ), + ), + ( + "profile", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="site_permissions", + to="core.haztrakprofile", + ), + ), + ( + "site", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="site.haztraksite" + ), + ), + ], + options={ + "verbose_name": "Site Permission", + "verbose_name_plural": "Site Permissions", + "ordering": ["profile"], + }, + ), + migrations.CreateModel( + name="RcraSitePermissions", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ("site_manager", models.BooleanField(default=False)), + ( + "annual_report", + models.CharField( + choices=[ + ("Certifier", "Certifier"), + ("Preparer", "Preparer"), + ("Viewer", "Viewer"), + ], + max_length=12, + ), + ), + ( + "biennial_report", + models.CharField( + choices=[ + ("Certifier", "Certifier"), + ("Preparer", "Preparer"), + ("Viewer", "Viewer"), + ], + max_length=12, + ), + ), + ( + "e_manifest", + models.CharField( + choices=[ + ("Certifier", "Certifier"), + ("Preparer", "Preparer"), + ("Viewer", "Viewer"), + ], + max_length=12, + ), + ), + ( + "my_rcra_id", + models.CharField( + choices=[ + ("Certifier", "Certifier"), + ("Preparer", "Preparer"), + ("Viewer", "Viewer"), + ], + max_length=12, + ), + ), + ( + "wiets", + models.CharField( + choices=[ + ("Certifier", "Certifier"), + ("Preparer", "Preparer"), + ("Viewer", "Viewer"), + ], + max_length=12, + ), + ), + ( + "profile", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="permissions", + to="core.rcrainfoprofile", + ), + ), + ( + "site", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="site.rcrasite" + ), + ), + ], + options={ + "verbose_name": "RCRAInfo Permission", + "verbose_name_plural": "RCRAInfo Permissions", + "ordering": ["site__epa_id"], + }, + ), + migrations.AddField( + model_name="haztraksite", + name="rcra_site", + field=models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + to="site.rcrasite", + verbose_name="rcra_site", + ), + ), + migrations.AddField( + model_name="contact", + name="phone", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="site.rcraphone", + ), + ), + ] diff --git a/server/apps/sites/migrations/__init__.py b/server/apps/site/migrations/__init__.py similarity index 100% rename from server/apps/sites/migrations/__init__.py rename to server/apps/site/migrations/__init__.py diff --git a/server/apps/sites/models/__init__.py b/server/apps/site/models/__init__.py similarity index 100% rename from server/apps/sites/models/__init__.py rename to server/apps/site/models/__init__.py diff --git a/server/apps/sites/models/base_models.py b/server/apps/site/models/base_models.py similarity index 100% rename from server/apps/sites/models/base_models.py rename to server/apps/site/models/base_models.py diff --git a/server/apps/sites/models/contact_models.py b/server/apps/site/models/contact_models.py similarity index 100% rename from server/apps/sites/models/contact_models.py rename to server/apps/site/models/contact_models.py diff --git a/server/apps/sites/models/site_models.py b/server/apps/site/models/site_models.py similarity index 95% rename from server/apps/sites/models/site_models.py rename to server/apps/site/models/site_models.py index 3579386ba..5109208a7 100644 --- a/server/apps/sites/models/site_models.py +++ b/server/apps/site/models/site_models.py @@ -7,10 +7,10 @@ from django.db import models from django.utils.translation import gettext_lazy as _ -from apps.sites.models import Address, Contact -from apps.sites.models.contact_models import RcraPhone +from apps.site.models import Address, Contact +from apps.site.models.contact_models import RcraPhone -from ...core.models import RcraProfile +from ...core.models import RcrainfoProfile from .base_models import SitesBaseManager, SitesBaseModel logger = logging.getLogger(__name__) @@ -45,16 +45,16 @@ class Meta: def rcrainfo_api_id_key(self) -> tuple[str, str] | None: """Returns the RcraInfo API credentials for the admin user""" try: - rcrainfo_profile = RcraProfile.objects.get(haztrak_profile__user=self.admin) + rcrainfo_profile = RcrainfoProfile.objects.get(haztrak_profile__user=self.admin) return rcrainfo_profile.rcra_api_id, rcrainfo_profile.rcra_api_key - except RcraProfile.DoesNotExist: + except RcrainfoProfile.DoesNotExist: return None @property def is_rcrainfo_integrated(self) -> bool: """Returns True if the admin user has RcraInfo API credentials""" - if RcraProfile.objects.filter(haztrak_profile__user=self.admin).exists(): - return RcraProfile.objects.get( + if RcrainfoProfile.objects.filter(haztrak_profile__user=self.admin).exists(): + return RcrainfoProfile.objects.get( haztrak_profile__user=self.admin ).has_rcrainfo_api_id_key else: @@ -316,7 +316,7 @@ class Meta: on_delete=models.CASCADE, ) profile = models.ForeignKey( - "core.RcraProfile", + "core.RcrainfoProfile", on_delete=models.PROTECT, related_name="permissions", ) diff --git a/server/apps/sites/serializers/__init__.py b/server/apps/site/serializers/__init__.py similarity index 100% rename from server/apps/sites/serializers/__init__.py rename to server/apps/site/serializers/__init__.py diff --git a/server/apps/sites/serializers/address_serializer.py b/server/apps/site/serializers/address_serializer.py similarity index 96% rename from server/apps/sites/serializers/address_serializer.py rename to server/apps/site/serializers/address_serializer.py index 37ef47f80..9f6655ba6 100644 --- a/server/apps/sites/serializers/address_serializer.py +++ b/server/apps/site/serializers/address_serializer.py @@ -3,7 +3,7 @@ from rest_framework import serializers from rest_framework.exceptions import ValidationError -from apps.sites.models import Address, RcraStates +from apps.site.models import Address, RcraStates from ..models.contact_models import RcraCountries from .base_serializer import SitesBaseSerializer diff --git a/server/apps/sites/serializers/base_serializer.py b/server/apps/site/serializers/base_serializer.py similarity index 100% rename from server/apps/sites/serializers/base_serializer.py rename to server/apps/site/serializers/base_serializer.py diff --git a/server/apps/sites/serializers/contact_serializer.py b/server/apps/site/serializers/contact_serializer.py similarity index 97% rename from server/apps/sites/serializers/contact_serializer.py rename to server/apps/site/serializers/contact_serializer.py index decd4db39..8b0e45cc6 100644 --- a/server/apps/sites/serializers/contact_serializer.py +++ b/server/apps/site/serializers/contact_serializer.py @@ -1,6 +1,6 @@ from rest_framework import serializers -from apps.sites.models import Contact +from apps.site.models import Contact from .base_serializer import SitesBaseSerializer diff --git a/server/apps/sites/serializers/profile_serializer.py b/server/apps/site/serializers/profile_serializer.py similarity index 98% rename from server/apps/sites/serializers/profile_serializer.py rename to server/apps/site/serializers/profile_serializer.py index e8875db64..2e6295b0c 100644 --- a/server/apps/sites/serializers/profile_serializer.py +++ b/server/apps/site/serializers/profile_serializer.py @@ -1,7 +1,7 @@ from rest_framework import serializers from rest_framework.exceptions import APIException, ValidationError -from apps.sites.models import RcraSitePermissions, SitePermissions +from apps.site.models import RcraSitePermissions, SitePermissions from .base_serializer import SitesBaseSerializer from .site_serializer import HaztrakSiteSerializer diff --git a/server/apps/sites/serializers/site_serializer.py b/server/apps/site/serializers/site_serializer.py similarity index 94% rename from server/apps/sites/serializers/site_serializer.py rename to server/apps/site/serializers/site_serializer.py index 36b1b4982..7a6183f6e 100644 --- a/server/apps/sites/serializers/site_serializer.py +++ b/server/apps/site/serializers/site_serializer.py @@ -1,7 +1,7 @@ from rest_framework import serializers -from apps.sites.models import HaztrakOrg, HaztrakSite, RcraSite, RcraSiteType -from apps.sites.serializers import AddressSerializer, ContactSerializer, RcraPhoneSerializer +from apps.site.models import HaztrakOrg, HaztrakSite, RcraSite, RcraSiteType +from apps.site.serializers import AddressSerializer, ContactSerializer, RcraPhoneSerializer from .base_serializer import SitesBaseSerializer diff --git a/server/apps/sites/services/__init__.py b/server/apps/site/services/__init__.py similarity index 100% rename from server/apps/sites/services/__init__.py rename to server/apps/site/services/__init__.py diff --git a/server/apps/sites/services/org_services.py b/server/apps/site/services/org_services.py similarity index 94% rename from server/apps/sites/services/org_services.py rename to server/apps/site/services/org_services.py index ac5d9fb55..95140dc6a 100644 --- a/server/apps/sites/services/org_services.py +++ b/server/apps/site/services/org_services.py @@ -1,6 +1,6 @@ from django.db.models import QuerySet -from apps.sites.models.site_models import HaztrakOrg, HaztrakSite +from apps.site.models.site_models import HaztrakOrg, HaztrakSite def get_org(org_id: str) -> HaztrakOrg: diff --git a/server/apps/sites/services/rcra_profile_services.py b/server/apps/site/services/rcra_profile_services.py similarity index 85% rename from server/apps/sites/services/rcra_profile_services.py rename to server/apps/site/services/rcra_profile_services.py index 6b8ef95d6..4964df2bd 100644 --- a/server/apps/sites/services/rcra_profile_services.py +++ b/server/apps/site/services/rcra_profile_services.py @@ -3,14 +3,14 @@ from django.db import transaction -from apps.core.models import HaztrakProfile, HaztrakUser, RcraProfile # type: ignore +from apps.core.models import HaztrakProfile, HaztrakUser, RcrainfoProfile # type: ignore from apps.core.services import ( # type: ignore RcrainfoService, get_or_create_haztrak_profile, get_rcrainfo_client, ) -from apps.sites.models import HaztrakSite, RcraSite, RcraSitePermissions # type: ignore -from apps.sites.serializers import RcraPermissionSerializer # type: ignore +from apps.site.models import HaztrakSite, RcraSite, RcraSitePermissions # type: ignore +from apps.site.serializers import RcraPermissionSerializer # type: ignore from .rcra_site_services import RcraSiteService from .site_services import HaztrakSiteService, HaztrakSiteServiceError # type: ignore @@ -18,9 +18,11 @@ logger = logging.getLogger(__name__) -def get_or_create_rcra_profile(*, username: str) -> tuple[RcraProfile, bool]: - """Retrieve a user's RcraProfile""" - profile, created = RcraProfile.objects.get_or_create(haztrak_profile__user__username=username) +def get_or_create_rcra_profile(*, username: str) -> tuple[RcrainfoProfile, bool]: + """Retrieve a user's RcrainfoProfile""" + profile, created = RcrainfoProfile.objects.get_or_create( + haztrak_profile__user__username=username + ) if created: haztrak_profile, created = get_or_create_haztrak_profile(username=username) haztrak_profile.rcrainfo_profile = profile @@ -38,14 +40,14 @@ def __init__(self, message: str): class RcraProfileService: """ - RcraProfileService encapsulates the RcraProfile subdomain business logic + RcraProfileService encapsulates the RcrainfoProfile subdomain business logic of a and exposes method corresponding to use cases. """ def __init__(self, *, username: str, rcrainfo: Optional[RcrainfoService] = None): self.username = username profile, created = get_or_create_rcra_profile(username=username) - self.profile: RcraProfile = profile + self.profile: RcrainfoProfile = profile self.rcrainfo = rcrainfo or get_rcrainfo_client(username=username) def update_rcrainfo_profile(self, *, rcrainfo_username: Optional[str] = None) -> None: @@ -64,7 +66,7 @@ def update_rcrainfo_profile(self, *, rcrainfo_username: Optional[str] = None) -> ) permissions = self._parse_rcra_response(rcra_response=profile_response.json()) self._save_rcrainfo_profile_permissions(permissions) - except (RcraProfile.DoesNotExist, RcraSite.DoesNotExist) as exc: + except (RcrainfoProfile.DoesNotExist, RcraSite.DoesNotExist) as exc: raise RcraProfileServiceError(exc) def _save_rcrainfo_profile_permissions(self, permissions: list[dict]) -> None: diff --git a/server/apps/sites/services/rcra_site_services.py b/server/apps/site/services/rcra_site_services.py similarity index 97% rename from server/apps/sites/services/rcra_site_services.py rename to server/apps/site/services/rcra_site_services.py index 75b802c04..29174f488 100644 --- a/server/apps/sites/services/rcra_site_services.py +++ b/server/apps/site/services/rcra_site_services.py @@ -7,8 +7,8 @@ from rest_framework.exceptions import ValidationError from apps.core.services import RcrainfoService, get_rcrainfo_client -from apps.sites.models import RcraSite -from apps.sites.serializers import RcraSiteSerializer +from apps.site.models import RcraSite +from apps.site.serializers import RcraSiteSerializer class HandlerSearchResults(TypedDict): diff --git a/server/apps/sites/services/site_services.py b/server/apps/site/services/site_services.py similarity index 93% rename from server/apps/sites/services/site_services.py rename to server/apps/site/services/site_services.py index 09022c5fd..f754da1be 100644 --- a/server/apps/sites/services/site_services.py +++ b/server/apps/site/services/site_services.py @@ -5,7 +5,7 @@ from django.db import transaction from apps.core.services import RcrainfoService, get_rcrainfo_client -from apps.sites.models import HaztrakSite +from apps.site.models import HaztrakSite from apps.trak.services import EManifest, PullManifestsResult, TaskResponse from apps.trak.tasks import sync_site_manifests @@ -23,11 +23,11 @@ class HaztrakSiteService: """ def __init__( - self, - *, - username: str, - site_id: Optional[str] = None, - rcrainfo: Optional[RcrainfoService] = None, + self, + *, + username: str, + site_id: Optional[str] = None, + rcrainfo: Optional[RcrainfoService] = None, ): self.username = username self.rcrainfo = rcrainfo or get_rcrainfo_client(username=username) @@ -63,6 +63,7 @@ def _get_updated_mtn(self, site_id: str, last_sync_date: datetime) -> list[str]: emanifest = EManifest(username=self.username, rcrainfo=self.rcrainfo) return emanifest.search(site_id=site_id, start_date=last_sync_date) + # ToDo: all of our current HaztrakSite service class (1) does not need to be a class and (2) should # probably be moved to the manifest service module # def get_user_sites(*, username: str) -> QuerySet[HaztrakSite]: diff --git a/server/apps/sites/tasks/__init__.py b/server/apps/site/tasks/__init__.py similarity index 100% rename from server/apps/sites/tasks/__init__.py rename to server/apps/site/tasks/__init__.py diff --git a/server/apps/sites/tasks/profile_tasks.py b/server/apps/site/tasks/profile_tasks.py similarity index 90% rename from server/apps/sites/tasks/profile_tasks.py rename to server/apps/site/tasks/profile_tasks.py index 61087b843..bcdee6d85 100644 --- a/server/apps/sites/tasks/profile_tasks.py +++ b/server/apps/site/tasks/profile_tasks.py @@ -4,7 +4,7 @@ from celery.exceptions import Ignore, Reject from requests import RequestException -from apps.sites.services.rcra_profile_services import RcraProfileServiceError +from apps.site.services.rcra_profile_services import RcraProfileServiceError logger = logging.getLogger(__name__) @@ -22,7 +22,7 @@ def sync_user_rcrainfo_sites(self: RcraProfileTasks, username: str) -> None: This task initiates a call to the RcraProfileService to pull a user's RCRAInfo profile and update that information in Haztrak. """ - from apps.sites.services import RcraProfileService + from apps.site.services import RcraProfileService try: rcra_profile = RcraProfileService(username=username) diff --git a/server/apps/sites/tasks/site_tasks.py b/server/apps/site/tasks/site_tasks.py similarity index 91% rename from server/apps/sites/tasks/site_tasks.py rename to server/apps/site/tasks/site_tasks.py index edf90b5f6..9b70c217b 100644 --- a/server/apps/sites/tasks/site_tasks.py +++ b/server/apps/site/tasks/site_tasks.py @@ -3,7 +3,7 @@ from celery import shared_task, states from celery.exceptions import Ignore -from apps.sites.services import RcraSiteService +from apps.site.services import RcraSiteService logger = logging.getLogger(__name__) diff --git a/server/apps/sites/tests/__init__.py b/server/apps/site/tests/__init__.py similarity index 100% rename from server/apps/sites/tests/__init__.py rename to server/apps/site/tests/__init__.py diff --git a/server/apps/sites/tests/conftest.py b/server/apps/site/tests/conftest.py similarity index 89% rename from server/apps/sites/tests/conftest.py rename to server/apps/site/tests/conftest.py index ddcc85614..65a0ab976 100644 --- a/server/apps/sites/tests/conftest.py +++ b/server/apps/site/tests/conftest.py @@ -2,8 +2,8 @@ import pytest -from apps.core.models import RcraProfile -from apps.sites.models import ( +from apps.core.models import RcrainfoProfile +from apps.site.models import ( RcraSite, RcraSitePermissions, ) @@ -15,7 +15,7 @@ def rcra_permission_factory(db, rcra_site_factory, rcra_profile_factory): def create_permission( site: Optional[RcraSite] = None, - profile: Optional[RcraProfile] = None, + profile: Optional[RcrainfoProfile] = None, site_manager: Optional[bool] = True, annual_report: Optional[str] = "Certifier", biennial_report: Optional[str] = "Certifier", diff --git a/server/apps/sites/tests/test_epa_site_views.py b/server/apps/site/tests/test_epa_site_views.py similarity index 96% rename from server/apps/sites/tests/test_epa_site_views.py rename to server/apps/site/tests/test_epa_site_views.py index 941c33b3f..d26e26118 100644 --- a/server/apps/sites/tests/test_epa_site_views.py +++ b/server/apps/site/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 # type: ignore -from apps.sites.views import SearchRcraSiteView # type: ignore +from apps.site.models import RcraSiteType # type: ignore +from apps.site.views import SearchRcraSiteView # type: ignore class TestEpaSiteView: diff --git a/server/apps/sites/tests/test_handler_services.py b/server/apps/site/tests/test_handler_services.py similarity index 94% rename from server/apps/sites/tests/test_handler_services.py rename to server/apps/site/tests/test_handler_services.py index df678b535..ce8d4be75 100644 --- a/server/apps/sites/tests/test_handler_services.py +++ b/server/apps/site/tests/test_handler_services.py @@ -1,8 +1,8 @@ import pytest from apps.core.services import RcrainfoService -from apps.sites.models import RcraSite -from apps.sites.services import RcraSiteService +from apps.site.models import RcraSite +from apps.site.services import RcraSiteService class TestHandlerService: diff --git a/server/apps/sites/tests/test_models.py b/server/apps/site/tests/test_models.py similarity index 97% rename from server/apps/sites/tests/test_models.py rename to server/apps/site/tests/test_models.py index b335a4022..4cd708df6 100644 --- a/server/apps/sites/tests/test_models.py +++ b/server/apps/site/tests/test_models.py @@ -2,8 +2,8 @@ from django.core.exceptions import ValidationError from django.db import IntegrityError -from apps.sites.models import Address, Contact, HaztrakSite -from apps.sites.models.site_models import HaztrakOrg +from apps.site.models import Address, Contact, HaztrakSite +from apps.site.models.site_models import HaztrakOrg @pytest.mark.django_db diff --git a/server/apps/sites/tests/test_serializers.py b/server/apps/site/tests/test_serializers.py similarity index 97% rename from server/apps/sites/tests/test_serializers.py rename to server/apps/site/tests/test_serializers.py index 51c6ea8c5..e4224eae3 100644 --- a/server/apps/sites/tests/test_serializers.py +++ b/server/apps/site/tests/test_serializers.py @@ -1,7 +1,7 @@ import pytest -from apps.sites.models import RcraSitePermissions -from apps.sites.serializers import ( +from apps.site.models import RcraSitePermissions +from apps.site.serializers import ( ContactSerializer, HaztrakOrgSerializer, RcraPermissionSerializer, diff --git a/server/apps/sites/tests/test_services.py b/server/apps/site/tests/test_services.py similarity index 88% rename from server/apps/sites/tests/test_services.py rename to server/apps/site/tests/test_services.py index ecfb6d7b2..3645f08fa 100644 --- a/server/apps/sites/tests/test_services.py +++ b/server/apps/site/tests/test_services.py @@ -1,8 +1,8 @@ import uuid -from apps.core.models import HaztrakProfile, RcraProfile -from apps.sites.services.org_services import get_org, get_org_rcrainfo_api_credentials -from apps.sites.services.rcra_profile_services import get_or_create_rcra_profile +from apps.core.models import HaztrakProfile, RcrainfoProfile +from apps.site.services.org_services import get_org, get_org_rcrainfo_api_credentials +from apps.site.services.rcra_profile_services import get_or_create_rcra_profile class TestOrgServices: @@ -46,7 +46,7 @@ def test_get_or_create_returns_true_if_new_profile(self, user_factory): username = "my_username" user_factory(username=username) retrieved_rcra_profile, created = get_or_create_rcra_profile(username=username) - assert isinstance(retrieved_rcra_profile, RcraProfile) + assert isinstance(retrieved_rcra_profile, RcrainfoProfile) assert created is True def test_creates_creates_a_haztrak_profile_if_not_present(self, user_factory): diff --git a/server/apps/sites/tests/test_site_views.py b/server/apps/site/tests/test_site_views.py similarity index 95% rename from server/apps/sites/tests/test_site_views.py rename to server/apps/site/tests/test_site_views.py index d0c55355d..3b9b8ebf1 100644 --- a/server/apps/sites/tests/test_site_views.py +++ b/server/apps/site/tests/test_site_views.py @@ -2,9 +2,9 @@ from rest_framework import status from rest_framework.test import APIClient, APIRequestFactory, force_authenticate -from apps.core.models import HaztrakUser, RcraProfile # type: ignore -from apps.sites.models import HaztrakSite, RcraSite, RcraSitePermissions # type: ignore -from apps.sites.views import SiteDetailView # type: ignore +from apps.core.models import HaztrakUser, RcrainfoProfile # type: ignore +from apps.site.models import HaztrakSite, RcraSite, RcraSitePermissions # type: ignore +from apps.site.views import SiteDetailView # type: ignore class TestHaztrakSiteListView: diff --git a/server/apps/sites/urls.py b/server/apps/site/urls.py similarity index 93% rename from server/apps/sites/urls.py rename to server/apps/site/urls.py index 536a037a0..ad44d5fa3 100644 --- a/server/apps/sites/urls.py +++ b/server/apps/site/urls.py @@ -1,6 +1,6 @@ from django.urls import include, path -from apps.sites.views import ( # type: ignore +from apps.site.views import ( # type: ignore GetRcraSiteView, HaztrakOrgSitesListView, HaztrakSiteListView, diff --git a/server/apps/sites/views.py b/server/apps/site/views.py similarity index 94% rename from server/apps/sites/views.py rename to server/apps/site/views.py index 34dc73183..07fe508c4 100644 --- a/server/apps/sites/views.py +++ b/server/apps/site/views.py @@ -9,10 +9,10 @@ from rest_framework.response import Response from rest_framework.views import APIView -from apps.sites.models import HaztrakSite, RcraSite, RcraSiteType # type: ignore -from apps.sites.serializers import HaztrakSiteSerializer, RcraSiteSerializer # type: ignore -from apps.sites.services import RcraSiteService, get_org_sites # type: ignore -from apps.sites.services.rcra_site_services import query_rcra_sites +from apps.site.models import HaztrakSite, RcraSite, RcraSiteType # type: ignore +from apps.site.serializers import HaztrakSiteSerializer, RcraSiteSerializer # type: ignore +from apps.site.services import RcraSiteService, get_org_sites # type: ignore +from apps.site.services.rcra_site_services import query_rcra_sites logger = logging.getLogger(__name__) diff --git a/server/apps/sites/migrations/0001_initial.py b/server/apps/sites/migrations/0001_initial.py deleted file mode 100644 index 53a17932c..000000000 --- a/server/apps/sites/migrations/0001_initial.py +++ /dev/null @@ -1,156 +0,0 @@ -# Generated by Django 4.2.7 on 2023-11-20 16:47 - -import apps.sites.models.contact_models -from django.conf import settings -import django.core.validators -from django.db import migrations, models -import django.db.models.deletion -import uuid - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ('core', '0001_initial'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='Address', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('street_number', models.CharField(blank=True, max_length=12, null=True)), - ('address1', models.CharField(max_length=50, verbose_name='address 1')), - ('address2', models.CharField(blank=True, default=None, max_length=50, null=True, verbose_name='address 2')), - ('city', models.CharField(blank=True, max_length=25, null=True)), - ('state', models.CharField(blank=True, choices=[('AK', 'Alaska'), ('AL', 'Alabama'), ('AP', 'Armed Forces Pacific'), ('AR', 'Arkansas'), ('AZ', 'Arizona'), ('CA', 'California'), ('CO', 'Colorado'), ('CT', 'Connecticut'), ('DC', 'Washington DC'), ('DE', 'Delaware'), ('FL', 'Florida'), ('GA', 'Georgia'), ('GU', 'Guam'), ('HI', 'Hawaii'), ('IA', 'Iowa'), ('ID', 'Idaho'), ('IL', 'Illinois'), ('IN', 'Indiana'), ('KS', 'Kansas'), ('KY', 'Kentucky'), ('LA', 'Louisiana'), ('MA', 'Massachusetts'), ('MD', 'Maryland'), ('ME', 'Maine'), ('MI', 'Michigan'), ('MN', 'Minnesota'), ('MO', 'Missouri'), ('MS', 'Mississippi'), ('MT', 'Montana'), ('NC', 'North Carolina'), ('ND', 'North Dakota'), ('NE', 'Nebraska'), ('NH', 'New Hampshire'), ('NJ', 'New Jersey'), ('NM', 'New Mexico'), ('NV', 'Nevada'), ('NY', 'New York'), ('OH', 'Ohio'), ('OK', 'Oklahoma'), ('OR', 'Oregon'), ('PA', 'Pennsylvania'), ('PR', 'Puerto Rico'), ('RI', 'Rhode Island'), ('SC', 'South Carolina'), ('SD', 'South Dakota'), ('TN', 'Tennessee'), ('TX', 'Texas'), ('UT', 'Utah'), ('VA', 'Virginia'), ('VI', 'Virgin Islands'), ('VT', 'Vermont'), ('WA', 'Washington'), ('WI', 'Wisconsin'), ('WV', 'West Virginia'), ('WY', 'Wyoming'), ('XA', 'REGION 01 PURVIEW'), ('XB', 'REGION 02 PURVIEW'), ('XC', 'REGION 03 PURVIEW'), ('XD', 'REGION 04 PURVIEW'), ('XE', 'REGION 05 PURVIEW'), ('XF', 'REGION 06 PURVIEW'), ('XG', 'REGION 07 PURVIEW'), ('XH', 'REGION 08 PURVIEW'), ('XI', 'REGION 09 PURVIEW'), ('XJ', 'REGION 10 PURVIEW')], max_length=3, null=True)), - ('country', models.CharField(blank=True, choices=[('US', 'United States'), ('MX', 'Mexico'), ('CA', 'Canada')], max_length=3, null=True)), - ('zip', models.CharField(blank=True, max_length=5, null=True)), - ], - options={ - 'ordering': ['address1'], - }, - ), - migrations.CreateModel( - name='Contact', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('first_name', models.CharField(blank=True, max_length=38, null=True)), - ('middle_initial', models.CharField(blank=True, max_length=1, null=True)), - ('last_name', models.CharField(blank=True, max_length=38, null=True)), - ('email', models.EmailField(blank=True, max_length=254, null=True)), - ('company_name', models.CharField(blank=True, max_length=80, null=True)), - ], - options={ - 'ordering': ['first_name'], - }, - ), - migrations.CreateModel( - name='HaztrakOrg', - fields=[ - ('name', models.CharField(max_length=200, unique=True)), - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('admin', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)), - ], - options={ - 'verbose_name': 'Organization', - 'verbose_name_plural': 'Organizations', - 'ordering': ['name'], - }, - ), - migrations.CreateModel( - name='HaztrakSite', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=200, validators=[django.core.validators.MinLengthValidator(2, 'site aliases must be longer than 2 characters')], verbose_name='site alias')), - ('last_rcrainfo_manifest_sync', models.DateTimeField(blank=True, null=True, verbose_name='last RCRAInfo manifest sync date')), - ('org', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='sites.haztrakorg')), - ], - options={ - 'verbose_name': 'Haztrak Site', - 'verbose_name_plural': 'Haztrak Sites', - 'ordering': ['rcra_site__epa_id'], - }, - ), - migrations.CreateModel( - name='RcraPhone', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('number', apps.sites.models.contact_models.RcraPhoneNumber(max_length=12)), - ('extension', models.CharField(blank=True, max_length=6, null=True)), - ], - options={ - 'ordering': ['number'], - }, - ), - migrations.CreateModel( - name='RcraSite', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('site_type', models.CharField(blank=True, choices=[('Generator', 'Generator'), ('Transporter', 'Transporter'), ('Tsdf', 'Tsdf'), ('Broker', 'Broker')], max_length=20, null=True)), - ('epa_id', models.CharField(max_length=25, unique=True, verbose_name='EPA ID number')), - ('name', models.CharField(max_length=200)), - ('modified', models.BooleanField(blank=True, null=True)), - ('registered', models.BooleanField(blank=True, null=True)), - ('gis_primary', models.BooleanField(blank=True, default=False, null=True, verbose_name='GIS primary')), - ('can_esign', models.BooleanField(blank=True, null=True, verbose_name='can electronically sign')), - ('limited_esign', models.BooleanField(blank=True, null=True, verbose_name='limited electronic signing ability')), - ('registered_emanifest_user', models.BooleanField(blank=True, default=False, null=True, verbose_name='has registered e-manifest user')), - ('contact', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='sites.contact', verbose_name='contact information')), - ('emergency_phone', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='sites.rcraphone')), - ('mail_address', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='mail_address', to='sites.address')), - ('site_address', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='site_address', to='sites.address')), - ], - options={ - 'verbose_name': 'RCRAInfo Site', - 'verbose_name_plural': 'RCRAInfo Sites', - 'ordering': ['epa_id'], - }, - ), - migrations.CreateModel( - name='SitePermissions', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('emanifest', models.CharField(choices=[('viewer', 'view'), ('editor', 'edit'), ('signer', 'sign')], default='view', max_length=6)), - ('profile', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='site_permissions', to='core.haztrakprofile')), - ('site', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='sites.haztraksite')), - ], - options={ - 'verbose_name': 'Site Permission', - 'verbose_name_plural': 'Site Permissions', - 'ordering': ['profile'], - }, - ), - migrations.CreateModel( - name='RcraSitePermissions', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('site_manager', models.BooleanField(default=False)), - ('annual_report', models.CharField(choices=[('Certifier', 'Certifier'), ('Preparer', 'Preparer'), ('Viewer', 'Viewer')], max_length=12)), - ('biennial_report', models.CharField(choices=[('Certifier', 'Certifier'), ('Preparer', 'Preparer'), ('Viewer', 'Viewer')], max_length=12)), - ('e_manifest', models.CharField(choices=[('Certifier', 'Certifier'), ('Preparer', 'Preparer'), ('Viewer', 'Viewer')], max_length=12)), - ('my_rcra_id', models.CharField(choices=[('Certifier', 'Certifier'), ('Preparer', 'Preparer'), ('Viewer', 'Viewer')], max_length=12)), - ('wiets', models.CharField(choices=[('Certifier', 'Certifier'), ('Preparer', 'Preparer'), ('Viewer', 'Viewer')], max_length=12)), - ('profile', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='permissions', to='core.rcraprofile')), - ('site', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='sites.rcrasite')), - ], - options={ - 'verbose_name': 'RCRAInfo Permission', - 'verbose_name_plural': 'RCRAInfo Permissions', - 'ordering': ['site__epa_id'], - }, - ), - migrations.AddField( - model_name='haztraksite', - name='rcra_site', - field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='sites.rcrasite', verbose_name='rcra_site'), - ), - migrations.AddField( - model_name='contact', - name='phone', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='sites.rcraphone'), - ), - ] diff --git a/server/apps/trak/migrations/0001_initial.py b/server/apps/trak/migrations/0001_initial.py index 6593568b9..97baac20e 100644 --- a/server/apps/trak/migrations/0001_initial.py +++ b/server/apps/trak/migrations/0001_initial.py @@ -7,249 +7,746 @@ class Migration(migrations.Migration): - initial = True dependencies = [ - ('sites', '0001_initial'), + ("site", "0001_initial"), ] operations = [ migrations.CreateModel( - name='AdditionalInfo', + name="AdditionalInfo", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('original_mtn', models.JSONField(blank=True, help_text='Original manifest tracking number of rejected manifestRegex expression validation: [0-9]{9}[A-Z]{3}', null=True, validators=[apps.trak.models.manifest_models.validate_mtn])), - ('new_destination', models.CharField(blank=True, choices=[('GEN', 'Generator'), ('TSD', 'Tsdf')], help_text='Destination of the new manifest created during rejection or residue.', max_length=255, null=True)), - ('consent_number', models.CharField(blank=True, max_length=12, null=True)), - ('comments', models.JSONField(blank=True, null=True)), - ('handling_instructions', models.CharField(blank=True, help_text='Special Handling Instructions', max_length=4000, null=True)), + ( + "id", + models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ( + "original_mtn", + models.JSONField( + blank=True, + help_text="Original manifest tracking number of rejected manifestRegex expression validation: [0-9]{9}[A-Z]{3}", + null=True, + validators=[apps.trak.models.manifest_models.validate_mtn], + ), + ), + ( + "new_destination", + models.CharField( + blank=True, + choices=[("GEN", "Generator"), ("TSD", "Tsdf")], + help_text="Destination of the new manifest created during rejection or residue.", + max_length=255, + null=True, + ), + ), + ("consent_number", models.CharField(blank=True, max_length=12, null=True)), + ("comments", models.JSONField(blank=True, null=True)), + ( + "handling_instructions", + models.CharField( + blank=True, + help_text="Special Handling Instructions", + max_length=4000, + null=True, + ), + ), ], options={ - 'verbose_name': 'Additional Info', - 'verbose_name_plural': 'Additional Info', + "verbose_name": "Additional Info", + "verbose_name_plural": "Additional Info", }, ), migrations.CreateModel( - name='DotLookup', + name="DotLookup", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('value', models.CharField(max_length=255)), - ('value_type', models.CharField(choices=[('ID', 'Id'), ('GROUP', 'Group'), ('NAME', 'Name'), ('CLASS', 'Class')], max_length=5)), + ( + "id", + models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ("value", models.CharField(max_length=255)), + ( + "value_type", + models.CharField( + choices=[ + ("ID", "Id"), + ("GROUP", "Group"), + ("NAME", "Name"), + ("CLASS", "Class"), + ], + max_length=5, + ), + ), ], options={ - 'verbose_name': 'DOT lookup', - 'verbose_name_plural': 'DOT lookups', - 'ordering': ['value_type', 'value'], + "verbose_name": "DOT lookup", + "verbose_name_plural": "DOT lookups", + "ordering": ["value_type", "value"], }, ), migrations.CreateModel( - name='Handler', + name="Handler", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ( + "id", + models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), ], options={ - 'ordering': ['rcra_site'], + "ordering": ["rcra_site"], }, ), migrations.CreateModel( - name='Manifest', + name="Manifest", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_date', models.DateTimeField(auto_now=True, null=True)), - ('update_date', models.DateTimeField(auto_now=True)), - ('mtn', models.CharField(default=apps.trak.models.manifest_models.draft_mtn, max_length=30, unique=True, validators=[apps.trak.models.manifest_models.validate_mtn], verbose_name='manifest Tracking Number')), - ('status', models.CharField(choices=[('NotAssigned', 'Not Assigned'), ('Pending', 'Pending'), ('Scheduled', 'Scheduled'), ('InTransit', 'In Transit'), ('ReadyForSignature', 'Ready for Signature'), ('Signed', 'Signed'), ('Corrected', 'Corrected'), ('UnderCorrection', 'Under Correction'), ('MtnValidationFailed', 'MTN Validation Failed')], default='NotAssigned', max_length=25)), - ('submission_type', models.CharField(choices=[('FullElectronic', 'Full Electronic'), ('DataImage5Copy', 'Data + Image'), ('Hybrid', 'Hybrid'), ('Image', 'Image')], default='FullElectronic', max_length=25)), - ('signature_status', models.BooleanField(blank=True, null=True)), - ('origin_type', models.CharField(choices=[('Web', 'Web'), ('Service', 'Service'), ('Mail', 'Mail')], default='Service', max_length=25)), - ('shipped_date', models.DateTimeField(blank=True, null=True)), - ('potential_ship_date', models.DateTimeField(blank=True, null=True, verbose_name='potential ship date')), - ('received_date', models.DateTimeField(blank=True, null=True)), - ('certified_date', models.DateTimeField(blank=True, null=True)), - ('certified_by', models.JSONField(blank=True, null=True)), - ('broker', models.JSONField(blank=True, null=True)), - ('rejection', models.BooleanField(blank=True, null=True)), - ('rejection_info', models.JSONField(blank=True, null=True, verbose_name='Rejection Information')), - ('discrepancy', models.BooleanField(blank=True, default=False)), - ('residue', models.BooleanField(blank=True, default=False)), - ('residue_new_mtn', models.JSONField(blank=True, default=list, verbose_name='residue new MTN')), - ('import_flag', models.BooleanField(blank=True, default=False, verbose_name='import')), - ('import_info', models.JSONField(blank=True, null=True, verbose_name='import information')), - ('contains_residue_or_rejection', models.BooleanField(blank=True, null=True, verbose_name='contains previous rejection or residue waste')), - ('printed_document', models.JSONField(blank=True, null=True)), - ('form_document', models.JSONField(blank=True, null=True)), - ('correction_info', models.JSONField(blank=True, null=True)), - ('ppc_status', models.JSONField(blank=True, null=True, verbose_name='PPC info')), - ('locked', models.BooleanField(blank=True, null=True)), - ('lock_reason', models.CharField(blank=True, choices=[('ACS', 'AsyncSign'), ('ECB', 'EpaChangeBiller'), ('EPC', 'EpaCorrection')], max_length=25, null=True)), - ('transfer_requested', models.BooleanField(blank=True, null=True)), - ('transfer_status', models.CharField(blank=True, max_length=200, null=True)), - ('original_sub_type', models.CharField(blank=True, choices=[('FullElectronic', 'Full Electronic'), ('DataImage5Copy', 'Data + Image'), ('Hybrid', 'Hybrid'), ('Image', 'Image')], max_length=25, null=True, verbose_name='original submission type')), - ('transfer_count', models.IntegerField(blank=True, null=True)), - ('next_transfer_time', models.DateTimeField(blank=True, null=True, verbose_name='next transfer time')), - ('additional_info', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='trak.additionalinfo')), - ('generator', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='generator', to='trak.handler')), - ('tsdf', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='designated_facility', to='trak.handler', verbose_name='designated facility')), + ( + "id", + models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ("created_date", models.DateTimeField(auto_now=True, null=True)), + ("update_date", models.DateTimeField(auto_now=True)), + ( + "mtn", + models.CharField( + default=apps.trak.models.manifest_models.draft_mtn, + max_length=30, + unique=True, + validators=[apps.trak.models.manifest_models.validate_mtn], + verbose_name="manifest Tracking Number", + ), + ), + ( + "status", + models.CharField( + choices=[ + ("NotAssigned", "Not Assigned"), + ("Pending", "Pending"), + ("Scheduled", "Scheduled"), + ("InTransit", "In Transit"), + ("ReadyForSignature", "Ready for Signature"), + ("Signed", "Signed"), + ("Corrected", "Corrected"), + ("UnderCorrection", "Under Correction"), + ("MtnValidationFailed", "MTN Validation Failed"), + ], + default="NotAssigned", + max_length=25, + ), + ), + ( + "submission_type", + models.CharField( + choices=[ + ("FullElectronic", "Full Electronic"), + ("DataImage5Copy", "Data + Image"), + ("Hybrid", "Hybrid"), + ("Image", "Image"), + ], + default="FullElectronic", + max_length=25, + ), + ), + ("signature_status", models.BooleanField(blank=True, null=True)), + ( + "origin_type", + models.CharField( + choices=[("Web", "Web"), ("Service", "Service"), ("Mail", "Mail")], + default="Service", + max_length=25, + ), + ), + ("shipped_date", models.DateTimeField(blank=True, null=True)), + ( + "potential_ship_date", + models.DateTimeField( + blank=True, null=True, verbose_name="potential ship date" + ), + ), + ("received_date", models.DateTimeField(blank=True, null=True)), + ("certified_date", models.DateTimeField(blank=True, null=True)), + ("certified_by", models.JSONField(blank=True, null=True)), + ("broker", models.JSONField(blank=True, null=True)), + ("rejection", models.BooleanField(blank=True, null=True)), + ( + "rejection_info", + models.JSONField(blank=True, null=True, verbose_name="Rejection Information"), + ), + ("discrepancy", models.BooleanField(blank=True, default=False)), + ("residue", models.BooleanField(blank=True, default=False)), + ( + "residue_new_mtn", + models.JSONField(blank=True, default=list, verbose_name="residue new MTN"), + ), + ( + "import_flag", + models.BooleanField(blank=True, default=False, verbose_name="import"), + ), + ( + "import_info", + models.JSONField(blank=True, null=True, verbose_name="import information"), + ), + ( + "contains_residue_or_rejection", + models.BooleanField( + blank=True, + null=True, + verbose_name="contains previous rejection or residue waste", + ), + ), + ("printed_document", models.JSONField(blank=True, null=True)), + ("form_document", models.JSONField(blank=True, null=True)), + ("correction_info", models.JSONField(blank=True, null=True)), + ("ppc_status", models.JSONField(blank=True, null=True, verbose_name="PPC info")), + ("locked", models.BooleanField(blank=True, null=True)), + ( + "lock_reason", + models.CharField( + blank=True, + choices=[ + ("ACS", "AsyncSign"), + ("ECB", "EpaChangeBiller"), + ("EPC", "EpaCorrection"), + ], + max_length=25, + null=True, + ), + ), + ("transfer_requested", models.BooleanField(blank=True, null=True)), + ("transfer_status", models.CharField(blank=True, max_length=200, null=True)), + ( + "original_sub_type", + models.CharField( + blank=True, + choices=[ + ("FullElectronic", "Full Electronic"), + ("DataImage5Copy", "Data + Image"), + ("Hybrid", "Hybrid"), + ("Image", "Image"), + ], + max_length=25, + null=True, + verbose_name="original submission type", + ), + ), + ("transfer_count", models.IntegerField(blank=True, null=True)), + ( + "next_transfer_time", + models.DateTimeField(blank=True, null=True, verbose_name="next transfer time"), + ), + ( + "additional_info", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="trak.additionalinfo", + ), + ), + ( + "generator", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="generator", + to="trak.handler", + ), + ), + ( + "tsdf", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="designated_facility", + to="trak.handler", + verbose_name="designated facility", + ), + ), ], options={ - 'ordering': ['update_date', 'mtn'], + "ordering": ["update_date", "mtn"], }, ), migrations.CreateModel( - name='ManifestPhone', + name="ManifestPhone", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('number', apps.trak.models.contact_models.ManifestPhoneNumber(max_length=12)), - ('extension', models.CharField(blank=True, max_length=6, null=True)), + ( + "id", + models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ("number", apps.trak.models.contact_models.ManifestPhoneNumber(max_length=12)), + ("extension", models.CharField(blank=True, max_length=6, null=True)), ], options={ - 'ordering': ['number'], + "ordering": ["number"], }, ), migrations.CreateModel( - name='PaperSignature', + name="PaperSignature", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('printed_name', models.CharField(max_length=255)), - ('sign_date', models.DateTimeField()), + ( + "id", + models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ("printed_name", models.CharField(max_length=255)), + ("sign_date", models.DateTimeField()), ], options={ - 'ordering': ['pk'], - 'abstract': False, + "ordering": ["pk"], + "abstract": False, }, ), migrations.CreateModel( - name='PortOfEntry', + name="PortOfEntry", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('state', models.CharField(blank=True, choices=[('AK', 'Alaska'), ('AL', 'Alabama'), ('AP', 'Armed Forces Pacific'), ('AR', 'Arkansas'), ('AZ', 'Arizona'), ('CA', 'California'), ('CO', 'Colorado'), ('CT', 'Connecticut'), ('DC', 'Washington DC'), ('DE', 'Delaware'), ('FL', 'Florida'), ('GA', 'Georgia'), ('GU', 'Guam'), ('HI', 'Hawaii'), ('IA', 'Iowa'), ('ID', 'Idaho'), ('IL', 'Illinois'), ('IN', 'Indiana'), ('KS', 'Kansas'), ('KY', 'Kentucky'), ('LA', 'Louisiana'), ('MA', 'Massachusetts'), ('MD', 'Maryland'), ('ME', 'Maine'), ('MI', 'Michigan'), ('MN', 'Minnesota'), ('MO', 'Missouri'), ('MS', 'Mississippi'), ('MT', 'Montana'), ('NC', 'North Carolina'), ('ND', 'North Dakota'), ('NE', 'Nebraska'), ('NH', 'New Hampshire'), ('NJ', 'New Jersey'), ('NM', 'New Mexico'), ('NV', 'Nevada'), ('NY', 'New York'), ('OH', 'Ohio'), ('OK', 'Oklahoma'), ('OR', 'Oregon'), ('PA', 'Pennsylvania'), ('PR', 'Puerto Rico'), ('RI', 'Rhode Island'), ('SC', 'South Carolina'), ('SD', 'South Dakota'), ('TN', 'Tennessee'), ('TX', 'Texas'), ('UT', 'Utah'), ('VA', 'Virginia'), ('VI', 'Virgin Islands'), ('VT', 'Vermont'), ('WA', 'Washington'), ('WI', 'Wisconsin'), ('WV', 'West Virginia'), ('WY', 'Wyoming'), ('XA', 'REGION 01 PURVIEW'), ('XB', 'REGION 02 PURVIEW'), ('XC', 'REGION 03 PURVIEW'), ('XD', 'REGION 04 PURVIEW'), ('XE', 'REGION 05 PURVIEW'), ('XF', 'REGION 06 PURVIEW'), ('XG', 'REGION 07 PURVIEW'), ('XH', 'REGION 08 PURVIEW'), ('XI', 'REGION 09 PURVIEW'), ('XJ', 'REGION 10 PURVIEW')], max_length=2, null=True)), - ('city_port', models.CharField(blank=True, max_length=100, null=True)), + ( + "id", + models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ( + "state", + models.CharField( + blank=True, + choices=[ + ("AK", "Alaska"), + ("AL", "Alabama"), + ("AP", "Armed Forces Pacific"), + ("AR", "Arkansas"), + ("AZ", "Arizona"), + ("CA", "California"), + ("CO", "Colorado"), + ("CT", "Connecticut"), + ("DC", "Washington DC"), + ("DE", "Delaware"), + ("FL", "Florida"), + ("GA", "Georgia"), + ("GU", "Guam"), + ("HI", "Hawaii"), + ("IA", "Iowa"), + ("ID", "Idaho"), + ("IL", "Illinois"), + ("IN", "Indiana"), + ("KS", "Kansas"), + ("KY", "Kentucky"), + ("LA", "Louisiana"), + ("MA", "Massachusetts"), + ("MD", "Maryland"), + ("ME", "Maine"), + ("MI", "Michigan"), + ("MN", "Minnesota"), + ("MO", "Missouri"), + ("MS", "Mississippi"), + ("MT", "Montana"), + ("NC", "North Carolina"), + ("ND", "North Dakota"), + ("NE", "Nebraska"), + ("NH", "New Hampshire"), + ("NJ", "New Jersey"), + ("NM", "New Mexico"), + ("NV", "Nevada"), + ("NY", "New York"), + ("OH", "Ohio"), + ("OK", "Oklahoma"), + ("OR", "Oregon"), + ("PA", "Pennsylvania"), + ("PR", "Puerto Rico"), + ("RI", "Rhode Island"), + ("SC", "South Carolina"), + ("SD", "South Dakota"), + ("TN", "Tennessee"), + ("TX", "Texas"), + ("UT", "Utah"), + ("VA", "Virginia"), + ("VI", "Virgin Islands"), + ("VT", "Vermont"), + ("WA", "Washington"), + ("WI", "Wisconsin"), + ("WV", "West Virginia"), + ("WY", "Wyoming"), + ("XA", "REGION 01 PURVIEW"), + ("XB", "REGION 02 PURVIEW"), + ("XC", "REGION 03 PURVIEW"), + ("XD", "REGION 04 PURVIEW"), + ("XE", "REGION 05 PURVIEW"), + ("XF", "REGION 06 PURVIEW"), + ("XG", "REGION 07 PURVIEW"), + ("XH", "REGION 08 PURVIEW"), + ("XI", "REGION 09 PURVIEW"), + ("XJ", "REGION 10 PURVIEW"), + ], + max_length=2, + null=True, + ), + ), + ("city_port", models.CharField(blank=True, max_length=100, null=True)), ], options={ - 'verbose_name': 'Port of Entry', - 'verbose_name_plural': 'Ports of Entry', + "verbose_name": "Port of Entry", + "verbose_name_plural": "Ports of Entry", }, ), migrations.CreateModel( - name='WasteCode', + name="WasteCode", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('code', models.CharField(max_length=6, unique=True)), - ('description', models.TextField(blank=True, null=True)), - ('code_type', models.CharField(choices=[('ST', 'State'), ('FD', 'Federal')], max_length=2)), - ('state_id', models.CharField(blank=True, choices=[('AK', 'Alaska'), ('AL', 'Alabama'), ('AP', 'Armed Forces Pacific'), ('AR', 'Arkansas'), ('AZ', 'Arizona'), ('CA', 'California'), ('CO', 'Colorado'), ('CT', 'Connecticut'), ('DC', 'District of Columbia'), ('DE', 'Delaware'), ('FL', 'Florida'), ('GA', 'Georgia'), ('GU', 'Guam'), ('HI', 'Hawaii'), ('IA', 'Iowa'), ('ID', 'Idaho'), ('IL', 'Illinois'), ('IN', 'Indiana'), ('KS', 'Kansas'), ('KY', 'Kentucky'), ('LA', 'Louisiana'), ('MA', 'Massachusetts'), ('MD', 'Maryland'), ('ME', 'Maine'), ('MI', 'Michigan'), ('MN', 'Minnesota'), ('MO', 'Missouri'), ('MS', 'Mississippi'), ('MT', 'Montana'), ('NC', 'North Carolina'), ('ND', 'North Dakota'), ('NE', 'Nebraska'), ('NH', 'New Hampshire'), ('NJ', 'New Jersey'), ('NM', 'New Mexico'), ('NV', 'Nevada'), ('NY', 'New York'), ('OH', 'Ohio'), ('OK', 'Oklahoma'), ('OR', 'Oregon'), ('PA', 'Pennsylvania'), ('PR', 'Puerto Rico'), ('RI', 'Rhode Island'), ('SC', 'South Carolina'), ('SD', 'South Dakota'), ('TN', 'Tennessee'), ('TX', 'Texas'), ('UT', 'Utah'), ('VA', 'Virginia'), ('VI', 'Virgin Islands'), ('VT', 'Vermont'), ('WA', 'Washington'), ('WI', 'Wisconsin'), ('WV', 'West Virginia'), ('WY', 'Wyoming')], max_length=3, null=True)), + ( + "id", + models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ("code", models.CharField(max_length=6, unique=True)), + ("description", models.TextField(blank=True, null=True)), + ( + "code_type", + models.CharField(choices=[("ST", "State"), ("FD", "Federal")], max_length=2), + ), + ( + "state_id", + models.CharField( + blank=True, + choices=[ + ("AK", "Alaska"), + ("AL", "Alabama"), + ("AP", "Armed Forces Pacific"), + ("AR", "Arkansas"), + ("AZ", "Arizona"), + ("CA", "California"), + ("CO", "Colorado"), + ("CT", "Connecticut"), + ("DC", "District of Columbia"), + ("DE", "Delaware"), + ("FL", "Florida"), + ("GA", "Georgia"), + ("GU", "Guam"), + ("HI", "Hawaii"), + ("IA", "Iowa"), + ("ID", "Idaho"), + ("IL", "Illinois"), + ("IN", "Indiana"), + ("KS", "Kansas"), + ("KY", "Kentucky"), + ("LA", "Louisiana"), + ("MA", "Massachusetts"), + ("MD", "Maryland"), + ("ME", "Maine"), + ("MI", "Michigan"), + ("MN", "Minnesota"), + ("MO", "Missouri"), + ("MS", "Mississippi"), + ("MT", "Montana"), + ("NC", "North Carolina"), + ("ND", "North Dakota"), + ("NE", "Nebraska"), + ("NH", "New Hampshire"), + ("NJ", "New Jersey"), + ("NM", "New Mexico"), + ("NV", "Nevada"), + ("NY", "New York"), + ("OH", "Ohio"), + ("OK", "Oklahoma"), + ("OR", "Oregon"), + ("PA", "Pennsylvania"), + ("PR", "Puerto Rico"), + ("RI", "Rhode Island"), + ("SC", "South Carolina"), + ("SD", "South Dakota"), + ("TN", "Tennessee"), + ("TX", "Texas"), + ("UT", "Utah"), + ("VA", "Virginia"), + ("VI", "Virgin Islands"), + ("VT", "Vermont"), + ("WA", "Washington"), + ("WI", "Wisconsin"), + ("WV", "West Virginia"), + ("WY", "Wyoming"), + ], + max_length=3, + null=True, + ), + ), ], options={ - 'ordering': ['code'], + "ordering": ["code"], }, ), migrations.CreateModel( - name='WasteLine', + name="WasteLine", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('dot_hazardous', models.BooleanField(verbose_name='DOT hazardous')), - ('dot_info', models.JSONField(blank=True, null=True, verbose_name='DOT information')), - ('quantity', models.JSONField(blank=True, null=True)), - ('hazardous_waste', models.JSONField(blank=True, null=True)), - ('line_number', models.PositiveIntegerField(verbose_name='waste line number')), - ('br', models.BooleanField(verbose_name='BR info provided')), - ('br_info', models.JSONField(blank=True, null=True, verbose_name='BR information')), - ('management_method', models.JSONField(blank=True, null=True, verbose_name='management method code')), - ('pcb', models.BooleanField(verbose_name='contains PCBs')), - ('pcb_infos', models.JSONField(blank=True, null=True, verbose_name='PCB information')), - ('discrepancy_info', models.JSONField(blank=True, null=True, verbose_name='discrepancy-residue information')), - ('epa_waste', models.BooleanField(verbose_name='EPA waste')), - ('additional_info', models.JSONField(blank=True, null=True)), - ('manifest', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='wastes', to='trak.manifest')), + ( + "id", + models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ("dot_hazardous", models.BooleanField(verbose_name="DOT hazardous")), + ( + "dot_info", + models.JSONField(blank=True, null=True, verbose_name="DOT information"), + ), + ("quantity", models.JSONField(blank=True, null=True)), + ("hazardous_waste", models.JSONField(blank=True, null=True)), + ("line_number", models.PositiveIntegerField(verbose_name="waste line number")), + ("br", models.BooleanField(verbose_name="BR info provided")), + ( + "br_info", + models.JSONField(blank=True, null=True, verbose_name="BR information"), + ), + ( + "management_method", + models.JSONField(blank=True, null=True, verbose_name="management method code"), + ), + ("pcb", models.BooleanField(verbose_name="contains PCBs")), + ( + "pcb_infos", + models.JSONField(blank=True, null=True, verbose_name="PCB information"), + ), + ( + "discrepancy_info", + models.JSONField( + blank=True, null=True, verbose_name="discrepancy-residue information" + ), + ), + ("epa_waste", models.BooleanField(verbose_name="EPA waste")), + ("additional_info", models.JSONField(blank=True, null=True)), + ( + "manifest", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="wastes", + to="trak.manifest", + ), + ), ], options={ - 'ordering': ['manifest__mtn', 'line_number'], + "ordering": ["manifest__mtn", "line_number"], }, ), migrations.CreateModel( - name='Signer', + name="Signer", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('rcra_user_id', models.CharField(blank=True, max_length=100, null=True)), - ('first_name', models.CharField(blank=True, max_length=38, null=True)), - ('middle_initial', models.CharField(blank=True, max_length=1, null=True)), - ('last_name', models.CharField(blank=True, max_length=38, null=True)), - ('email', models.CharField(blank=True, max_length=38, null=True)), - ('company_name', models.CharField(blank=True, max_length=80, null=True)), - ('contact_type', models.CharField(blank=True, choices=[('email', 'Email'), ('voice', 'Voice'), ('text', 'Text')], max_length=5, null=True)), - ('signer_role', models.CharField(choices=[('Industry', 'Industry'), ('PPC', 'Ppc'), ('EPA', 'Epa'), ('State', 'State')], max_length=10, null=True)), - ('phone', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='trak.manifestphone')), + ( + "id", + models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ("rcra_user_id", models.CharField(blank=True, max_length=100, null=True)), + ("first_name", models.CharField(blank=True, max_length=38, null=True)), + ("middle_initial", models.CharField(blank=True, max_length=1, null=True)), + ("last_name", models.CharField(blank=True, max_length=38, null=True)), + ("email", models.CharField(blank=True, max_length=38, null=True)), + ("company_name", models.CharField(blank=True, max_length=80, null=True)), + ( + "contact_type", + models.CharField( + blank=True, + choices=[("email", "Email"), ("voice", "Voice"), ("text", "Text")], + max_length=5, + null=True, + ), + ), + ( + "signer_role", + models.CharField( + choices=[ + ("Industry", "Industry"), + ("PPC", "Ppc"), + ("EPA", "Epa"), + ("State", "State"), + ], + max_length=10, + null=True, + ), + ), + ( + "phone", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="trak.manifestphone", + ), + ), ], options={ - 'ordering': ['first_name'], + "ordering": ["first_name"], }, ), migrations.CreateModel( - name='ImportInfo', + name="ImportInfo", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('import_generator', models.JSONField(blank=True, null=True)), - ('port_of_entry', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='trak.portofentry')), + ( + "id", + models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ("import_generator", models.JSONField(blank=True, null=True)), + ( + "port_of_entry", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.PROTECT, + to="trak.portofentry", + ), + ), ], options={ - 'verbose_name': 'Import Info', - 'verbose_name_plural': 'Import Info', + "verbose_name": "Import Info", + "verbose_name_plural": "Import Info", }, ), migrations.AddField( - model_name='handler', - name='paper_signature', - field=models.OneToOneField(blank=True, help_text='The signature associated with hazardous waste custody exchange', null=True, on_delete=django.db.models.deletion.CASCADE, to='trak.papersignature'), + model_name="handler", + name="paper_signature", + field=models.OneToOneField( + blank=True, + help_text="The signature associated with hazardous waste custody exchange", + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="trak.papersignature", + ), ), migrations.AddField( - model_name='handler', - name='rcra_site', - field=models.ForeignKey(help_text='Hazardous waste rcra_site associated with the manifest', on_delete=django.db.models.deletion.CASCADE, to='sites.rcrasite'), + model_name="handler", + name="rcra_site", + field=models.ForeignKey( + help_text="Hazardous waste rcra_site associated with the manifest", + on_delete=django.db.models.deletion.CASCADE, + to="site.rcrasite", + ), ), migrations.CreateModel( - name='ESignature', + name="ESignature", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('sign_date', models.DateTimeField(blank=True, null=True)), - ('cromerr_activity_id', models.CharField(blank=True, max_length=100, null=True)), - ('cromerr_document_id', models.CharField(blank=True, max_length=100, null=True)), - ('on_behalf', models.BooleanField(blank=True, default=False, null=True)), - ('manifest_handler', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='e_signatures', to='trak.handler')), - ('signer', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='trak.signer')), + ( + "id", + models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ("sign_date", models.DateTimeField(blank=True, null=True)), + ("cromerr_activity_id", models.CharField(blank=True, max_length=100, null=True)), + ("cromerr_document_id", models.CharField(blank=True, max_length=100, null=True)), + ("on_behalf", models.BooleanField(blank=True, default=False, null=True)), + ( + "manifest_handler", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="e_signatures", + to="trak.handler", + ), + ), + ( + "signer", + models.OneToOneField( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="trak.signer", + ), + ), ], options={ - 'verbose_name': 'e-Signature', - 'ordering': ['sign_date'], + "verbose_name": "e-Signature", + "ordering": ["sign_date"], }, ), migrations.CreateModel( - name='CorrectionInfo', + name="CorrectionInfo", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('version_number', models.IntegerField(blank=True, null=True)), - ('active', models.BooleanField(blank=True, null=True)), - ('ppc_active', models.BooleanField(blank=True, null=True)), - ('epa_site_id', models.CharField(blank=True, max_length=100, null=True)), - ('initiator_role', models.CharField(blank=True, choices=[('IN', 'Industry'), ('PP', 'Ppc'), ('EP', 'Epa'), ('ST', 'State')], max_length=2, null=True)), - ('update_role', models.CharField(blank=True, choices=[('IN', 'Industry'), ('PP', 'Ppc'), ('EP', 'Epa'), ('ST', 'State')], max_length=2, null=True)), - ('electronic_signature_info', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='trak.esignature')), + ( + "id", + models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ("version_number", models.IntegerField(blank=True, null=True)), + ("active", models.BooleanField(blank=True, null=True)), + ("ppc_active", models.BooleanField(blank=True, null=True)), + ("epa_site_id", models.CharField(blank=True, max_length=100, null=True)), + ( + "initiator_role", + models.CharField( + blank=True, + choices=[ + ("IN", "Industry"), + ("PP", "Ppc"), + ("EP", "Epa"), + ("ST", "State"), + ], + max_length=2, + null=True, + ), + ), + ( + "update_role", + models.CharField( + blank=True, + choices=[ + ("IN", "Industry"), + ("PP", "Ppc"), + ("EP", "Epa"), + ("ST", "State"), + ], + max_length=2, + null=True, + ), + ), + ( + "electronic_signature_info", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.PROTECT, + to="trak.esignature", + ), + ), ], options={ - 'verbose_name': 'Correction Info', - 'verbose_name_plural': 'Correction Info', + "verbose_name": "Correction Info", + "verbose_name_plural": "Correction Info", }, ), migrations.CreateModel( - name='Transporter', + name="Transporter", fields=[ - ('handler_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='trak.handler')), - ('order', models.PositiveIntegerField()), - ('manifest', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='transporters', to='trak.manifest')), + ( + "handler_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="trak.handler", + ), + ), + ("order", models.PositiveIntegerField()), + ( + "manifest", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="transporters", + to="trak.manifest", + ), + ), ], options={ - 'ordering': ['manifest__mtn'], + "ordering": ["manifest__mtn"], }, - bases=('trak.handler',), + bases=("trak.handler",), ), ] diff --git a/server/apps/trak/models/handler_models.py b/server/apps/trak/models/handler_models.py index 5a0537084..b998f8948 100644 --- a/server/apps/trak/models/handler_models.py +++ b/server/apps/trak/models/handler_models.py @@ -4,7 +4,7 @@ from django.core.exceptions import ValidationError from django.db import models -from apps.sites.models import RcraSite +from apps.site.models import RcraSite from . import ManifestPhone from .base_models import TrakBaseManager, TrakBaseModel @@ -60,7 +60,7 @@ class Meta: objects = HandlerManager() rcra_site = models.ForeignKey( - "sites.RcraSite", + "site.RcraSite", on_delete=models.CASCADE, help_text="Hazardous waste rcra_site associated with the manifest", ) diff --git a/server/apps/trak/models/manifest_models.py b/server/apps/trak/models/manifest_models.py index 606fb9c27..e8d68a7eb 100644 --- a/server/apps/trak/models/manifest_models.py +++ b/server/apps/trak/models/manifest_models.py @@ -7,7 +7,7 @@ from django.db.models import Q, QuerySet from django.utils.translation import gettext_lazy as _ -from apps.sites.models import RcraSiteType, RcraStates, Role +from apps.site.models import RcraSiteType, RcraStates, Role from apps.trak.models import Handler, Transporter from .base_models import TrakBaseManager, TrakBaseModel diff --git a/server/apps/trak/serializers/handler_serializer.py b/server/apps/trak/serializers/handler_serializer.py index a5522746f..eaa3d762f 100644 --- a/server/apps/trak/serializers/handler_serializer.py +++ b/server/apps/trak/serializers/handler_serializer.py @@ -3,7 +3,7 @@ from rest_framework import serializers from rest_framework.exceptions import ValidationError -from apps.sites.serializers import RcraSiteSerializer +from apps.site.serializers import RcraSiteSerializer from apps.trak.models import Handler, ManifestPhone, Transporter from .signature_serializer import ESignatureSerializer, PaperSignatureSerializer diff --git a/server/apps/trak/serializers/manifest_serializer.py b/server/apps/trak/serializers/manifest_serializer.py index 0b87aafe7..3968b0341 100644 --- a/server/apps/trak/serializers/manifest_serializer.py +++ b/server/apps/trak/serializers/manifest_serializer.py @@ -3,7 +3,7 @@ from rest_framework import serializers -from apps.sites.models import RcraStates, Role +from apps.site.models import RcraStates, Role from apps.trak.models import Manifest from apps.trak.models.manifest_models import ( AdditionalInfo, diff --git a/server/apps/trak/serializers/signature_serializer.py b/server/apps/trak/serializers/signature_serializer.py index af9c0a7a2..dac273ac7 100644 --- a/server/apps/trak/serializers/signature_serializer.py +++ b/server/apps/trak/serializers/signature_serializer.py @@ -4,7 +4,7 @@ from rest_framework import serializers -from apps.sites.serializers.contact_serializer import RcraPhoneSerializer +from apps.site.serializers.contact_serializer import RcraPhoneSerializer from apps.trak.models import ESignature, PaperSignature, QuickerSign, Signer from .base_serializer import TrakBaseSerializer diff --git a/server/apps/trak/services/manifest_services.py b/server/apps/trak/services/manifest_services.py index 49a3276c5..3786f29f9 100644 --- a/server/apps/trak/services/manifest_services.py +++ b/server/apps/trak/services/manifest_services.py @@ -4,7 +4,7 @@ from django.db import transaction from django.db.models import Q, QuerySet -from apps.sites.models import HaztrakSite +from apps.site.models import HaztrakSite from apps.trak.models import Manifest from apps.trak.serializers import ManifestSerializer from apps.trak.services import EManifest, EManifestError, TaskResponse diff --git a/server/apps/trak/signals.py b/server/apps/trak/signals.py index 3e481162e..e9630bae1 100644 --- a/server/apps/trak/signals.py +++ b/server/apps/trak/signals.py @@ -2,13 +2,13 @@ from django.db.models.signals import post_save from django.dispatch import receiver -from apps.core.models import RcraProfile +from apps.core.models import RcrainfoProfile @receiver(post_save, sender=User) def create_profile(sender, instance, created, **kwargs): if created: - RcraProfile.objects.create(user=instance) + RcrainfoProfile.objects.create(user=instance) @receiver(post_save, sender=User) diff --git a/server/apps/trak/tasks/manifest_task.py b/server/apps/trak/tasks/manifest_task.py index 8fa6152c6..929402748 100644 --- a/server/apps/trak/tasks/manifest_task.py +++ b/server/apps/trak/tasks/manifest_task.py @@ -57,7 +57,7 @@ def sign_manifest( @shared_task(name="sync site manifests", bind=True) def sync_site_manifests(self, *, site_id: str, username: str): """asynchronous task to sync an EPA site's manifests""" - from apps.sites.services import HaztrakSiteService + from apps.site.services import HaztrakSiteService try: site_service = HaztrakSiteService(username=username) diff --git a/server/apps/trak/tests/conftest.py b/server/apps/trak/tests/conftest.py index 7598f0042..e77b0e2bc 100644 --- a/server/apps/trak/tests/conftest.py +++ b/server/apps/trak/tests/conftest.py @@ -7,7 +7,7 @@ from faker import Faker from faker.providers import BaseProvider -from apps.sites.models import RcraSite, RcraSiteType +from apps.site.models import RcraSite, RcraSiteType from apps.trak.models import ( DotLookup, ESignature, @@ -38,8 +38,8 @@ def manifest_handler_factory(db, rcra_site_factory, paper_signature_factory): """Abstract factory for Haztrak Handler model""" def create_manifest_handler( - rcra_site: Optional[RcraSite] = None, - paper_signature: Optional[PaperSignature] = None, + rcra_site: Optional[RcraSite] = None, + paper_signature: Optional[PaperSignature] = None, ) -> Handler: return Handler.objects.create( rcra_site=rcra_site or rcra_site_factory(), @@ -54,10 +54,10 @@ def manifest_transporter_factory(db, rcra_site_factory, paper_signature_factory) """Abstract factory for Haztrak Handler model""" def create_manifest_handler( - rcra_site: Optional[RcraSite] = None, - paper_signature: Optional[PaperSignature] = None, - manifest: Manifest = None, - order: Optional[int] = 1, + rcra_site: Optional[RcraSite] = None, + paper_signature: Optional[PaperSignature] = None, + manifest: Manifest = None, + order: Optional[int] = 1, ) -> Transporter: return Transporter.objects.create( manifest=manifest, @@ -74,8 +74,8 @@ def paper_signature_factory(db, faker: Faker): """Abstract factory for Paper Signature""" def create_signature( - printed_name: Optional[str] = None, - sign_date: Optional[datetime] = None, + printed_name: Optional[str] = None, + sign_date: Optional[datetime] = None, ) -> PaperSignature: return PaperSignature.objects.create( printed_name=printed_name or faker.name(), @@ -90,8 +90,8 @@ def e_signature_factory(db, signer_factory, manifest_handler_factory, faker: Fak """Abstract factory for Haztrak Handler model""" def create_e_signature( - signer: Optional[Signer] = None, - manifest_handler: Optional[Handler] = None, + signer: Optional[Signer] = None, + manifest_handler: Optional[Handler] = None, ) -> ESignature: return ESignature.objects.create( signer=signer or signer_factory(), @@ -110,10 +110,10 @@ def waste_code_factory(db): """Abstract factory for waste codes""" def create_waste_code( - code: Optional[str] = "D001", - description: Optional[str] = "IGNITABLE WASTE", - code_type: Optional[WasteCode.CodeType] = WasteCode.CodeType.FEDERAL, - state_id: Optional[str] = "VA", + code: Optional[str] = "D001", + description: Optional[str] = "IGNITABLE WASTE", + code_type: Optional[WasteCode.CodeType] = WasteCode.CodeType.FEDERAL, + state_id: Optional[str] = "VA", ) -> WasteCode: if code_type == WasteCode.CodeType.STATE: waste_code = WasteCode.objects.create( @@ -138,12 +138,12 @@ def signer_factory(db, faker: Faker): """Abstract factory for Haztrak Signer model""" def creat_signer( - first_name: Optional[str] = None, - middle_initial: Optional[str] = None, - last_name: Optional[str] = None, - signer_role: Optional[str] = "EP", - company_name: Optional[str] = None, - rcra_user_id: Optional[str] = None, + first_name: Optional[str] = None, + middle_initial: Optional[str] = None, + last_name: Optional[str] = None, + signer_role: Optional[str] = "EP", + company_name: Optional[str] = None, + rcra_user_id: Optional[str] = None, ) -> Signer: return Signer.objects.create( first_name=first_name or faker.first_name(), @@ -162,10 +162,10 @@ def manifest_factory(db, manifest_handler_factory, rcra_site_factory): """Abstract factory for Haztrak Manifest model""" def create_manifest( - mtn: Optional[str] = None, - generator: Optional[Handler] = None, - tsdf: Optional[Handler] = None, - status: Optional[str] = None, + mtn: Optional[str] = None, + generator: Optional[Handler] = None, + tsdf: Optional[Handler] = None, + status: Optional[str] = None, ) -> Manifest: fake = Faker() fake.add_provider(ManifestProvider) @@ -175,12 +175,11 @@ def create_manifest( created_date=datetime.now(UTC), potential_ship_date=datetime.now(UTC), generator=generator - or manifest_handler_factory( + or manifest_handler_factory( rcra_site=rcra_site_factory(site_type=RcraSiteType.GENERATOR) ), tsdf=tsdf - or manifest_handler_factory( - rcra_site=rcra_site_factory(site_type=RcraSiteType.TSDF)), + or manifest_handler_factory(rcra_site=rcra_site_factory(site_type=RcraSiteType.TSDF)), ) return create_manifest @@ -191,13 +190,13 @@ def waste_line_factory(db): """Abstract factory for Haztrak DotLookup model""" def create_waste_line( - manifest: Manifest = None, - dot_hazardous: Optional[bool] = True, - quantity: Optional[dict] = None, - line_number: Optional[int] = 1, - br: Optional[bool] = False, - pcb: Optional[bool] = False, - epa_waste: Optional[bool] = True, + manifest: Manifest = None, + dot_hazardous: Optional[bool] = True, + quantity: Optional[dict] = None, + line_number: Optional[int] = 1, + br: Optional[bool] = False, + pcb: Optional[bool] = False, + epa_waste: Optional[bool] = True, ) -> WasteLine: return WasteLine.objects.create( manifest=manifest, @@ -217,8 +216,8 @@ def dot_option_factory(db, faker: Faker): """Abstract factory for Haztrak DotLookup model""" def create_dot_option( - value: Optional[str] = None, - value_type: Optional[DotLookupType] = DotLookupType.ID, + value: Optional[str] = None, + value_type: Optional[DotLookupType] = DotLookupType.ID, ) -> Manifest: return DotLookup.objects.create( value=value or faker.pystr(max_chars=10), value_type=value_type diff --git a/server/apps/trak/tests/models/test_manifest_models.py b/server/apps/trak/tests/models/test_manifest_models.py index 6db8351f3..533df5e95 100644 --- a/server/apps/trak/tests/models/test_manifest_models.py +++ b/server/apps/trak/tests/models/test_manifest_models.py @@ -4,7 +4,7 @@ from django.core.exceptions import ValidationError from django.db.models import Q -from apps.sites.models import RcraSiteType +from apps.site.models import RcraSiteType from apps.trak.models import WasteLine from apps.trak.models.manifest_models import Manifest, draft_mtn, validate_mtn from apps.trak.serializers import HandlerSerializer, WasteLineSerializer diff --git a/server/apps/trak/views/manifest_view.py b/server/apps/trak/views/manifest_view.py index ac43b449b..170b2d8f5 100644 --- a/server/apps/trak/views/manifest_view.py +++ b/server/apps/trak/views/manifest_view.py @@ -7,7 +7,7 @@ from rest_framework.response import Response from rest_framework.views import APIView -from apps.sites.services import HaztrakSiteService +from apps.site.services import HaztrakSiteService from apps.trak.models import Manifest from apps.trak.serializers import ManifestSerializer from apps.trak.serializers.signature_serializer import QuickerSignSerializer diff --git a/server/fixtures/dev_data.yaml b/server/fixtures/dev_data.yaml index da36978c2..a0f11d259 100644 --- a/server/fixtures/dev_data.yaml +++ b/server/fixtures/dev_data.yaml @@ -152,7 +152,7 @@ fields: user: 4ac96f68-42cf-47ea-bffb-f24d423dbc35 created: 2022-12-18 13:10:40.552000+00:00 -- model: sites.address +- model: site.address pk: 1 fields: street_number: null @@ -167,12 +167,12 @@ fields: number: 321-321-3214 extension: null -- model: sites.rcraphone +- model: site.rcraphone pk: 1 fields: number: 321-321-3214 extension: null -- model: sites.contact +- model: site.contact pk: 1 fields: first_name: David @@ -181,7 +181,7 @@ phone: 1 email: testuser1@haztrak.net company_name: haztrak -- model: sites.rcrasite +- model: site.rcrasite pk: 1 fields: site_type: Generator @@ -197,7 +197,7 @@ can_esign: true limited_esign: true registered_emanifest_user: true -- model: sites.rcrasite +- model: site.rcrasite pk: 2 fields: site_type: Transporter @@ -213,7 +213,7 @@ can_esign: true limited_esign: true registered_emanifest_user: true -- model: sites.rcrasite +- model: site.rcrasite pk: 3 fields: site_type: Transporter @@ -229,7 +229,7 @@ can_esign: true limited_esign: true registered_emanifest_user: true -- model: sites.rcrasite +- model: site.rcrasite pk: 4 fields: site_type: Tsdf @@ -245,25 +245,25 @@ can_esign: true limited_esign: true registered_emanifest_user: true -- model: core.rcraprofile +- model: core.rcrainfoprofile pk: d74f904a-7843-4a60-862c-3b94b6051359 fields: rcra_username: dpgraham4401 phone_number: null email: orgadmin@generator.com -- model: core.rcraprofile +- model: core.rcrainfoprofile pk: 1fd27bec-8743-4eb3-a44c-fd063ea62021 fields: rcra_username: '' phone_number: null email: superadmin@gmail.com -- model: core.rcraprofile +- model: core.rcrainfoprofile pk: 192c73f4-24f1-4f21-8239-dee2da43c547 fields: rcra_username: emanifestpyuser1 phone_number: null email: testuser1@haztrak.net -- model: sites.haztrakorg +- model: site.haztrakorg pk: efb9e104-7f61-4365-a9af-9d7b55c854c4 fields: name: Generators Org LLC @@ -286,20 +286,20 @@ user: 4ac96f68-42cf-47ea-bffb-f24d423dbc35 rcrainfo_profile: 192c73f4-24f1-4f21-8239-dee2da43c547 org: efb9e104-7f61-4365-a9af-9d7b55c854c4 -- model: sites.haztraksite +- model: site.haztraksite pk: 1 fields: name: VA TEST GEN 2021 rcra_site: 1 last_rcrainfo_manifest_sync: null org: efb9e104-7f61-4365-a9af-9d7b55c854c4 -- model: sites.sitepermissions +- model: site.sitepermissions pk: 1 fields: site: 1 emanifest: signer profile: 87e355dd-1898-4a0a-81c3-1e9ac8473143 -- model: sites.rcrasitepermissions +- model: site.rcrasitepermissions pk: 1 fields: site: 1 diff --git a/server/haztrak/settings/base.py b/server/haztrak/settings/base.py index 04ec76aad..8d65278a9 100644 --- a/server/haztrak/settings/base.py +++ b/server/haztrak/settings/base.py @@ -22,28 +22,34 @@ WSGI_APPLICATION = "haztrak.wsgi.application" +SITE_ID = 1 + INSTALLED_APPS = [ "django.contrib.admin", "django.contrib.auth", + "django.contrib.sites", "django.contrib.contenttypes", "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", "rest_framework", "rest_framework.authtoken", - "drf_spectacular", + "allauth", + "allauth.account", + "dj_rest_auth", "corsheaders", "django_extensions", - "django_celery_results", - "django_celery_beat", "health_check", "health_check.db", "health_check.cache", "health_check.contrib.celery", "health_check.contrib.migrations", "health_check.contrib.redis", + "django_celery_results", + "django_celery_beat", + "drf_spectacular", "apps.trak", - "apps.sites", + "apps.site", "apps.core", ] @@ -54,8 +60,9 @@ "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", - "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "allauth.account.middleware.AccountMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", ] @@ -101,6 +108,10 @@ "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", }, ] +AUTHENTICATION_BACKENDS = [ + "django.contrib.auth.backends.ModelBackend", + "allauth.account.auth_backends.AuthenticationBackend", +] # Internationalization LANGUAGE_CODE = "en-us" @@ -198,7 +209,7 @@ "handlers": ["console"], "propagate": False, }, - "apps.sites": { + "apps.site": { "level": HT_TRAK_LOG_LEVEL, "handlers": ["console"], "propagate": False, @@ -210,3 +221,5 @@ }, }, } + +REST_AUTH = {"USER_DETAILS_SERIALIZER": "apps.core.serializers.HaztrakUserSerializer"} diff --git a/server/haztrak/urls.py b/server/haztrak/urls.py index 00e272c82..3d29f2f04 100644 --- a/server/haztrak/urls.py +++ b/server/haztrak/urls.py @@ -29,7 +29,7 @@ [ path("", include("apps.core.urls")), path("", include("apps.trak.urls")), - path("", include("apps.sites.urls")), + path("", include("apps.site.urls")), path("schema/", SpectacularAPIView.as_view(), name="schema"), path( "schema/swagger-ui", diff --git a/server/requirements.txt b/server/requirements.txt index 2fb600263..3cc759aa0 100644 --- a/server/requirements.txt +++ b/server/requirements.txt @@ -16,3 +16,5 @@ celery==5.3.6 redis==5.0.1 drf-spectacular==0.26.5 django-health-check==3.17.0 +django-allauth==0.59.0 +dj-rest-auth==5.0.2