Skip to content

Commit

Permalink
Merge pull request #27 from ucsb-cs156-s24/Sreeganesh-SchoolsCRUD-Create
Browse files Browse the repository at this point in the history
Schools Create and Index Page and Fixed Backend Issues
  • Loading branch information
pconrad authored Jun 2, 2024
2 parents d1e0c1e + 206619d commit 3eca1ad
Show file tree
Hide file tree
Showing 7 changed files with 246 additions and 66 deletions.
2 changes: 1 addition & 1 deletion .env.SAMPLE
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
GITHUB_CLIENT_ID=see-instructions-in-readme
GITHUB_CLIENT_SECRET=see-instructions-in-readme
ADMIN_GITHUB_LOGINS=pconrad,phtcon
ADMIN_GITHUB_LOGINS=pconrad,phtcon
12 changes: 11 additions & 1 deletion frontend/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@ import CoursesEditPage from "main/pages/CoursesEditPage";

import AdminUsersPage from "main/pages/AdminUsersPage";
import AdminJobsPage from "main/pages/AdminJobsPage";
import SchoolIndexPage from "main/pages/SchoolIndexPage";

import CoursesCreatePage from "main/pages/CoursesCreatePage";
import CoursesIndexPage from "main/pages/CoursesIndexPage";
import CoursesStaffPage from "main/pages/CoursesStaffPage";

import SchoolCreatePage from "main/pages/SchoolCreatePage";
import SchoolIndexPage from "main/pages/SchoolIndexPage";

import { hasRole, useCurrentUser } from "main/utils/currentUser";
import NotFoundPage from "main/pages/NotFoundPage";
import StaffCreatePage from "main/pages/StaffCreatePage";
Expand Down Expand Up @@ -51,6 +53,13 @@ function App() {
</>
) : null;

const schoolRoutes = (hasRole(currentUser, "ROLE_ADMIN") || hasRole(currentUser, "ROLE_INSTRUCTOR")) ? (
<>
<Route path="/schools/create" element={<SchoolCreatePage />} />
<Route path="/schools" element={<SchoolIndexPage />} />
</>
) : null;

const homeRoute = (hasRole(currentUser, "ROLE_ADMIN") || hasRole(currentUser, "ROLE_USER"))
? <Route path="/" element={<HomePage />} />
: <Route path="/" element={<LoginPage />} />;
Expand Down Expand Up @@ -90,6 +99,7 @@ function App() {
{adminRoutes}
{userRoutes}
{courseRoutes}
{schoolRoutes}
<Route path="*" element={<NotFoundPage />} />
</Routes>
)}
Expand Down
47 changes: 47 additions & 0 deletions frontend/src/main/pages/SchoolCreatePage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import BasicLayout from "main/layouts/BasicLayout/BasicLayout";
import SchoolForm from "main/components/School/SchoolForm";
import { Navigate } from 'react-router-dom'
import { useBackendMutation } from "main/utils/useBackend";
import { toast } from "react-toastify";

export default function SchoolCreatePage({storybook=false}) {


const objectToAxiosParams = (school) => ({
url: "/api/schools/post",
method: "POST",
data: school
});

const onSuccess = (school) => {
toast(`New school created - id: ${school.abbrev}`);
}

const mutation = useBackendMutation(
objectToAxiosParams,
{ onSuccess },
// Stryker disable next-line all : hard to set up test for caching
["/api/schools/all"] // mutation makes this key stale so that pages relying on it reload
);

const { isSuccess } = mutation

const onSubmit = async (data) => {
mutation.mutate(data);
}

if (isSuccess && !storybook) {
return <Navigate to="/schools" />
}

return (
<BasicLayout>
<div className="pt-2">
<h1>Create New School</h1>

<SchoolForm submitAction={onSubmit} />

</div>
</BasicLayout>
)
}
29 changes: 29 additions & 0 deletions frontend/src/stories/pages/SchoolCreatePage.stories.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import React from 'react';
import { apiCurrentUserFixtures } from "fixtures/currentUserFixtures";
import { systemInfoFixtures } from "fixtures/systemInfoFixtures";
import { rest } from "msw";

import SchoolCreatePage from "main/pages/SchoolCreatePage";

export default {
title: 'pages/SchoolCreatePage',
component: SchoolCreatePage
};

const Template = () => <SchoolCreatePage storybook={true} />;

export const Default = Template.bind({});
Default.parameters = {
msw: [
rest.get('/api/currentUser', (_req, res, ctx) => {
return res(ctx.json(apiCurrentUserFixtures.userOnly));
}),
rest.get('/api/systemInfo', (_req, res, ctx) => {
return res(ctx.json(systemInfoFixtures.showingNeither));
}),
rest.post('/api/schools/post', (req, res, ctx) => {
window.alert("POST: " + JSON.stringify(req.url));
return res(ctx.status(200),ctx.json({}));
}),
]
}
114 changes: 114 additions & 0 deletions frontend/src/tests/pages/SchoolCreatePage.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import { QueryClient, QueryClientProvider } from "react-query";
import { MemoryRouter } from "react-router-dom";
import SchoolCreatePage from "main/pages/SchoolCreatePage";

