diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md index 463b5d2f6..b2485592a 100644 --- a/.github/ISSUE_TEMPLATE/bug.md +++ b/.github/ISSUE_TEMPLATE/bug.md @@ -1,5 +1,5 @@ --- -name: ':bug: Bug Report' +name: '🐞 Bug Report' about: 'Report an issue.' title: '' labels: 'bug' diff --git a/.github/ISSUE_TEMPLATE/other.md b/.github/ISSUE_TEMPLATE/other.md index cd13edb7e..cca855004 100644 --- a/.github/ISSUE_TEMPLATE/other.md +++ b/.github/ISSUE_TEMPLATE/other.md @@ -1,5 +1,5 @@ --- -name: ':nut_and_bolt: Something else' +name: 'Something Else' about: 'Questions, discussions, chores, or other' title: '' labels: 'chore' diff --git a/client/src/App.tsx b/client/src/App.tsx index eea5203e0..8303346a9 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -18,6 +18,7 @@ import './App.scss'; import { getProfile } from 'store/rcraProfileSlice'; import { selectRcraProfile } from 'store/rcraProfileSlice'; import { selectUserName } from 'store/userSlice'; +import { getHaztrakUser } from 'store/userSlice/user.slice'; function App(): ReactElement { const userName = useAppSelector(selectUserName); @@ -28,6 +29,7 @@ function App(): ReactElement { useEffect(() => { if (userName) { dispatch(getProfile()); + dispatch(getHaztrakUser()); } }, [profile.user]); diff --git a/client/src/components/Ht/HtForm.tsx b/client/src/components/Ht/HtForm.tsx index 755418987..a1ad728e6 100644 --- a/client/src/components/Ht/HtForm.tsx +++ b/client/src/components/Ht/HtForm.tsx @@ -32,7 +32,7 @@ HtForm.InputGroup = function (props: InputGroupProps): ReactElement { HtForm.Label = function (props: FormLabelProps): ReactElement { return ( - + {props.children} ); diff --git a/client/src/components/Nav/Sidebar.spec.tsx b/client/src/components/Nav/Sidebar.spec.tsx index 98b70f692..cefeecbe7 100644 --- a/client/src/components/Nav/Sidebar.spec.tsx +++ b/client/src/components/Nav/Sidebar.spec.tsx @@ -9,17 +9,17 @@ afterEach(() => { describe('Sidebar', () => { test('renders when use is logged in', () => { - const userName = 'testuser1'; + const username = 'testuser1'; renderWithProviders(, { preloadedState: { user: { - user: userName, + user: { username: username, isLoading: false }, token: 'fakeToken', loading: false, }, }, }); - expect(screen.getByText(userName)).toBeInTheDocument(); + expect(screen.getByText(username)).toBeInTheDocument(); }); test('returns nothing when user not logged in', () => { const userName = 'testuser1'; diff --git a/client/src/components/Nav/Sidebar.tsx b/client/src/components/Nav/Sidebar.tsx index f664f5100..4bb46be77 100644 --- a/client/src/components/Nav/Sidebar.tsx +++ b/client/src/components/Nav/Sidebar.tsx @@ -147,7 +147,7 @@ export function Sidebar(): ReactElement | null {
Logged in as:
- {authUser} + {authUser.username}
diff --git a/client/src/components/Nav/TopNav.spec.tsx b/client/src/components/Nav/TopNav.spec.tsx index e915df6a1..24eb9f2c3 100644 --- a/client/src/components/Nav/TopNav.spec.tsx +++ b/client/src/components/Nav/TopNav.spec.tsx @@ -9,11 +9,11 @@ afterEach(() => { describe('TopNav', () => { test('renders when user is logged in', () => { - const userName = 'testuser1'; + const username = 'testuser1'; renderWithProviders(, { preloadedState: { user: { - user: userName, + user: { username: username, isLoading: false }, token: 'fakeToken', loading: false, }, diff --git a/client/src/features/home/Home.spec.tsx b/client/src/features/home/Home.spec.tsx index 5a7967f1f..789df382f 100644 --- a/client/src/features/home/Home.spec.tsx +++ b/client/src/features/home/Home.spec.tsx @@ -22,7 +22,7 @@ describe('Home', () => { renderWithProviders(, { preloadedState: { user: { - user: username, + user: { username: username, isLoading: false }, token: 'fake_token', loading: false, error: undefined, diff --git a/client/src/features/login/Login.tsx b/client/src/features/login/Login.tsx index 36cf49b16..46379ac32 100644 --- a/client/src/features/login/Login.tsx +++ b/client/src/features/login/Login.tsx @@ -5,7 +5,7 @@ import { useForm } from 'react-hook-form'; import { login, useAppDispatch, useAppSelector } from 'store'; import { useNavigate } from 'react-router-dom'; import { useTitle } from 'hooks'; -import { selectUser } from 'store/userSlice'; +import { selectUserState } from 'store/userSlice'; import { z } from 'zod'; import { Col, Container, Form, Row } from 'react-bootstrap'; import logo from 'assets/haztrak-logos/low-resolution/svg/haztrak-low-resolution-logo-black-on-transparent-background.svg'; @@ -24,7 +24,7 @@ type LoginSchema = z.infer; export function Login(): ReactElement { useTitle('Login'); const dispatch = useAppDispatch(); - const user = useAppSelector(selectUser); + const userState = useAppSelector(selectUserState); const navigation = useNavigate(); const { register, @@ -34,10 +34,10 @@ export function Login(): ReactElement { useEffect(() => { // redirect to home if already logged in - if (user.user) { + if (userState.user?.username) { navigation('/'); } - }, [user.user]); + }, [userState.user?.username]); function onSubmit({ username, password }: LoginSchema) { return dispatch(login({ username, password })); @@ -86,8 +86,8 @@ export function Login(): ReactElement { {isSubmitting && } Login - {user.error && ( -
{String(user.error)}
+ {userState.error && ( +
{String(userState.error)}
)} diff --git a/client/src/features/profile/Profile.tsx b/client/src/features/profile/Profile.tsx index 9c8aedde8..1015ebae4 100644 --- a/client/src/features/profile/Profile.tsx +++ b/client/src/features/profile/Profile.tsx @@ -1,4 +1,5 @@ import { HtCard } from 'components/Ht'; +import { HaztrakUser, selectUser } from 'store/userSlice/user.slice'; import { RcraProfile } from './RcraProfile'; import { UserProfile } from './UserProfile'; import { useTitle } from 'hooks'; @@ -14,8 +15,13 @@ import { getProfile, selectRcraProfile } from 'store/rcraProfileSlice'; export function Profile(): ReactElement { const dispatch = useAppDispatch(); const profile = useAppSelector(selectRcraProfile); + const user: HaztrakUser | undefined = useAppSelector(selectUser); useTitle('Profile'); + if (!user) { + return
loading...
; + } + useEffect(() => { dispatch(getProfile()); }, [profile.user]); @@ -31,7 +37,7 @@ export function Profile(): ReactElement { - + diff --git a/client/src/features/profile/RcraProfile.tsx b/client/src/features/profile/RcraProfile.tsx index 4b0f1d21a..0d8299aeb 100644 --- a/client/src/features/profile/RcraProfile.tsx +++ b/client/src/features/profile/RcraProfile.tsx @@ -15,7 +15,6 @@ interface ProfileViewProps { profile: RcraProfileState; } -// ToDo: Each field should be empty or meet the min length requirements // ToDo: Either rcraAPIId & rcraAPIID should both be empty or both be non-empty const rcraProfileForm = z.object({ rcraAPIID: z.string().min(36).optional(), @@ -28,7 +27,7 @@ type RcraProfileForm = z.infer; export function RcraProfile({ profile }: ProfileViewProps) { const [editable, setEditable] = useState(false); const [profileLoading, setProfileLoading] = useState(false); - const { error, rcraSites, loading, ...formValues } = profile; + const { rcraSites, loading, ...formValues } = profile; const dispatch = useAppDispatch(); const { diff --git a/client/src/features/profile/UserProfile.spec.tsx b/client/src/features/profile/UserProfile.spec.tsx new file mode 100644 index 000000000..3dd7e02b1 --- /dev/null +++ b/client/src/features/profile/UserProfile.spec.tsx @@ -0,0 +1,65 @@ +import { cleanup } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { UserProfile } from 'features/profile/UserProfile'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import React from 'react'; +import { HaztrakUser } from 'store/userSlice/user.slice'; +import { renderWithProviders, screen } from 'test-utils'; +import { API_BASE_URL } from 'test-utils/mock/handlers'; +import { vi } from 'vitest'; + +const DEFAULT_USER: HaztrakUser = { + username: 'test', + firstName: 'David', + lastName: 'smith', + email: 'test@mail.com', +}; + +const server = setupServer( + rest.put(`${API_BASE_URL}/api/user/`, (req, res, ctx) => { + const user: HaztrakUser = { ...DEFAULT_USER }; + // @ts-ignore + return res(ctx.status(200), ctx.json({ ...user, ...req.body })); + }) +); + +// pre-/post-test hooks +beforeAll(() => server.listen()); +afterEach(() => { + server.resetHandlers(); + cleanup(); + vi.resetAllMocks(); +}); +afterAll(() => server.close()); // Disable API mocking after the tests are done. + +describe('UserProfile', () => { + test('renders', () => { + const user: HaztrakUser = { + ...DEFAULT_USER, + username: 'test', + firstName: 'David', + }; + renderWithProviders(, {}); + expect(screen.getByRole('textbox', { name: 'First Name' })).toHaveValue(user.firstName); + expect(screen.getByText(user.username)).toBeInTheDocument(); + }); + test('update profile fields', async () => { + // Arrange + const newEmail = 'newMockEmail@mail.com'; + const user: HaztrakUser = { + ...DEFAULT_USER, + }; + renderWithProviders(, {}); + const editButton = screen.getByRole('button', { name: 'Edit' }); + const emailTextBox = screen.getByRole('textbox', { name: 'Email' }); + // Act + await userEvent.click(editButton); + await userEvent.clear(emailTextBox); + await userEvent.type(emailTextBox, newEmail); + const saveButton = screen.getByRole('button', { name: 'Save' }); + await userEvent.click(saveButton); + // Assert + expect(await screen.findByRole('textbox', { name: 'Email' })).toHaveValue(newEmail); + }); +}); diff --git a/client/src/features/profile/UserProfile.tsx b/client/src/features/profile/UserProfile.tsx index 0b3320afa..9c0c852b8 100644 --- a/client/src/features/profile/UserProfile.tsx +++ b/client/src/features/profile/UserProfile.tsx @@ -1,21 +1,155 @@ -import React from 'react'; -import { Button, Container } from 'react-bootstrap'; -import { RcraProfileState } from 'store/rcraProfileSlice/rcraProfile.slice'; +import { faUser } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { HtForm } from 'components/Ht'; +import React, { createRef, useState } from 'react'; +import { Button, Col, Container, Form, Row } from 'react-bootstrap'; +import { useForm } from 'react-hook-form'; +import { htApi } from 'services'; +import { useAppDispatch } from 'store'; +import { HaztrakUser, updateUserProfile } from 'store/userSlice/user.slice'; +import { z } from 'zod'; -interface ProfileViewProps { - profile: RcraProfileState; +interface UserProfileProps { + user: HaztrakUser; } -export function UserProfile({ profile }: ProfileViewProps) { +const haztrakUserForm = z.object({ + firstName: z.string().min(4, 'First name must be a least 4 character(s)').optional(), + lastName: z.string().min(5, 'Last name must be at least 5 character(s)').optional(), + email: z.string().email('Not a valid email address').optional(), +}); + +export function UserProfile({ user }: UserProfileProps) { + const [editable, setEditable] = useState(false); + const fileRef = createRef(); + const dispatch = useAppDispatch(); + + const { + register, + reset, + handleSubmit, + formState: { errors }, + } = useForm({ values: user, resolver: zodResolver(haztrakUserForm) }); + + const onSubmit = (data: any) => { + setEditable(!editable); + htApi + .put('/user/', data) + .then((r) => { + dispatch(updateUserProfile(r.data)); + }) + .catch((r) => console.error(r)); + }; + return ( - <> - -

Username

-

{profile.user}

-
- -
-
- + + + + + + +
+ +
+
+

{user.username}

+
+ +
+
+ + + + First Name + +
{errors.firstName?.message}
+
+ + + + Last Name + +
{errors.lastName?.message}
+
+ +
+ + + + Email + +
{errors.email?.message}
+
+ +
+ +
+ {!editable ? ( + <> + + + ) : ( + <> + + + + )} +
+
+
+
); } diff --git a/client/src/store/rcraProfileSlice/rcraProfile.slice.ts b/client/src/store/rcraProfileSlice/rcraProfile.slice.ts index cb65e5bbc..76f25e8c5 100644 --- a/client/src/store/rcraProfileSlice/rcraProfile.slice.ts +++ b/client/src/store/rcraProfileSlice/rcraProfile.slice.ts @@ -5,6 +5,7 @@ import { createAsyncThunk, createSelector, createSlice, PayloadAction } from '@reduxjs/toolkit'; import axios from 'axios'; import { HaztrakSite } from 'components/HaztrakSite'; +import { htApi } from 'services'; import { RootState } from 'store'; /** @@ -95,8 +96,8 @@ export const getProfile = createAsyncThunk( 'rcraProfile/getProfile', async (arg, thunkAPI) => { const state = thunkAPI.getState() as RootState; - const username = state.user.user; - const response = await axios.get(`${import.meta.env.VITE_HT_API_URL}/api/profile/${username}`); + const username = state.user.user?.username; + const response = await htApi.get(`${import.meta.env.VITE_HT_API_URL}/api/profile/${username}`); const { rcraSites, ...rest } = response.data as RcraProfileResponse; // Convert the array of RcraSite permissions we get from our backend // to an object which each key corresponding to the RcraSite's ID number diff --git a/client/src/store/userSlice/index.ts b/client/src/store/userSlice/index.ts index 8939923e7..b7d291a23 100644 --- a/client/src/store/userSlice/index.ts +++ b/client/src/store/userSlice/index.ts @@ -1,4 +1,4 @@ -import userReducer, { login, selectUserName, selectUser } from './user.slice'; +import userReducer, { login, selectUserName, selectUser, selectUserState } from './user.slice'; export default userReducer; -export { login, selectUserName, selectUser }; +export { login, selectUserName, selectUser, selectUserState }; diff --git a/client/src/store/userSlice/user.slice.ts b/client/src/store/userSlice/user.slice.ts index 80e92d173..333966e48 100644 --- a/client/src/store/userSlice/user.slice.ts +++ b/client/src/store/userSlice/user.slice.ts @@ -1,13 +1,22 @@ -import { createAsyncThunk, createSelector, createSlice } from '@reduxjs/toolkit'; +import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; import axios from 'axios'; -import { RcraProfileSite, RcraProfileState } from 'store/rcraProfileSlice/rcraProfile.slice'; +import { htApi } from 'services'; import { RootState } from 'store/rootStore'; +export interface HaztrakUser { + username: string; + email?: string; + firstName?: string; + lastName?: string; + isLoading?: boolean; + error?: string; +} + /** * The Redux stored information on the current haztrak user */ export interface UserState { - user?: string; + user?: HaztrakUser; token?: string; loading?: boolean; error?: string; @@ -15,7 +24,7 @@ export interface UserState { const initialState: UserState = { // Retrieve the user's username and token from local storage. For convenience - user: JSON.parse(localStorage.getItem('user') || 'null') || null, + user: { username: JSON.parse(localStorage.getItem('user') || 'null') || null, isLoading: false }, token: JSON.parse(localStorage.getItem('token') || 'null') || null, loading: false, error: undefined, @@ -28,10 +37,23 @@ export const login = createAsyncThunk( username, password, }); - return response.data as UserState; + // return response.data as UserState; + return { + user: { username: response.data.user }, + token: response.data.token, + } as UserState; } ); +export const getHaztrakUser = createAsyncThunk('user/getHaztrakUser', async (arg, thunkAPI) => { + const response = await htApi.get(`${import.meta.env.VITE_HT_API_URL}/api/user`); + if (response.status >= 200 && response.status < 300) { + return response.data as HaztrakUser; + } else { + return thunkAPI.rejectWithValue(response.data); + } +}); + /** * User logout Redux reducer Function * @@ -47,11 +69,21 @@ function logout(user: UserState) { return { ...initialState, user: undefined, token: undefined } as UserState; } +/** + * update the HaztrakUser state with the new user information + */ + const userSlice = createSlice({ name: 'user', initialState, reducers: { logout, + updateUserProfile(state: UserState, action: any) { + return { + ...state, + user: action.payload, + }; + }, }, extraReducers: (builder) => { builder @@ -67,7 +99,7 @@ const userSlice = createSlice({ // Todo: currently, we store username and jwt token in local storage so // the user stays logged in between page refreshes. This is a known vulnerability to be // fixed in the future. For now, it's a development convenience. - localStorage.setItem('user', JSON.stringify(authResponse.user)); + localStorage.setItem('user', JSON.stringify(authResponse.user?.username)); localStorage.setItem('token', JSON.stringify(authResponse.token)); return { loading: false, @@ -82,6 +114,28 @@ const userSlice = createSlice({ 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, + }; }); }, }); @@ -89,11 +143,17 @@ const userSlice = createSlice({ /** * Get the current user's username from the Redux store */ -export const selectUserName = (state: RootState) => state.user.user; +export const selectUserName = (state: RootState): string | undefined => state.user.user?.username; /** * Select the current user */ -export const selectUser = (state: RootState) => state.user; +export const selectUser = (state: RootState): HaztrakUser | undefined => state.user.user; + +/** + * Select the current User State + */ +export const selectUserState = (state: RootState): UserState => state.user; export default userSlice.reducer; +export const { updateUserProfile } = userSlice.actions; diff --git a/client/src/store/userSlice/user.spec.ts b/client/src/store/userSlice/user.spec.ts index ba7aeb705..c40ddfc3c 100644 --- a/client/src/store/userSlice/user.spec.ts +++ b/client/src/store/userSlice/user.spec.ts @@ -12,7 +12,7 @@ const initialState: UserState = { }; const userPayload: UserState = { - user: 'testuser1', + user: { username: 'testuser1' }, token: 'mockToken', }; diff --git a/docs/haztrak_book/src/assets/erd.png b/docs/haztrak_book/src/assets/erd.png new file mode 100644 index 000000000..af299cca3 Binary files /dev/null and b/docs/haztrak_book/src/assets/erd.png differ diff --git a/runhaz.sh b/runhaz.sh index 17ebce6b5..891e37d26 100755 --- a/runhaz.sh +++ b/runhaz.sh @@ -87,7 +87,7 @@ generate_api_schema() { graph_models() { print_style "Generating Entity Relationship Diagram...\n" "success"; - exec_cmd="$base_py_cmd graph_models sites trak \ + exec_cmd="$base_py_cmd graph_models sites trak core\ -g \ --settings haztrak.settings \ --rankdir=RL \ diff --git a/server/apps/conftest.py b/server/apps/conftest.py index 75f8213d0..9032bdb32 100644 --- a/server/apps/conftest.py +++ b/server/apps/conftest.py @@ -11,12 +11,11 @@ from django.contrib.auth.models import User from rest_framework.test import APIClient -from apps.core.models import HaztrakUser +from apps.core.models import HaztrakUser, RcraProfile from apps.sites.models import ( Address, Contact, RcraPhone, - RcraProfile, RcraSite, Site, ) diff --git a/server/apps/core/migrations/0001_initial.py b/server/apps/core/migrations/0001_initial.py index 3c7dc83db..62a8fe5f5 100644 --- a/server/apps/core/migrations/0001_initial.py +++ b/server/apps/core/migrations/0001_initial.py @@ -1,8 +1,10 @@ -# Generated by Django 4.1.7 on 2023-05-06 13:55 +# Generated by Django 4.2.1 on 2023-06-12 18:32 +from django.conf import settings import django.contrib.auth.models import django.contrib.auth.validators from django.db import migrations, models +import django.db.models.deletion import django.utils.timezone @@ -113,4 +115,29 @@ class Migration(migrations.Migration): ("objects", django.contrib.auth.models.UserManager()), ], ), + migrations.CreateModel( + name="RcraProfile", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ("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)), + ( + "user", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL + ), + ), + ], + options={ + "ordering": ["rcra_username"], + }, + ), ] diff --git a/server/apps/core/models.py b/server/apps/core/models.py index ef61624d3..bc5f0d494 100644 --- a/server/apps/core/models.py +++ b/server/apps/core/models.py @@ -1,5 +1,80 @@ from django.contrib.auth.models import AbstractUser +from django.db import models + +from haztrak import settings + + +class CoreBaseModel(models.Model): + """Base class for all apps.core models""" + + class Meta: + abstract = True + ordering = ["pk"] + + def __str__(self): + return f"{self.__class__.__name__}" + + def __repr__(self): + field_values = ", ".join( + f"{field.name}={getattr(self, field.name)!r}" for field in self._meta.fields + ) + return f"<{self.__class__.__name__}({field_values})>" class HaztrakUser(AbstractUser): + """Haztrak abstract user model. It simply inherits from Django's AbstractUser model.""" + pass + + +class RcraProfile(CoreBaseModel): + """ + Contains a user's RcraProfile information, such as username, and API credentials. + Has a one-to-one relationship with the User model. + """ + + class Meta: + ordering = ["rcra_username"] + + user = models.OneToOneField( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + ) + rcra_api_key = models.CharField( + max_length=128, + null=True, + blank=True, + ) + rcra_api_id = models.CharField( + max_length=128, + null=True, + blank=True, + ) + rcra_username = models.CharField( + max_length=128, + null=True, + blank=True, + ) + phone_number = models.CharField( + max_length=15, + null=True, + blank=True, + ) + email = models.EmailField() + + def __str__(self): + return f"{self.user.username}" + + def sync(self): + """Launch task to sync use profile. ToDo: remove this method""" + from apps.sites.tasks import sync_user_sites + + task = sync_user_sites.delay(str(self.user.username)) + return task + + @property + def is_api_user(self) -> bool: + """Returns true if the use has Rcrainfo API credentials""" + if self.rcra_username and self.rcra_api_id and self.rcra_api_key: + return True + return False diff --git a/server/apps/core/serializers.py b/server/apps/core/serializers.py new file mode 100644 index 000000000..75b5bcd03 --- /dev/null +++ b/server/apps/core/serializers.py @@ -0,0 +1,79 @@ +from rest_framework import serializers +from rest_framework.serializers import ModelSerializer + +from apps.core.models import HaztrakUser, RcraProfile +from apps.sites.serializers import RcraSitePermissionSerializer + + +class HaztrakUserSerializer(ModelSerializer): + """ + Model serializer for marshalling/unmarshalling a user's HaztrakUser + """ + + username = serializers.CharField( + required=False, + ) + firstName = serializers.CharField( + source="first_name", + required=False, + ) + lastName = serializers.CharField( + source="last_name", + required=False, + ) + + class Meta: + model = HaztrakUser + fields = [ + "username", + "firstName", + "lastName", + "email", + ] + + +class RcraProfileSerializer(ModelSerializer): + """ + Model serializer for marshalling/unmarshalling a user's RcraProfile + """ + + user = serializers.StringRelatedField() + rcraSites = RcraSitePermissionSerializer( + source="permissions", + required=False, + many=True, + ) + phoneNumber = serializers.CharField( + source="phone_number", + required=False, + ) + rcraAPIID = serializers.CharField( + source="rcra_api_id", + required=False, + ) + rcraAPIKey = serializers.CharField( + source="rcra_api_key", + required=False, + write_only=True, + ) + rcraUsername = serializers.CharField( + source="rcra_username", + required=False, + ) + apiUser = serializers.BooleanField( + source="is_api_user", + required=False, + allow_null=False, + ) + + class Meta: + model = RcraProfile + fields = [ + "user", + "rcraAPIID", + "rcraAPIKey", + "rcraUsername", + "rcraSites", + "phoneNumber", + "apiUser", + ] diff --git a/server/apps/core/services/rcrainfo_service.py b/server/apps/core/services/rcrainfo_service.py index a84c42933..3663ccd32 100644 --- a/server/apps/core/services/rcrainfo_service.py +++ b/server/apps/core/services/rcrainfo_service.py @@ -4,7 +4,7 @@ from django.db import IntegrityError from emanifest import RcrainfoClient, RcrainfoResponse -from apps.sites.models.profile_models import RcraProfile +from apps.core.models import RcraProfile from apps.trak.models import WasteCode diff --git a/server/apps/core/urls.py b/server/apps/core/urls.py index a1fecd9ed..9c3f970ec 100644 --- a/server/apps/core/urls.py +++ b/server/apps/core/urls.py @@ -1,8 +1,21 @@ from django.urls import path -from .views import ExampleTaskView, Login, TaskStatusView +from .views import ( + ExampleTaskView, + HaztrakUserView, + Login, + RcraProfileView, + RcraSitePermissionView, + SyncProfileView, + TaskStatusView, +) urlpatterns = [ + # Rcra Profile + path("profile//sync", SyncProfileView.as_view()), + path("profile/", RcraProfileView.as_view()), + path("site/permission/", RcraSitePermissionView.as_view()), + path("user/", HaztrakUserView.as_view()), path("user/login/", Login.as_view()), path("task/example", ExampleTaskView.as_view()), path("task/", TaskStatusView.as_view()), diff --git a/server/apps/core/views/__init__.py b/server/apps/core/views/__init__.py index 2c99aff1c..bb9d5b890 100644 --- a/server/apps/core/views/__init__.py +++ b/server/apps/core/views/__init__.py @@ -1,2 +1,8 @@ from .auth_view import Login +from .profile_views import ( + HaztrakUserView, + RcraProfileView, + RcraSitePermissionView, + SyncProfileView, +) from .task_views import ExampleTaskView, TaskStatusView diff --git a/server/apps/sites/views/profile_views.py b/server/apps/core/views/profile_views.py similarity index 67% rename from server/apps/sites/views/profile_views.py rename to server/apps/core/views/profile_views.py index 486a97bd8..aeb0e451b 100644 --- a/server/apps/sites/views/profile_views.py +++ b/server/apps/core/views/profile_views.py @@ -5,14 +5,26 @@ from rest_framework.request import Request from rest_framework.response import Response -from apps.sites.models import RcraProfile, RcraSitePermission +from apps.core.models import HaztrakUser, RcraProfile +from apps.core.serializers import HaztrakUserSerializer, RcraProfileSerializer +from apps.sites.models import RcraSitePermission from apps.sites.serializers import ( - RcraPermissionSerializer, - RcraProfileSerializer, RcraSitePermissionSerializer, ) +class HaztrakUserView(RetrieveUpdateAPIView): + """Retrieve the current user's base information""" + + queryset = HaztrakUser.objects.all() + serializer_class = HaztrakUserSerializer + permission_classes = [permissions.AllowAny] # ToDo - temporary remove this + + def get_object(self): + # return HaztrakUser.objects.get(username="testuser1") + return self.request.user + + class RcraProfileView(RetrieveUpdateAPIView): """ Responsible for Create/Update operations related to the user RcraProfile, @@ -25,7 +37,6 @@ class RcraProfileView(RetrieveUpdateAPIView): response = Response lookup_field = "user__username" lookup_url_kwarg = "user" - permission_classes = [permissions.AllowAny] # temporary, remove me class SyncProfileView(GenericAPIView): @@ -49,22 +60,12 @@ def get(self, request: Request, user: str = None) -> Response: class RcraSitePermissionView(RetrieveAPIView): """ - For Viewing a user's Site Permissions in haztrak's internal JSON structure. - This is not included in the current URL configs, but kept here for documentation. + For Viewing the RcraSite Permissions for the given user """ queryset = RcraSitePermission.objects.all() serializer_class = RcraSitePermissionSerializer - permission_classes = [permissions.AllowAny] - -class RcraPermissionView(RetrieveAPIView): - """ - For Viewing a user's Site Permissions in the same JSON structure as RCRAInfo. - - This is not included in the current URL configs, but kept here for documentation. - """ - - queryset = RcraSitePermission.objects.all() - serializer_class = RcraPermissionSerializer - permission_classes = [permissions.AllowAny] + def get_queryset(self): + user = self.request.user + return RcraSitePermission.objects.filter(profile__user=user) diff --git a/server/apps/sites/admin.py b/server/apps/sites/admin.py index 8a472faee..c7a2ae441 100644 --- a/server/apps/sites/admin.py +++ b/server/apps/sites/admin.py @@ -3,7 +3,8 @@ from django.utils.html import format_html, urlencode from apps.core.admin import HiddenListView -from apps.sites.models import Address, Contact, RcraProfile, RcraSite, RcraSitePermission, Site +from apps.core.models import RcraProfile +from apps.sites.models import Address, Contact, RcraSite, RcraSitePermission, Site @admin.register(RcraSite) diff --git a/server/apps/sites/migrations/0001_initial.py b/server/apps/sites/migrations/0001_initial.py index d1506f0bc..ea9a12858 100644 --- a/server/apps/sites/migrations/0001_initial.py +++ b/server/apps/sites/migrations/0001_initial.py @@ -1,7 +1,6 @@ -# Generated by Django 4.1.7 on 2023-05-06 13:55 +# Generated by Django 4.2.1 on 2023-06-12 18:32 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 @@ -11,7 +10,7 @@ class Migration(migrations.Migration): initial = True dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("core", "0001_initial"), ] operations = [ @@ -162,31 +161,6 @@ class Migration(migrations.Migration): "ordering": ["number"], }, ), - migrations.CreateModel( - name="RcraProfile", - fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, primary_key=True, serialize=False, verbose_name="ID" - ), - ), - ("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)), - ( - "user", - models.OneToOneField( - on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL - ), - ), - ], - options={ - "ordering": ["rcra_username"], - }, - ), migrations.CreateModel( name="RcraSite", fields=[ @@ -394,7 +368,7 @@ class Migration(migrations.Migration): models.ForeignKey( on_delete=django.db.models.deletion.PROTECT, related_name="permissions", - to="sites.rcraprofile", + to="core.rcraprofile", ), ), ( diff --git a/server/apps/sites/models/__init__.py b/server/apps/sites/models/__init__.py index cf5f13125..8f01cbc10 100644 --- a/server/apps/sites/models/__init__.py +++ b/server/apps/sites/models/__init__.py @@ -1,3 +1,3 @@ +from ...core.models import RcraProfile from .contact_models import Address, Contact, RcraPhone, RcraStates -from .profile_models import RcraProfile, RcraSitePermission -from .site_models import RcraSite, RcraSiteType, Site, Role +from .site_models import RcraSite, RcraSitePermission, RcraSiteType, Role, Site diff --git a/server/apps/sites/models/profile_models.py b/server/apps/sites/models/profile_models.py deleted file mode 100644 index 8549112ab..000000000 --- a/server/apps/sites/models/profile_models.py +++ /dev/null @@ -1,123 +0,0 @@ -from django.core.exceptions import ValidationError -from django.db import models - -from haztrak import settings - -from .base_models import SitesBaseModel -from .site_models import Site - - -class RcraProfile(SitesBaseModel): - """ - Contains a user's RcraProfile information, such as username, and API credentials. - Has a one-to-one relationship with the User model. - """ - - class Meta: - ordering = ["rcra_username"] - - user = models.OneToOneField( - settings.AUTH_USER_MODEL, - on_delete=models.CASCADE, - ) - rcra_api_key = models.CharField( - max_length=128, - null=True, - blank=True, - ) - rcra_api_id = models.CharField( - max_length=128, - null=True, - blank=True, - ) - rcra_username = models.CharField( - max_length=128, - null=True, - blank=True, - ) - phone_number = models.CharField( - max_length=15, - null=True, - blank=True, - ) - email = models.EmailField() - - def __str__(self): - return f"{self.user.username}" - - def sync(self): - """Launch task to sync use profile. ToDo: remove this method""" - from apps.sites.tasks import sync_user_sites - - task = sync_user_sites.delay(str(self.user.username)) - return task - - @property - def is_api_user(self) -> bool: - """Returns true if the use has Rcrainfo API credentials""" - if self.rcra_username and self.rcra_api_id and self.rcra_api_key: - return True - return False - - -class RcraSitePermission(SitesBaseModel): - """ - RCRAInfo Site Permissions per module connected to a user's RcraProfile - and the corresponding Site - """ - - CERTIFIER = "Certifier" - PREPARER = "Preparer" - VIEWER = "Viewer" - - EPA_PERMISSION_LEVEL = [ - (CERTIFIER, "Certifier"), - (PREPARER, "Preparer"), - (VIEWER, "Viewer"), - ] - - class Meta: - verbose_name = "RCRA Site Permission" - ordering = ["site__rcra_site__epa_id"] - - site = models.ForeignKey(Site, on_delete=models.CASCADE) - profile = models.ForeignKey( - RcraProfile, - on_delete=models.PROTECT, - related_name="permissions", - ) - site_manager = models.BooleanField( - default=False, - ) - annual_report = models.CharField( - max_length=12, - choices=EPA_PERMISSION_LEVEL, - ) - biennial_report = models.CharField( - max_length=12, - choices=EPA_PERMISSION_LEVEL, - ) - e_manifest = models.CharField( - max_length=12, - choices=EPA_PERMISSION_LEVEL, - ) - my_rcra_id = models.CharField( - max_length=12, - choices=EPA_PERMISSION_LEVEL, - ) - wiets = models.CharField( - max_length=12, - choices=EPA_PERMISSION_LEVEL, - ) - - def __str__(self): - return f"{self.profile.user}: {self.site.rcra_site.epa_id}" - - def clean(self): - if self.site_manager: - fields = ["annual_report", "biennial_report", "e_manifest", "my_rcra_id", "wiets"] - for field_name in fields: - if getattr(self, field_name) != "Certifier": - raise ValidationError( - f"The value for the '{field_name}' field must be set to 'Certifier'." - ) diff --git a/server/apps/sites/models/site_models.py b/server/apps/sites/models/site_models.py index 36ae2c0bb..1b4cb7b3c 100644 --- a/server/apps/sites/models/site_models.py +++ b/server/apps/sites/models/site_models.py @@ -206,7 +206,73 @@ def __str__(self): class Role(models.TextChoices): - INDUSTRY = "IN", _("Industry") - PPC = "PP", _("Ppc") - EPA = "EP", _("Epa") - STATE = "ST", _("State") + INDUSTRY = "IN", _("Industry") + PPC = "PP", _("Ppc") + EPA = "EP", _("Epa") + STATE = "ST", _("State") + + +class RcraSitePermission(SitesBaseModel): + """ + RCRAInfo Site Permissions per module connected to a user's RcraProfile + and the corresponding Site + """ + + CERTIFIER = "Certifier" + PREPARER = "Preparer" + VIEWER = "Viewer" + + EPA_PERMISSION_LEVEL = [ + (CERTIFIER, "Certifier"), + (PREPARER, "Preparer"), + (VIEWER, "Viewer"), + ] + + class Meta: + verbose_name = "RCRA Site Permission" + ordering = ["site__rcra_site__epa_id"] + + site = models.ForeignKey( + Site, + on_delete=models.CASCADE, + ) + profile = models.ForeignKey( + "core.RcraProfile", + on_delete=models.PROTECT, + related_name="permissions", + ) + site_manager = models.BooleanField( + default=False, + ) + annual_report = models.CharField( + max_length=12, + choices=EPA_PERMISSION_LEVEL, + ) + biennial_report = models.CharField( + max_length=12, + choices=EPA_PERMISSION_LEVEL, + ) + e_manifest = models.CharField( + max_length=12, + choices=EPA_PERMISSION_LEVEL, + ) + my_rcra_id = models.CharField( + max_length=12, + choices=EPA_PERMISSION_LEVEL, + ) + wiets = models.CharField( + max_length=12, + choices=EPA_PERMISSION_LEVEL, + ) + + def __str__(self): + return f"{self.profile.user}: {self.site.rcra_site.epa_id}" + + def clean(self): + if self.site_manager: + fields = ["annual_report", "biennial_report", "e_manifest", "my_rcra_id", "wiets"] + for field_name in fields: + if getattr(self, field_name) != "Certifier": + raise ValidationError( + f"The value for the '{field_name}' field must be set to 'Certifier'." + ) diff --git a/server/apps/sites/serializers/__init__.py b/server/apps/sites/serializers/__init__.py index 6a89221d9..68e5eda03 100644 --- a/server/apps/sites/serializers/__init__.py +++ b/server/apps/sites/serializers/__init__.py @@ -2,7 +2,6 @@ from .contact_ser import ContactSerializer, RcraPhoneSerializer from .profile_ser import ( RcraPermissionSerializer, - RcraProfileSerializer, RcraSitePermissionSerializer, ) from .site_ser import RcraSiteSerializer, SiteSerializer diff --git a/server/apps/sites/serializers/profile_ser.py b/server/apps/sites/serializers/profile_ser.py index 5e986b796..eb91f8dd1 100644 --- a/server/apps/sites/serializers/profile_ser.py +++ b/server/apps/sites/serializers/profile_ser.py @@ -1,8 +1,7 @@ from rest_framework import serializers from rest_framework.exceptions import APIException -from rest_framework.serializers import ModelSerializer -from apps.sites.models.profile_models import RcraProfile, RcraSitePermission +from apps.sites.models import RcraSitePermission from .base_ser import SitesBaseSerializer from .site_ser import SiteSerializer @@ -163,50 +162,3 @@ class Meta: "WIETS", "myRCRAid", ] - - -class RcraProfileSerializer(ModelSerializer): - """ - Model serializer for marshalling/unmarshalling a user's RcraProfile - """ - - user = serializers.StringRelatedField() - rcraSites = RcraSitePermissionSerializer( - source="permissions", - required=False, - many=True, - ) - phoneNumber = serializers.CharField( - source="phone_number", - required=False, - ) - rcraAPIID = serializers.CharField( - source="rcra_api_id", - required=False, - ) - rcraAPIKey = serializers.CharField( - source="rcra_api_key", - required=False, - write_only=True, - ) - rcraUsername = serializers.CharField( - source="rcra_username", - required=False, - ) - apiUser = serializers.BooleanField( - source="is_api_user", - required=False, - allow_null=False, - ) - - class Meta: - model = RcraProfile - fields = [ - "user", - "rcraAPIID", - "rcraAPIKey", - "rcraUsername", - "rcraSites", - "phoneNumber", - "apiUser", - ] diff --git a/server/apps/sites/services/profile_services.py b/server/apps/sites/services/profile_services.py index a4e11b5f4..d6c1500fd 100644 --- a/server/apps/sites/services/profile_services.py +++ b/server/apps/sites/services/profile_services.py @@ -3,9 +3,10 @@ from django.db import transaction from apps.core.services import RcrainfoService -from apps.sites.models import RcraProfile, RcraSitePermission, Site +from apps.sites.models import RcraSitePermission, Site from apps.sites.serializers import RcraPermissionSerializer +from ...core.models import RcraProfile from .site_services import RcraSiteService, SiteService diff --git a/server/apps/sites/tests/conftest.py b/server/apps/sites/tests/conftest.py index 2e2220195..d96e46a60 100644 --- a/server/apps/sites/tests/conftest.py +++ b/server/apps/sites/tests/conftest.py @@ -2,8 +2,8 @@ import pytest +from apps.core.models import RcraProfile from apps.sites.models import ( - RcraProfile, RcraSitePermission, Site, ) diff --git a/server/apps/sites/tests/test_epa_profile_views.py b/server/apps/sites/tests/test_epa_profile_views.py index cc23e99bb..f17f09b12 100644 --- a/server/apps/sites/tests/test_epa_profile_views.py +++ b/server/apps/sites/tests/test_epa_profile_views.py @@ -3,7 +3,7 @@ from rest_framework.response import Response from rest_framework.test import APIRequestFactory, force_authenticate -from apps.sites.views import RcraProfileView +from apps.core.views import RcraProfileView class TestRcraProfileView: diff --git a/server/apps/sites/tests/test_models.py b/server/apps/sites/tests/test_models.py index fae97d97d..2a5d84782 100644 --- a/server/apps/sites/tests/test_models.py +++ b/server/apps/sites/tests/test_models.py @@ -2,7 +2,8 @@ from django.core.exceptions import ValidationError from django.db import IntegrityError -from apps.sites.models import Address, Contact, RcraProfile +from apps.core.models import RcraProfile +from apps.sites.models import Address, Contact @pytest.mark.django_db diff --git a/server/apps/sites/tests/test_serializers.py b/server/apps/sites/tests/test_serializers.py index 1c8246b5e..43424921c 100644 --- a/server/apps/sites/tests/test_serializers.py +++ b/server/apps/sites/tests/test_serializers.py @@ -1,5 +1,3 @@ -import json - import pytest from apps.sites.models import RcraSitePermission diff --git a/server/apps/sites/tests/test_site_views.py b/server/apps/sites/tests/test_site_views.py index 7905e17a4..76e8f5e85 100644 --- a/server/apps/sites/tests/test_site_views.py +++ b/server/apps/sites/tests/test_site_views.py @@ -6,7 +6,8 @@ from rest_framework.response import Response from rest_framework.test import APIClient, APIRequestFactory, force_authenticate -from apps.sites.models import RcraProfile, RcraSite, RcraSitePermission, Site +from apps.core.models import RcraProfile +from apps.sites.models import RcraSite, RcraSitePermission, Site from apps.sites.views import SiteDetailView, SiteMtnListView diff --git a/server/apps/sites/urls.py b/server/apps/sites/urls.py index 324a63421..151cc8007 100644 --- a/server/apps/sites/urls.py +++ b/server/apps/sites/urls.py @@ -1,21 +1,14 @@ from django.urls import path from apps.sites.views import ( - RcraProfileView, - RcraSitePermissionView, RcraSiteSearchView, RcraSiteView, SiteDetailView, SiteListView, SiteMtnListView, - SyncProfileView, ) urlpatterns = [ - # Rcra Profile - path("profile//sync", SyncProfileView.as_view()), - path("profile/", RcraProfileView.as_view()), - path("site/permission/", RcraSitePermissionView.as_view()), # Site path("site/", SiteListView.as_view()), path("site/", SiteDetailView.as_view()), diff --git a/server/apps/sites/views/__init__.py b/server/apps/sites/views/__init__.py index cf57ae5f0..ad5391971 100644 --- a/server/apps/sites/views/__init__.py +++ b/server/apps/sites/views/__init__.py @@ -1,9 +1,3 @@ -from .profile_views import ( - RcraPermissionView, - RcraProfileView, - RcraSitePermissionView, - SyncProfileView, -) from .site_views import ( RcraSiteSearchView, RcraSiteView, diff --git a/server/apps/trak/migrations/0001_initial.py b/server/apps/trak/migrations/0001_initial.py index 4f2bcca3e..ce6cc4d69 100644 --- a/server/apps/trak/migrations/0001_initial.py +++ b/server/apps/trak/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.1.7 on 2023-05-06 13:55 +# Generated by Django 4.2.1 on 2023-06-12 18:32 import apps.trak.models.contact_models import apps.trak.models.manifest_models @@ -570,6 +570,62 @@ class Migration(migrations.Migration): "ordering": ["sign_date"], }, ), + migrations.CreateModel( + 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", + ), + ), + ], + options={ + "verbose_name": "Correction Info", + "verbose_name_plural": "Correction Info", + }, + ), migrations.CreateModel( name="Transporter", fields=[ diff --git a/server/apps/trak/migrations/0002_correctioninfo.py b/server/apps/trak/migrations/0002_correctioninfo.py deleted file mode 100644 index 3569fa6c0..000000000 --- a/server/apps/trak/migrations/0002_correctioninfo.py +++ /dev/null @@ -1,69 +0,0 @@ -# Generated by Django 4.1.7 on 2023-05-11 09:18 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - dependencies = [ - ("trak", "0001_initial"), - ] - - operations = [ - migrations.CreateModel( - 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", - ), - ), - ], - options={ - "verbose_name": "Correction Info", - "verbose_name_plural": "Correction Info", - }, - ), - ] diff --git a/server/apps/trak/signals.py b/server/apps/trak/signals.py index 8ddb949ef..3e481162e 100644 --- a/server/apps/trak/signals.py +++ b/server/apps/trak/signals.py @@ -2,7 +2,7 @@ from django.db.models.signals import post_save from django.dispatch import receiver -from apps.sites.models.profile_models import RcraProfile +from apps.core.models import RcraProfile @receiver(post_save, sender=User) diff --git a/server/fixtures/dev_data.yaml b/server/fixtures/dev_data.yaml index 76d019e86..d260b6a27 100644 --- a/server/fixtures/dev_data.yaml +++ b/server/fixtures/dev_data.yaml @@ -20,9 +20,9 @@ last_login: null is_superuser: false username: testuser1 - first_name: '' - last_name: '' - email: '' + first_name: 'David' + last_name: 'Graham' + email: 'myemail@proton.me' is_staff: false is_active: true date_joined: 2022-12-17 19:17:58.260000+00:00 @@ -669,7 +669,7 @@ can_esign: true limited_esign: true registered_emanifest_user: true -- model: sites.rcraprofile +- model: core.rcraprofile pk: 1 fields: user: 2 diff --git a/server/haztrak/settings.py b/server/haztrak/settings.py index d8b00d77d..ed5e1cd4c 100644 --- a/server/haztrak/settings.py +++ b/server/haztrak/settings.py @@ -163,8 +163,7 @@ "DESCRIPTION": "An open-source web app illustrating how hazardous waste " "management software can integrate with EPA's RCRAInfo", "VERSION": HAZTRAK_VERSION, - "SERVE_INCLUDE_SCHEMA": True, - # "SCHEMA_PATH_PREFIX": r"/api/[a-zA-Z]*/", # ToDo: redesign endpoints + "SERVE_INCLUDE_SCHEMA": False, "SWAGGER_UI_SETTINGS": { "deepLinking": True, "persistAuthorization": True,