Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Schools Create and Index Page and Fixed Backend Issues #27

Merged
merged 26 commits into from
Jun 2, 2024
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
395e6fe
ss - created the create page for school and linked to the app.js file…
SR33G33 May 23, 2024
83df6fb
ss - removed .env.SAMPLE
SR33G33 May 28, 2024
125f562
ss - fixed params in create page
SR33G33 May 28, 2024
8773f13
ss - removed commented code for edit page to be used in next PR
SR33G33 May 28, 2024
2f4571c
removed .env sample
SR33G33 May 28, 2024
3e3da7e
ss - added test file for SchoolCreatePage
SR33G33 May 28, 2024
9b675e6
fixed backend issues that were causing Create Tests to fail
SR33G33 May 29, 2024
583a9ec
ss - fixed some SchoolController backend stuff for CreateTests to pass
SR33G33 May 29, 2024
e58e450
ss - fixed syntax error in SchoolController
SR33G33 May 29, 2024
4b20da1
ss - accidentally changed invalidRegex test to ucsb instead of UCSB
SR33G33 May 29, 2024
a21b5fb
ss - attempted fix for SchoolControllerTests so it accepts the new JS…
SR33G33 May 29, 2024
d5311d8
ss - more fixes to SchoolControllerTests, forgot to change base admin…
SR33G33 May 29, 2024
d0b7d00
ss - added requestBodyCheck to admin can create school test
SR33G33 May 29, 2024
94b5b00
ss - accidentally deleted in previous commit, resetting this file fro…
SR33G33 May 30, 2024
c94a48a
ss - Update CoursesEditPage.stories.js to be same as one in main branch
SR33G33 May 30, 2024
89fa28f
ss - Update CoursesEditPage.stories.js
SR33G33 May 30, 2024
89f7577
ss - accidentally deleted in old commit, resetting this file from mai…
SR33G33 Jun 1, 2024
882945e
Update .env.SAMPLE
SR33G33 Jun 1, 2024
daf9edb
ss - TermDescription is a field that just determines term type, has n…
SR33G33 Jun 1, 2024
ee8755a
ss - changed backend tests to match this update
SR33G33 Jun 1, 2024
2d249f9
ss - should fix mutation testing for backend
SR33G33 Jun 1, 2024
c2059dc
ss - attempted fix for backend test mutation failing
SR33G33 Jun 1, 2024
31e5bc0
ss - more minor backend test fixes for clarity
SR33G33 Jun 1, 2024
b57543a
ss - another attempt to fix backend mutation test
SR33G33 Jun 1, 2024
a5f39af
ss - the pitfalls of blindly find and replacing, minor fixes to backe…
SR33G33 Jun 1, 2024
206619d
ss - another attempted fix for mutation
SR33G33 Jun 1, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .env.SAMPLE

This file was deleted.

12 changes: 11 additions & 1 deletion frontend/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@ 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 CourseIndexPage from "main/pages/CourseIndexPage";

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";

Expand Down Expand Up @@ -45,6 +47,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 @@ -84,6 +93,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>
)
}
4 changes: 4 additions & 0 deletions frontend/src/stories/pages/CoursesEditPage.stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { coursesFixtures } from 'fixtures/coursesFixtures';
import { rest } from "msw";

import CoursesEditPage from 'main/pages/CoursesEditPage';
import {schoolsFixtures} from "../../fixtures/schoolsFixtures";

export default {
title: 'pages/CoursesEditPage',
Expand All @@ -26,6 +27,9 @@ Default.parameters = {
rest.get('/api/courses', (_req, res, ctx) => {
return res(ctx.json(coursesFixtures.threeCourses[0]));
}),
rest.get('/api/schools/all', (_req, res, ctx) => {
return res(ctx.json(schoolsFixtures.threeSchools));
}),
rest.put('/api/courses', async (req, res, ctx) => {
var reqBody = await req.text();
window.alert("PUT: " + req.url + " and body: " + reqBody);
Expand Down
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: "s24",
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: 's24' } });
SR33G33 marked this conversation as resolved.
Show resolved Hide resolved
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": "s24",
SR33G33 marked this conversation as resolved.
Show resolved Hide resolved
"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 @@ -86,7 +86,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 @@ -112,34 +111,21 @@ 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");
}

if (!school.getTermDescription().matches(school.getTermRegex())) {
throw new IllegalArgumentException("Invalid termDescription format. It must follow the pattern " + school.getTermRegex());
}

SR33G33 marked this conversation as resolved.
Show resolved Hide resolved
School savedSchool = schoolRepository.save(school);

return savedSchool;
}




}
}
Loading
Loading