import { apiCurrentUserFixtures } from "fixtures/currentUserFixtures";
import { systemInfoFixtures } from "fixtures/systemInfoFixtures";

import axios from "axios";
import AxiosMockAdapter from "axios-mock-adapter";

const mockToast = jest.fn();
jest.mock('react-toastify', () => {
const originalModule = jest.requireActual('react-toastify');
return {
__esModule: true,
...originalModule,
toast: (x) => mockToast(x)
};
});

const mockNavigate = jest.fn();
jest.mock('react-router-dom', () => {
const originalModule = jest.requireActual('react-router-dom');
return {
__esModule: true,
...originalModule,
Navigate: (x) => { mockNavigate(x); return null; }
};
});

describe("SchoolCreatePage tests", () => {

const axiosMock =new AxiosMockAdapter(axios);

beforeEach(() => {
jest.clearAllMocks();
axiosMock.reset();
axiosMock.resetHistory();
axiosMock.onGet("/api/currentUser").reply(200, apiCurrentUserFixtures.userOnly);
axiosMock.onGet("/api/systemInfo").reply(200, systemInfoFixtures.showingNeither);
});



const queryClient = new QueryClient();
test("renders without crashing", () => {
render(
<QueryClientProvider client={queryClient}>
<MemoryRouter>
<SchoolCreatePage />
</MemoryRouter>
</QueryClientProvider>
);
});

test("on submit, makes request to backend", async () => {

const queryClient = new QueryClient();
const school = {
abbrev: "ucsb",
name: "UC Santa Barbara",
termRegex: "[WSMF]\\d\\d",
termDescription: "quarter",
termError: "test"
};

axiosMock.onPost("/api/schools/post").reply(200, school);

render(
<QueryClientProvider client={queryClient}>
<MemoryRouter>
<SchoolCreatePage />
</MemoryRouter>
</QueryClientProvider>
);


expect(await screen.findByText("Create New School")).toBeInTheDocument();

const abbrevField = screen.getByTestId("SchoolForm-abbrev");
const nameField = screen.getByTestId("SchoolForm-name");
const termRegexField = screen.getByTestId("SchoolForm-termRegex");
const termDescriptionField = screen.getByTestId("SchoolForm-termDescription");
const termErrorField = screen.getByTestId("SchoolForm-termError");
const submitButton = screen.getByTestId("SchoolForm-submit");


fireEvent.change(abbrevField, { target: { value: 'ucsb' } });
fireEvent.change(nameField, { target: { value: 'UC Santa Barbara' } });
fireEvent.change(termRegexField, { target: { value: '[WSMF]\\d\\d' } });
fireEvent.change(termDescriptionField, { target: { value: 'quarter' } });
fireEvent.change(termErrorField, { target: { value: 'test' } });


fireEvent.click(submitButton);

await waitFor(() => expect(axiosMock.history.post.length).toBe(1));

expect(axiosMock.history.post[0].data).toEqual(
JSON.stringify({
"abbrev": "ucsb",
"name": "UC Santa Barbara",
"termRegex": "[WSMF]\\d\\d",
"termDescription": "quarter",
"termError": "test"
}));

expect(mockToast).toBeCalledWith("New school created - id: ucsb");
expect(mockNavigate).toBeCalledWith({ "to": "/schools" });
});


});
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,6 @@ public Iterable<School> allSchools() {
@Operation(summary= "Get a single school by abbreviation")
@PreAuthorize("hasRole('ROLE_USER')")
@GetMapping("")

public School getById(
@Parameter(name="abbrev") @RequestParam String abbrev) {
Optional<School> schoolOptional = schoolRepository.findById(abbrev);
Expand All @@ -111,25 +110,11 @@ public Object deleteSchool(
@PreAuthorize("hasRole('ROLE_ADMIN')")
@PostMapping("/post")
public School postSchool(
@Parameter(name="abbrev", description="university domain name", example="ucsb") @RequestParam String abbrev,
@Parameter(name="name", description="University name") @RequestParam String name,
@Parameter(name="termRegex", description="Format: Example [WSMF]\\d\\d") @RequestParam String termRegex,
@Parameter(name="termDescription", description="Enter quarter, e.g. F23, W24, S24, M24") @RequestParam String termDescription,
@Parameter(name="termError", description="input error?") @RequestParam String termError)
{

School school = School.builder().build();
school.setAbbrev(abbrev);
school.setName(name);
school.setTermRegex(termRegex);
school.setTermDescription(termDescription);
school.setTermError(termError);

if (!termDescription.matches(school.getTermRegex())) {
throw new IllegalArgumentException("Invalid termDescription format. It must follow the pattern " + school.getTermRegex());
}
@Parameter(name = "school", description="school in json format") @RequestBody School school
)
{

if (!abbrev.equals(abbrev.toLowerCase())){
if (!school.getAbbrev().equals(school.getAbbrev().toLowerCase())){
throw new IllegalArgumentException("Invalid abbrev format. Abbrev must be all lowercase");
}

Expand All @@ -138,7 +123,4 @@ public School postSchool(
return savedSchool;
}




}
}
Loading

0 comments on commit 3eca1ad

Please sign in to comment.