diff --git a/client/src/actions/cities.ts b/client/src/actions/cities.ts index 59f8be983260eec6f4e46e39d8218e2b5c8243eb..c06c94a032c0e4d04f35e679ecd8869d92e76d86 100644 --- a/client/src/actions/cities.ts +++ b/client/src/actions/cities.ts @@ -13,15 +13,15 @@ export const getCities = () => async (dispatch: AppDispatch) => { .then((res) => { dispatch({ type: Types.SET_CITIES, - payload: res.data.items, + payload: res.data, }) dispatch({ - type: Types.SET_CITIES_COUNT, - payload: res.data.total_count, + type: Types.SET_CITIES_TOTAL, + payload: res.data.length, }) dispatch({ - type: Types.SET_CITIES_TOTAL, - payload: res.data.count, + type: Types.SET_CITIES_COUNT, + payload: res.data.length, }) }) .catch((err) => console.log(err)) diff --git a/client/src/actions/competitionLogin.ts b/client/src/actions/competitionLogin.ts index 42a050532ef5ebe571eeb6ab67a3d8eaf1fa0129..b2198f74698f8fb7b40acd10f7a2b2c64ebb66d7 100644 --- a/client/src/actions/competitionLogin.ts +++ b/client/src/actions/competitionLogin.ts @@ -15,7 +15,7 @@ export const loginCompetition = (code: string, history: History, redirect: boole ) => { dispatch({ type: Types.LOADING_COMPETITION_LOGIN }) await axios - .post('/api/auth/login/code', { code }) + .post('/api/auth/code', { code }) .then((res) => { const token = `Bearer ${res.data.access_token}` localStorage.setItem(`${res.data.view}Token`, token) //setting token to local storage diff --git a/client/src/actions/competitions.test.ts b/client/src/actions/competitions.test.ts index 52be36acb658576e9b5c1ffc1b40394c338d0f5f..2ebb22b74f7ef7b187c11df9e347988a82e4baf5 100644 --- a/client/src/actions/competitions.test.ts +++ b/client/src/actions/competitions.test.ts @@ -11,25 +11,24 @@ const mockStore = configureMockStore(middlewares) it('dispatches correct actions when getting competitions', async () => { const compRes: any = { - data: { - items: [ - { - id: 21, - name: 'ggff', - year: 2021, - style_id: 1, - city: { name: 'city_name', id: 5 }, - }, - { - id: 22, - name: 'sssss', - year: 2021, - style_id: 1, - city: { name: 'city_name', id: 5 }, - }, - ], - count: 2, - total_count: 3, + data: [ + { + id: 21, + name: 'ggff', + year: 2021, + style_id: 1, + city: { name: 'city_name', id: 5 }, + }, + { + id: 22, + name: 'sssss', + year: 2021, + style_id: 1, + city: { name: 'city_name', id: 5 }, + }, + ], + headers: { + pagination: '{"count": 2,"total": 3, "page_size": 5}', }, } @@ -37,9 +36,9 @@ it('dispatches correct actions when getting competitions', async () => { return Promise.resolve(compRes) }) const expectedActions = [ - { type: Types.SET_COMPETITIONS, payload: compRes.data.items }, - { type: Types.SET_COMPETITIONS_TOTAL, payload: compRes.data.total_count }, - { type: Types.SET_COMPETITIONS_COUNT, payload: compRes.data.count }, + { type: Types.SET_COMPETITIONS, payload: compRes.data }, + { type: Types.SET_COMPETITIONS_TOTAL, payload: 3 }, + { type: Types.SET_COMPETITIONS_COUNT, payload: 2 }, ] const store = mockStore({ competitions: { filterParams: [] } }) await getCompetitions()(store.dispatch, store.getState as any) diff --git a/client/src/actions/competitions.ts b/client/src/actions/competitions.ts index 64fd11e1a61614e30296f685bf85eac8700e7a96..e934726d8265a2ea88751137c8cc3bbaef9375b1 100644 --- a/client/src/actions/competitions.ts +++ b/client/src/actions/competitions.ts @@ -24,15 +24,16 @@ export const getCompetitions = () => async (dispatch: AppDispatch, getState: () .then((res) => { dispatch({ type: Types.SET_COMPETITIONS, - payload: res.data.items, + payload: res.data, }) + const pagination = JSON.parse(res.headers.pagination) dispatch({ type: Types.SET_COMPETITIONS_TOTAL, - payload: res.data.total_count, + payload: pagination.total, }) dispatch({ type: Types.SET_COMPETITIONS_COUNT, - payload: res.data.count, + payload: res.data.length, }) }) .catch((err) => { diff --git a/client/src/actions/roles.ts b/client/src/actions/roles.ts index 68b86b30e415daa1c5b00470fb5e86b1cb10bd74..810e13b1437ef0465039a89ea0b65dbbac3c3cb6 100644 --- a/client/src/actions/roles.ts +++ b/client/src/actions/roles.ts @@ -13,7 +13,7 @@ export const getRoles = () => async (dispatch: AppDispatch) => { .then((res) => { dispatch({ type: Types.SET_ROLES, - payload: res.data.items, + payload: res.data, }) }) .catch((err) => console.log(err)) diff --git a/client/src/actions/searchUser.test.ts b/client/src/actions/searchUser.test.ts index 66c155079a82d0efd21f0df949c669ab70f85c22..71d539d5dccab99c065c9c3f899f6ea8a1b61f8d 100644 --- a/client/src/actions/searchUser.test.ts +++ b/client/src/actions/searchUser.test.ts @@ -10,27 +10,26 @@ const middlewares = [thunk] const mockStore = configureMockStore(middlewares) it('dispatches correct actions when getting users', async () => { const userRes: any = { - data: { - items: [ - { - id: 21, - name: 'ggff', - email: 'email@test.com', - year: 2021, - role_id: 1, - city_id: 0, - }, - { - id: 22, - name: 'sssss', - email: 'email@test.com', - year: 2021, - role_id: 1, - city_id: 0, - }, - ], - count: 2, - total_count: 3, + data: [ + { + id: 21, + name: 'ggff', + email: 'email@test.com', + year: 2021, + role_id: 1, + city_id: 0, + }, + { + id: 22, + name: 'sssss', + email: 'email@test.com', + year: 2021, + role_id: 1, + city_id: 0, + }, + ], + headers: { + pagination: '{"count": 2,"total": 3, "page_size":5 }', }, } @@ -38,9 +37,9 @@ it('dispatches correct actions when getting users', async () => { return Promise.resolve(userRes) }) const expectedActions = [ - { type: Types.SET_SEARCH_USERS, payload: userRes.data.items }, - { type: Types.SET_SEARCH_USERS_TOTAL_COUNT, payload: userRes.data.total_count }, - { type: Types.SET_SEARCH_USERS_COUNT, payload: userRes.data.count }, + { type: Types.SET_SEARCH_USERS, payload: userRes.data }, + { type: Types.SET_SEARCH_USERS_TOTAL_COUNT, payload: 3 }, + { type: Types.SET_SEARCH_USERS_COUNT, payload: userRes.data.length }, ] const store = mockStore({ searchUsers: { filterParams: [] } }) await getSearchUsers()(store.dispatch, store.getState as any) diff --git a/client/src/actions/searchUser.ts b/client/src/actions/searchUser.ts index d47d4dd0903f57e1e298418bb72f43122b90407f..4197ee188e51e4f2c17a207d9686a25d2cf00b63 100644 --- a/client/src/actions/searchUser.ts +++ b/client/src/actions/searchUser.ts @@ -24,15 +24,17 @@ export const getSearchUsers = () => async (dispatch: AppDispatch, getState: () = .then((res) => { dispatch({ type: Types.SET_SEARCH_USERS, - payload: res.data.items, + payload: res.data, }) + + const pagination = JSON.parse(res.headers.pagination) dispatch({ type: Types.SET_SEARCH_USERS_TOTAL_COUNT, - payload: res.data.total_count, + payload: pagination.total, }) dispatch({ type: Types.SET_SEARCH_USERS_COUNT, - payload: res.data.count, + payload: res.data.length, }) }) .catch((err) => { diff --git a/client/src/pages/admin/AdminPage.test.tsx b/client/src/pages/admin/AdminPage.test.tsx index 445373a032207faa3f546a0c480fb03a093b1529..61d409733c09d3b7ff1fb2ef902eb8a10ab67e34 100644 --- a/client/src/pages/admin/AdminPage.test.tsx +++ b/client/src/pages/admin/AdminPage.test.tsx @@ -8,35 +8,33 @@ import AdminPage from './AdminPage' it('renders admin view', () => { const cityRes: any = { - data: { - items: [ - { - id: 1, - name: 'Link\u00f6ping', - }, - { - id: 2, - name: 'Stockholm', - }, - ], - count: 2, - total_count: 3, + data: [ + { + id: 1, + name: 'Link\u00f6ping', + }, + { + id: 2, + name: 'Stockholm', + }, + ], + headers: { + pagination: '{"count": 2,"total_count": 3}', }, } const rolesRes: any = { - data: { - items: [ - { - id: 1, - name: 'role1', - }, - { - id: 2, - name: 'role2', - }, - ], - count: 2, - total_count: 3, + data: [ + { + id: 1, + name: 'role1', + }, + { + id: 2, + name: 'role2', + }, + ], + headers: { + pagination: '{"count": 2,"total_count": 3}', }, } ;(mockedAxios.get as jest.Mock).mockImplementation((path: string, params?: any) => { diff --git a/client/src/pages/admin/competitions/CompetitionManager.test.tsx b/client/src/pages/admin/competitions/CompetitionManager.test.tsx index 7af04abb66bba7ded7655398f288cba1c62bf78b..0b270a7df39653567333c06516090c9c0364c900 100644 --- a/client/src/pages/admin/competitions/CompetitionManager.test.tsx +++ b/client/src/pages/admin/competitions/CompetitionManager.test.tsx @@ -8,42 +8,36 @@ import CompetitionManager from './CompetitionManager' it('renders competition manager', () => { const cityRes: any = { - data: { - items: [ - { - id: 1, - name: 'Link\u00f6ping', - }, - { - id: 2, - name: 'Stockholm', - }, - ], - count: 2, - total_count: 3, - }, + data: [ + { + id: 1, + name: 'Link\u00f6ping', + }, + { + id: 2, + name: 'Stockholm', + }, + ], + pagination: '{"count": 2,"total": 3, "page_size": 5}', } const compRes: any = { - data: { - items: [ - { - id: 21, - name: 'ggff', - year: 2021, - style_id: 1, - city: cityRes.data.items[0], - }, - { - id: 22, - name: 'sssss', - year: 2021, - style_id: 1, - city: cityRes.data.items[1], - }, - ], - count: 2, - total_count: 3, - }, + data: [ + { + id: 21, + name: 'ggff', + year: 2021, + style_id: 1, + city: cityRes.data[0], + }, + { + id: 22, + name: 'sssss', + year: 2021, + style_id: 1, + city: cityRes.data[1], + }, + ], + headers: { pagination: '{"count": 2,"total": 3, "page_size": 5}' }, } ;(mockedAxios.get as jest.Mock).mockImplementation((path: string, params?: any) => { diff --git a/client/src/pages/admin/competitions/CompetitionManager.tsx b/client/src/pages/admin/competitions/CompetitionManager.tsx index 0545102893ce342878602972d63c1dbe879ac59a..7c2c969efba20debfc116818a6f04945cb6ecbfe 100644 --- a/client/src/pages/admin/competitions/CompetitionManager.tsx +++ b/client/src/pages/admin/competitions/CompetitionManager.tsx @@ -161,7 +161,7 @@ const CompetitionManager: React.FC = (props: any) => { await axios .get(`/api/competitions/${id}/codes`) .then((response) => { - setCodes(response.data.items) + setCodes(response.data) }) .catch(console.log) } @@ -171,8 +171,7 @@ const CompetitionManager: React.FC = (props: any) => { await axios .get(`/api/competitions/${id}/teams`) .then((response) => { - // console.log(response.data.items) - setTeams(response.data.items) + setTeams(response.data) }) .catch((err) => { console.log(err) @@ -344,8 +343,8 @@ const CompetitionManager: React.FC = (props: any) => { rowsPerPageOptions={[]} rowsPerPage={filterParams.pageSize} count={competitionTotal} - page={filterParams.page} - onChangePage={(event, newPage) => handleFilterChange({ ...filterParams, page: newPage })} + page={filterParams.page - 1} + onChangePage={(event, newPage) => handleFilterChange({ ...filterParams, page: newPage + 1 })} /> <Menu id="simple-menu" anchorEl={anchorEl} keepMounted open={Boolean(anchorEl)} onClose={handleClose}> <MenuItem onClick={handleStartCompetition}>Starta</MenuItem> diff --git a/client/src/pages/admin/regions/Regions.test.tsx b/client/src/pages/admin/regions/Regions.test.tsx index 7046bff04fc8fc9fc8cb3f08d0d140d0d475b5a3..864001b31dcc9cf17edbaf70af65aac240232663 100644 --- a/client/src/pages/admin/regions/Regions.test.tsx +++ b/client/src/pages/admin/regions/Regions.test.tsx @@ -8,19 +8,18 @@ import RegionManager from './Regions' it('renders region manager', () => { const cityRes: any = { - data: { - items: [ - { - id: 1, - name: 'Link\u00f6ping', - }, - { - id: 2, - name: 'Stockholm', - }, - ], - count: 2, - total_count: 3, + data: [ + { + id: 1, + name: 'Link\u00f6ping', + }, + { + id: 2, + name: 'Stockholm', + }, + ], + headers: { + pagination: '{"count": 2,"total_count": 3}', }, } diff --git a/client/src/pages/admin/users/AddUser.tsx b/client/src/pages/admin/users/AddUser.tsx index 05f5745c94dc58d6f851556b9571f4aeff7c45ac..cde3fa322dd236edbac13b664dfdafe3985bc56f 100644 --- a/client/src/pages/admin/users/AddUser.tsx +++ b/client/src/pages/admin/users/AddUser.tsx @@ -59,7 +59,7 @@ const AddUser: React.FC = (props: any) => { role_id: selectedRole?.id as number, } await axios - .post('/api/auth/signup', params) + .post('/api/users', params) .then(() => { actions.resetForm() setAnchorEl(null) diff --git a/client/src/pages/admin/users/EditUser.tsx b/client/src/pages/admin/users/EditUser.tsx index 44375e4a8b5382996ecf20720865bdf1ac161fde..9e9d5d2a1d61d235abf715f6c4075bd5f4439ffa 100644 --- a/client/src/pages/admin/users/EditUser.tsx +++ b/client/src/pages/admin/users/EditUser.tsx @@ -14,7 +14,7 @@ import { TextField, Theme, useMediaQuery, - useTheme, + useTheme } from '@material-ui/core' import MoreHorizIcon from '@material-ui/icons/MoreHoriz' import { Alert, AlertTitle } from '@material-ui/lab' @@ -110,7 +110,7 @@ const EditUser = ({ user }: UserIdProps) => { const handleDeleteUsers = async () => { setOpen(false) await axios - .delete(`/api/auth/delete/${user.id}`) + .delete(`/api/users/${user.id}`) .then(() => { setAnchorEl(null) dispatch(getSearchUsers()) diff --git a/client/src/pages/admin/users/UserManager.test.tsx b/client/src/pages/admin/users/UserManager.test.tsx index f50cbed8ded2b12d3d487e96f1d185bca2a2cbf8..69117bc5829fc6fca85bc2316c1faf668112d3f7 100644 --- a/client/src/pages/admin/users/UserManager.test.tsx +++ b/client/src/pages/admin/users/UserManager.test.tsx @@ -8,25 +8,24 @@ import UserManager from './UserManager' it('renders user manager', () => { const userRes: any = { - data: { - items: [ - { - id: 1, - name: 'user1', - email: 'user1@email.com', - role_id: 0, - city_id: 0, - }, - { - id: 2, - name: 'Stockholm', - email: 'user2@email.com', - role_id: 0, - city_id: 0, - }, - ], - count: 2, - total_count: 3, + data: [ + { + id: 1, + name: 'user1', + email: 'user1@email.com', + role_id: 0, + city_id: 0, + }, + { + id: 2, + name: 'Stockholm', + email: 'user2@email.com', + role_id: 0, + city_id: 0, + }, + ], + headers: { + pagination: '{"count": 2,"total_count": 3, "page_size": 5}', }, } diff --git a/client/src/pages/admin/users/UserManager.tsx b/client/src/pages/admin/users/UserManager.tsx index 9713e90f3159ee1d8cc91b2cc9916438a2537ab0..0b32e86893a979c3e37061102e49ab4fb5ed4e7f 100644 --- a/client/src/pages/admin/users/UserManager.tsx +++ b/client/src/pages/admin/users/UserManager.tsx @@ -170,9 +170,9 @@ const UserManager: React.FC = (props: any) => { component="div" rowsPerPageOptions={[]} rowsPerPage={filterParams.pageSize} - count={usersTotal} - page={filterParams.page} - onChangePage={(event, newPage) => handleFilterChange({ ...filterParams, page: newPage })} + count={usersTotal || 0} + page={filterParams.page - 1} + onChangePage={(event, newPage) => handleFilterChange({ ...filterParams, page: newPage + 1 })} /> </div> ) diff --git a/client/src/pages/presentationEditor/PresentationEditorPage.test.tsx b/client/src/pages/presentationEditor/PresentationEditorPage.test.tsx index 7254a69d3bdae4e39c6ce1097dc06e753ff333f0..539d54e809926bc7a583ea06b00acddd10084f1e 100644 --- a/client/src/pages/presentationEditor/PresentationEditorPage.test.tsx +++ b/client/src/pages/presentationEditor/PresentationEditorPage.test.tsx @@ -18,14 +18,12 @@ it('renders presentation editor', () => { }, } const citiesRes: any = { - data: { - items: [ - { - name: '', - city_id: 0, - }, - ], - }, + data: [ + { + name: '', + city_id: 0, + }, + ], } const typesRes: any = { data: { diff --git a/client/src/pages/presentationEditor/components/answerComponents/AnswerMatch.tsx b/client/src/pages/presentationEditor/components/answerComponents/AnswerMatch.tsx index bedd621fa5629f87a93176369542f1420222bf98..3d854b87112a990c28dfc92408bf254e15c2678f 100644 --- a/client/src/pages/presentationEditor/components/answerComponents/AnswerMatch.tsx +++ b/client/src/pages/presentationEditor/components/answerComponents/AnswerMatch.tsx @@ -70,7 +70,7 @@ const AnswerMatch = ({ variant, activeSlide, competitionId }: AnswerMultipleProp }, [teamId]) const getButtonStyle = () => { - if (activeSlide?.timer !== undefined && !timer.enabled) { + if (activeSlide?.timer !== null && !timer.enabled) { return { fill: '#AAAAAA' } // Buttons are light grey if timer is not on } return {} @@ -79,7 +79,7 @@ const AnswerMatch = ({ variant, activeSlide, competitionId }: AnswerMultipleProp const onMove = async (previousIndex: number, resultIndex: number) => { // moved outside the list if (resultIndex < 0 || resultIndex >= sortedAnswers.length || variant !== 'presentation') return - if (activeSlide?.timer !== undefined && !timer.enabled) return + if (activeSlide?.timer !== null && !timer.enabled) return const answersCopy = [...sortedAnswers] const [removed] = answersCopy.splice(previousIndex, 1) answersCopy.splice(resultIndex, 0, removed) diff --git a/client/src/pages/presentationEditor/components/answerComponents/AnswerMultiple.tsx b/client/src/pages/presentationEditor/components/answerComponents/AnswerMultiple.tsx index c687faf00c1b811c793cd88035becec93f638d53..039d9dfb558fa037d578566ef1b2280527f6c42b 100644 --- a/client/src/pages/presentationEditor/components/answerComponents/AnswerMultiple.tsx +++ b/client/src/pages/presentationEditor/components/answerComponents/AnswerMultiple.tsx @@ -48,7 +48,7 @@ const AnswerMultiple = ({ variant, activeSlide, competitionId }: AnswerMultipleP const updateAnswer = async (alternative: QuestionAlternative, checked: boolean) => { // TODO: fix. Make list of alternatives and delete & post instead of put to allow multiple boxes checked. - if (!activeSlide || (activeSlide?.timer !== undefined && !timer.enabled)) { + if (!activeSlide || (activeSlide?.timer !== null && !timer.enabled)) { return } const url = `/api/competitions/${competitionId}/teams/${teamId}/answers/question_alternatives/${alternative.id}` @@ -91,7 +91,7 @@ const AnswerMultiple = ({ variant, activeSlide, competitionId }: AnswerMultipleP <div key={alt.id}> <ListItem divider> <GreenCheckbox - disabled={activeSlide?.timer !== undefined && !timer.enabled} + disabled={activeSlide?.timer !== null && !timer.enabled} checked={decideChecked(alt)} onChange={(event: any) => updateAnswer(alt, event.target.checked)} /> diff --git a/client/src/pages/presentationEditor/components/answerComponents/AnswerSingle.tsx b/client/src/pages/presentationEditor/components/answerComponents/AnswerSingle.tsx index 70d59c01c4985541f8f503448172998a93dd4a48..1ff4b7cf16542dbe4540234d0ee1f66a45c474c0 100644 --- a/client/src/pages/presentationEditor/components/answerComponents/AnswerSingle.tsx +++ b/client/src/pages/presentationEditor/components/answerComponents/AnswerSingle.tsx @@ -50,7 +50,7 @@ const AnswerSingle = ({ variant, activeSlide, competitionId }: AnswerSingleProps } const updateAnswer = async (alternative: QuestionAlternative) => { - if (!activeSlide || (activeSlide?.timer !== undefined && !timer.enabled)) { + if (!activeSlide || (activeSlide?.timer !== null && !timer.enabled)) { return } @@ -79,7 +79,7 @@ const AnswerSingle = ({ variant, activeSlide, competitionId }: AnswerSingleProps */ const renderRadioButton = (alt: QuestionAlternative) => { let disabledStyle - if (activeSlide?.timer !== undefined && !timer.enabled) { + if (activeSlide?.timer !== null && !timer.enabled) { disabledStyle = { fill: '#AAAAAA' } // Buttons are light grey if timer is not on } if (variant === 'presentation') { diff --git a/client/src/pages/presentationEditor/components/answerComponents/AnswerText.tsx b/client/src/pages/presentationEditor/components/answerComponents/AnswerText.tsx index 595462c97ecbc4583754d40cc4f94eeee204e224..b8048c750c433cbbce68fa521e98a4b7937f38d8 100644 --- a/client/src/pages/presentationEditor/components/answerComponents/AnswerText.tsx +++ b/client/src/pages/presentationEditor/components/answerComponents/AnswerText.tsx @@ -39,7 +39,7 @@ const AnswerText = ({ activeSlide, competitionId }: AnswerTextProps) => { setTimerHandle(undefined) } //Only updates answer if the timer is on - if (activeSlide?.timer !== undefined && !timer.enabled) { + if (!(activeSlide?.timer !== null && !timer.enabled)) { //Only updates answer 100ms after last input was made setTimerHandle(window.setTimeout(() => updateAnswer(answer), 100)) } @@ -79,7 +79,7 @@ const AnswerText = ({ activeSlide, competitionId }: AnswerTextProps) => { </ListItem> <ListItem style={{ height: '100%' }}> <TextField - disabled={team === undefined || (activeSlide?.timer !== undefined && !timer.enabled)} + disabled={team === undefined || (activeSlide?.timer !== null && !timer.enabled)} defaultValue={getDefaultString()} style={{ height: '100%' }} variant="outlined" diff --git a/client/src/pages/presentationEditor/components/slideSettingsComponents/Timer.tsx b/client/src/pages/presentationEditor/components/slideSettingsComponents/Timer.tsx index 086a64a480c1f7d6de820d91c03d47cb327c1467..1cc012b8febf61346cd0e7c6e62679d4a3207f99 100644 --- a/client/src/pages/presentationEditor/components/slideSettingsComponents/Timer.tsx +++ b/client/src/pages/presentationEditor/components/slideSettingsComponents/Timer.tsx @@ -32,7 +32,7 @@ const Timer = ({ activeSlide, competitionId }: TimerProps) => { if (activeSlide) { setTimer(timerValue) await axios - .put(`/api/competitions/${competitionId}/slides/${activeSlide.id}`, { timer: timerValue }) + .put(`/api/competitions/${competitionId}/slides/${activeSlide.id}`, { timer: timerValue || null }) .then(() => { dispatch(getEditorCompetition(competitionId)) }) diff --git a/client/src/pages/views/JudgeViewPage.tsx b/client/src/pages/views/JudgeViewPage.tsx index df80d4fc4ecff26ccbab5ab863a33bf520c52e51..ad328fa018ea9d1430c9d454e64eea9073479a1a 100644 --- a/client/src/pages/views/JudgeViewPage.tsx +++ b/client/src/pages/views/JudgeViewPage.tsx @@ -124,19 +124,18 @@ const JudgeViewPage: React.FC = () => { <div className={classes.toolbar} /> <List> {slides.map((slide, index) => ( - <> + <div key={slide.id}> <SlideListItem selected={slide.order === currentSlide?.order} onClick={() => handleSelectSlide(index)} button - key={slide.id} style={{ border: 2, borderStyle: slide.id === operatorActiveSlideId ? 'dashed' : 'none' }} > {renderSlideIcon(slide)} <ListItemText primary={`Sida ${slide.order + 1}`} /> </SlideListItem> <Divider /> - </> + </div> ))} </List> </LeftDrawer> diff --git a/client/src/pages/views/OperatorViewPage.test.tsx b/client/src/pages/views/OperatorViewPage.test.tsx index 3259fcfcf07f7f54a725e01b2ea6fa8b69a89b21..f20cb695a695f94ccc519fe1f149ad5d957a86d2 100644 --- a/client/src/pages/views/OperatorViewPage.test.tsx +++ b/client/src/pages/views/OperatorViewPage.test.tsx @@ -15,19 +15,18 @@ it('renders operator view page', async () => { }, } const teamsRes: any = { - data: { - items: [ - { - id: 1, - name: 'team1', - }, - { - id: 2, - name: 'team2', - }, - ], - count: 2, - total_count: 3, + data: [ + { + id: 1, + name: 'team1', + }, + { + id: 2, + name: 'team2', + }, + ], + headers: { + pagination: '{"count": 2,"total_count": 3}', }, } diff --git a/client/src/pages/views/OperatorViewPage.tsx b/client/src/pages/views/OperatorViewPage.tsx index 130240c20fcff7ee742347c06b38b0424232fbb9..55946eec366106c42886ea5f0cb9fb69de225eb3 100644 --- a/client/src/pages/views/OperatorViewPage.tsx +++ b/client/src/pages/views/OperatorViewPage.tsx @@ -152,7 +152,7 @@ const OperatorViewPage: React.FC = () => { await axios .get(`/api/competitions/${activeId}/codes`) .then((response) => { - setCodes(response.data.items) + setCodes(response.data) }) .catch(console.log) } diff --git a/client/src/reducers/competitionsReducer.ts b/client/src/reducers/competitionsReducer.ts index 62b7006ebe0e379ab58774b6e317a1fdbe592d22..e7cab2058b2b903eb7c8f4e02904050701deff95 100644 --- a/client/src/reducers/competitionsReducer.ts +++ b/client/src/reducers/competitionsReducer.ts @@ -16,7 +16,7 @@ const initialState: CompetitionState = { competitions: [], total: 0, count: 0, - filterParams: { pageSize: 10, page: 0 }, + filterParams: { pageSize: 10, page: 1 }, } /** Intercept actions for competitions state and update the state */ diff --git a/client/src/reducers/searchUserReducer.ts b/client/src/reducers/searchUserReducer.ts index 1e78f8961e014714dd6f1a8b762902298a2287a2..4fea6ef9c12cdcce2d05c5a197ff046a25fc904d 100644 --- a/client/src/reducers/searchUserReducer.ts +++ b/client/src/reducers/searchUserReducer.ts @@ -16,7 +16,7 @@ const initialState: SearchUserState = { users: [], total: 0, count: 0, - filterParams: { pageSize: 10, page: 0 }, + filterParams: { pageSize: 10, page: 1 }, } /** Intercept actions for searchUser state and update the state */ diff --git a/client/src/utils/checkAuthenticationCompetition.test.ts b/client/src/utils/checkAuthenticationCompetition.test.ts index 478c4f5e0d7ae43211b45e37bcf92476d6dac004..ecd8e9beb4d2541746c1d0d879e7e7b26aefa421 100644 --- a/client/src/utils/checkAuthenticationCompetition.test.ts +++ b/client/src/utils/checkAuthenticationCompetition.test.ts @@ -15,22 +15,25 @@ it('dispatches correct actions when auth token is ok', async () => { const decodedToken = { iat: 1620216181, exp: 32514436993, - user_claims: { competition_id: 123123, team_id: 321321, view: 'Participant', code: 'ABCDEF' }, + competition_id: 123123, + team_id: 321321, + view: 'Participant', + code: 'ABCDEF', } const testToken = - 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjMyNTE0NDM2OTkzLCJ1c2VyX2NsYWltcyI6eyJjb21wZXRpdGlvbl9pZCI6MTIzMTIzLCJ0ZWFtX2lkIjozMjEzMjEsInZpZXciOiJQYXJ0aWNpcGFudCIsImNvZGUiOiJBQkNERUYifX0.1gPRJcjn3xuPOcgUUffMngIQDoDtxS9RZczcbdyyaaA' + 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2MjAyMTYxODEsImV4cCI6MzI1MTQ0MzY5OTMsImNvbXBldGl0aW9uX2lkIjoxMjMxMjMsInRlYW1faWQiOjMyMTMyMSwidmlldyI6IlBhcnRpY2lwYW50IiwiY29kZSI6IkFCQ0RFRiJ9.fNrU8s-ZHPFCLYqtD2nogmSy31sBtX-8KWu911xNC8I' localStorage.setItem('JudgeToken', testToken) await CheckAuthenticationCompetition('Judge') expect(spy).toBeCalledWith({ type: Types.SET_COMPETITION_LOGIN_DATA, payload: { - competition_id: decodedToken.user_claims.competition_id, - team_id: decodedToken.user_claims.team_id, - view: decodedToken.user_claims.view, + competition_id: decodedToken.competition_id, + team_id: decodedToken.team_id, + view: decodedToken.view, }, }) - expect(spy).toBeCalledWith({ type: Types.SET_PRESENTATION_CODE, payload: decodedToken.user_claims.code }) + expect(spy).toBeCalledWith({ type: Types.SET_PRESENTATION_CODE, payload: decodedToken.code }) expect(spy).toBeCalledWith({ type: Types.SET_PRESENTATION_COMPETITION, payload: compRes.data, @@ -48,7 +51,7 @@ it('dispatches correct actions when getting user data fails', async () => { }) const spy = jest.spyOn(store, 'dispatch') const testToken = - 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjMyNTE0NDM2OTkzLCJ1c2VyX2NsYWltcyI6eyJjb21wZXRpdGlvbl9pZCI6MTIzMTIzLCJ0ZWFtX2lkIjozMjEzMjEsInZpZXciOiJQYXJ0aWNpcGFudCIsImNvZGUiOiJBQkNERUYifX0.1gPRJcjn3xuPOcgUUffMngIQDoDtxS9RZczcbdyyaaA' + 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2MjAyMTYxODEsImV4cCI6MzI1MTQ0MzY5OTMsImNvbXBldGl0aW9uX2lkIjoxMjMxMjMsInRlYW1faWQiOjMyMTMyMSwidmlldyI6IlBhcnRpY2lwYW50IiwiY29kZSI6IkFCQ0RFRiJ9.fNrU8s-ZHPFCLYqtD2nogmSy31sBtX-8KWu911xNC8I' localStorage.setItem('AudienceToken', testToken) await CheckAuthenticationCompetition('Audience') expect(spy).toBeCalledWith({ type: Types.SET_COMPETITION_LOGIN_UNAUTHENTICATED }) diff --git a/client/src/utils/checkAuthenticationCompetition.ts b/client/src/utils/checkAuthenticationCompetition.ts index f6dfd8fe227e5ef5bf71dc1d7cef0a4daf9b9ee9..5da2eb3dac6f6d33bc0fce3b79d8272cfe4ded71 100644 --- a/client/src/utils/checkAuthenticationCompetition.ts +++ b/client/src/utils/checkAuthenticationCompetition.ts @@ -21,13 +21,13 @@ export const CheckAuthenticationCompetition = async (role: 'Judge' | 'Operator' store.dispatch({ type: Types.SET_COMPETITION_LOGIN_DATA, payload: { - competition_id: decodedToken.user_claims.competition_id, - team_id: decodedToken.user_claims.team_id, - view: decodedToken.user_claims.view, + competition_id: decodedToken.competition_id, + team_id: decodedToken.team_id, + view: decodedToken.view, }, }) - getPresentationCompetition(decodedToken.user_claims.competition_id)(store.dispatch, store.getState) - setPresentationCode(decodedToken.user_claims.code)(store.dispatch) + getPresentationCompetition(decodedToken.competition_id)(store.dispatch, store.getState) + setPresentationCode(decodedToken.code)(store.dispatch) }) .catch((error) => { console.log(error) diff --git a/server/app/__init__.py b/server/app/__init__.py index 5edf5d5d7567caa88537716e7702b77a4e48ccfc..e1512dd590b55127e9aa54f2d9925aaa816a09f3 100644 --- a/server/app/__init__.py +++ b/server/app/__init__.py @@ -1,9 +1,11 @@ from flask import Flask, redirect, request from flask_uploads import configure_uploads +from flask_uploads.extensions import IMAGES +from flask_uploads.flask_uploads import UploadSet import app.database.models as models +from app.apis import init_api from app.core import bcrypt, db, jwt, ma -from app.core.dto import MediaDTO def create_app(config_name="configmodule.DevelopmentConfig"): @@ -25,7 +27,7 @@ def create_app(config_name="configmodule.DevelopmentConfig"): db.init_app(app) db.create_all() ma.init_app(app) - configure_uploads(app, (MediaDTO.image_set,)) + configure_uploads(app, (UploadSet("photos", IMAGES),)) # Init socket from app.core.sockets import sio @@ -36,6 +38,7 @@ def create_app(config_name="configmodule.DevelopmentConfig"): from app.apis import flask_api flask_api.init_app(app) + init_api() # Flask helpers methods diff --git a/server/app/apis/__init__.py b/server/app/apis/__init__.py index ad71728a7d6d73d3453e628c9c4305f2c1da30ed..5bb204b3de372e93a4182943013f20a3b7d5b3f9 100644 --- a/server/app/apis/__init__.py +++ b/server/app/apis/__init__.py @@ -1,18 +1,31 @@ -import app.core.http_codes as http_codes +from functools import wraps + from flask_jwt_extended import verify_jwt_in_request -from flask_jwt_extended.utils import get_jwt_claims -from flask_restx.errors import abort +from flask_jwt_extended.utils import get_jwt +from flask_smorest import Blueprint, abort +from flask_smorest.error_handler import ErrorSchema + +Blueprint.PAGINATION_HEADER_FIELD_NAME = "pagination" + +ALL = ["*"] -def validate_editor(db_item, *views): - claims = get_jwt_claims() - city_id = int(claims.get("city_id")) - if db_item.city_id != city_id: - abort(http_codes.UNAUTHORIZED) + +class http_codes: + OK = 200 + NO_CONTENT = 204 + BAD_REQUEST = 400 + UNAUTHORIZED = 401 + FORBIDDEN = 403 + NOT_FOUND = 404 + CONFLICT = 409 + GONE = 410 + INTERNAL_SERVER_ERROR = 500 + SERVICE_UNAVAILABLE = 503 def _is_allowed(allowed, actual): - return actual and "*" in allowed or actual in allowed + return actual and allowed == ALL or actual in allowed def _has_access(in_claim, in_route): @@ -20,104 +33,108 @@ def _has_access(in_claim, in_route): return not in_route or in_claim and in_claim == in_route -def protect_route(allowed_roles=None, allowed_views=None): - def wrapper(func): - def inner(*args, **kwargs): - verify_jwt_in_request() - claims = get_jwt_claims() +# class AuthorizationHeadersSchema(Schema): - # Authorize request if roles has access to the route # +# Authorization = fields.String(required=True) - nonlocal allowed_roles - allowed_roles = allowed_roles or [] - role = claims.get("role") - if _is_allowed(allowed_roles, role): - return func(*args, **kwargs) - # Authorize request if view has access and is trying to access the - # competition its in. Also check team if client is a team. - # Allow request if route doesn't belong to any competition. - - nonlocal allowed_views - allowed_views = allowed_views or [] - view = claims.get("view") - if not _is_allowed(allowed_views, view): - abort( - http_codes.UNAUTHORIZED, - f"Client with view '{view}' is not allowed to access route with allowed views {allowed_views}.", - ) - - claim_competition_id = claims.get("competition_id") - route_competition_id = kwargs.get("competition_id") - if not _has_access(claim_competition_id, route_competition_id): - abort( - http_codes.UNAUTHORIZED, - f"Client in competition '{claim_competition_id}' is not allowed to access competition '{route_competition_id}'.", - ) - - if view == "Team": - claim_team_id = claims.get("team_id") - route_team_id = kwargs.get("team_id") - if not _has_access(claim_team_id, route_team_id): - abort( - http_codes.UNAUTHORIZED, - f"Client in team '{claim_team_id}' is not allowed to access team '{route_team_id}'.", - ) +class ExtendedBlueprint(Blueprint): + def authorization(self, allowed_roles=None, allowed_views=None): + def decorator(func): + + # func = self.arguments(AuthorizationHeadersSchema, location="headers")(func) + func = self.alt_response(http_codes.UNAUTHORIZED, ErrorSchema, description="Unauthorized")(func) + + @wraps(func) + def wrapper(*args, **kwargs): - return func(*args, **kwargs) + # Check that allowed_roles and allowed_views have correct type + nonlocal allowed_roles + nonlocal allowed_views + allowed_roles = allowed_roles or [] + allowed_views = allowed_views or [] + assert ( + isinstance(allowed_roles, list) or allowed_roles == "*" + ), f"Allowed roles must be a list or '*', not '{allowed_roles}'" + assert ( + isinstance(allowed_views, list) or allowed_views == "*" + ), f"Allowed views must be a list or '*', not '{allowed_views}'" - return inner + verify_jwt_in_request() + jwt = get_jwt() - return wrapper + # Authorize request if roles has access to the route # + role = jwt.get("role") + if _is_allowed(allowed_roles, role): + return func(*args, **kwargs) -def text_response(message, code=http_codes.OK): - return {"message": message}, code + # Authorize request if view has access and is trying to access the + # competition its in. Also check team if client is a team. + # Allow request if route doesn't belong to any competition. + view = jwt.get("view") + if not _is_allowed(allowed_views, view): + abort( + http_codes.UNAUTHORIZED, + f"Client with view '{view}' is not allowed to access route with allowed views {allowed_views}.", + ) + + claim_competition_id = jwt.get("competition_id") + route_competition_id = kwargs.get("competition_id") + if not _has_access(claim_competition_id, route_competition_id): + abort( + http_codes.UNAUTHORIZED, + f"Client in competition '{claim_competition_id}' is not allowed to access competition '{route_competition_id}'.", + ) -def list_response(items, total=None, code=http_codes.OK): - if type(items) is not list: - abort(http_codes.INTERNAL_SERVER_ERROR) - if not total: - total = len(items) - return {"items": items, "count": len(items), "total_count": total}, code + if view == "Team": + claim_team_id = jwt.get("team_id") + route_team_id = kwargs.get("team_id") + if not _has_access(claim_team_id, route_team_id): + abort( + http_codes.UNAUTHORIZED, + f"Client in team '{claim_team_id}' is not allowed to access team '{route_team_id}'.", + ) + return func(*args, **kwargs) -def item_response(item, code=http_codes.OK): - if isinstance(item, list): - abort(http_codes.INTERNAL_SERVER_ERROR) - return item, code + return wrapper + return decorator -from flask_restx import Api -from .alternatives import api as alternative_ns -from .answers import api as answer_ns -from .auth import api as auth_ns -from .codes import api as code_ns -from .competitions import api as comp_ns -from .components import api as component_ns -from .media import api as media_ns -from .misc import api as misc_ns -from .questions import api as question_ns -from .scores import api as score_ns -from .slides import api as slide_ns -from .teams import api as team_ns -from .users import api as user_ns +from flask_smorest import Api flask_api = Api() -flask_api.add_namespace(media_ns, path="/api/media") -flask_api.add_namespace(misc_ns, path="/api/misc") -flask_api.add_namespace(user_ns, path="/api/users") -flask_api.add_namespace(auth_ns, path="/api/auth") -flask_api.add_namespace(comp_ns, path="/api/competitions") -flask_api.add_namespace(slide_ns, path="/api/competitions/<competition_id>/slides") -flask_api.add_namespace( - alternative_ns, path="/api/competitions/<competition_id>/slides/<slide_id>/questions/<question_id>/alternatives" -) -flask_api.add_namespace(team_ns, path="/api/competitions/<competition_id>/teams") -flask_api.add_namespace(code_ns, path="/api/competitions/<competition_id>/codes") -flask_api.add_namespace(question_ns, path="/api/competitions/<competition_id>") -flask_api.add_namespace(component_ns, path="/api/competitions/<competition_id>/slides/<slide_id>/components") -flask_api.add_namespace(answer_ns, path="/api/competitions/<competition_id>/teams/<team_id>/answers") -flask_api.add_namespace(score_ns, path="/api/competitions/<competition_id>/teams/<team_id>/answers/question_scores") + + +def init_api(): + + from .alternatives import blp as alternative_blp + from .answers import blp as answer_blp + from .auth import blp as auth_blp + from .codes import blp as code_blp + from .competitions import blp as competition_blp + from .components import blp as component_blp + from .media import blp as media_blp + from .misc import blp as misc_blp + from .questions import blp as question_blp + from .scores import blp as score_blp + from .slides import blp as slide_blp + from .teams import blp as team_blp + from .users import blp as user_blp + + flask_api.register_blueprint(user_blp) + flask_api.register_blueprint(auth_blp) + flask_api.register_blueprint(competition_blp) + flask_api.register_blueprint(misc_blp) + flask_api.register_blueprint(media_blp) + flask_api.register_blueprint(slide_blp) + flask_api.register_blueprint(question_blp) + flask_api.register_blueprint(team_blp) + flask_api.register_blueprint(code_blp) + flask_api.register_blueprint(alternative_blp) + flask_api.register_blueprint(component_blp) + flask_api.register_blueprint(answer_blp) + flask_api.register_blueprint(score_blp) diff --git a/server/app/apis/alternatives.py b/server/app/apis/alternatives.py index d3ae5d9d4be9f3a5835c4bb4c6f00ee5c38aa34d..d4db80fc3405374f10b53665544714d9f97ad41e 100644 --- a/server/app/apis/alternatives.py +++ b/server/app/apis/alternatives.py @@ -3,79 +3,86 @@ All API calls concerning question alternatives. Default route: /api/competitions/<competition_id>/slides/<slide_id>/questions/<question_id>/alternatives """ -from os import abort -import app.core.http_codes as codes import app.database.controller as dbc -from app.apis import item_response, list_response, protect_route -from app.core.dto import QuestionAlternativeDTO -from app.core.parsers import sentinel +from app.core import ma +from app.core.schemas import BaseSchema, QuestionAlternativeSchema +from app.database import models from app.database.models import Question, QuestionAlternative -from flask_restx import Resource, reqparse +from flask.views import MethodView +from flask_smorest import abort +from flask_smorest.error_handler import ErrorSchema -api = QuestionAlternativeDTO.api -schema = QuestionAlternativeDTO.schema -list_schema = QuestionAlternativeDTO.list_schema +from . import ALL, ExtendedBlueprint, http_codes -alternative_parser_add = reqparse.RequestParser() -alternative_parser_add.add_argument("alternative", type=str, default="", location="json") -alternative_parser_add.add_argument("correct", type=str, default="", location="json") +blp = ExtendedBlueprint( + "alternative", + "alternative", + url_prefix="/api/competitions/<competition_id>/slides/<slide_id>/questions/<question_id>/alternatives", + description="Adding, updating, deleting and copy alternatives", +) -alternative_parser_edit = reqparse.RequestParser() -alternative_parser_edit.add_argument("alternative", type=str, default=sentinel, location="json") -alternative_parser_edit.add_argument("alternative_order", type=int, default=sentinel, location="json") -alternative_parser_edit.add_argument("correct", type=str, default=sentinel, location="json") -alternative_parser_edit.add_argument("correct_order", type=int, default=sentinel, location="json") +class AlternativeAddArgsSchema(BaseSchema): + class Meta(BaseSchema.Meta): + model = models.QuestionAlternative -@api.route("") -@api.param("competition_id, slide_id, question_id") -class QuestionAlternativeList(Resource): - @protect_route(allowed_roles=["*"], allowed_views=["*"]) + alternative = ma.auto_field(required=False, missing="") + correct = ma.auto_field(required=False, missing="") + + +class AlternativeEditArgsSchema(BaseSchema): + class Meta(BaseSchema.Meta): + model = models.QuestionAlternative + + alternative = ma.auto_field(required=False) + alternative_order = ma.auto_field(required=False, missing=None) + correct = ma.auto_field(required=False) + correct_order = ma.auto_field(required=False, missing=None) + + +@blp.route("") +class Alternatives(MethodView): + @blp.authorization(allowed_roles=ALL, allowed_views=ALL) + @blp.response(http_codes.OK, QuestionAlternativeSchema(many=True)) def get(self, competition_id, slide_id, question_id): """ Gets the all question alternatives to the specified question. """ + return dbc.get.question_alternative_list(competition_id, slide_id, question_id) - items = dbc.get.question_alternative_list( - competition_id, - slide_id, - question_id, - ) - return list_response(list_schema.dump(items)) - - @protect_route(allowed_roles=["*"]) - def post(self, competition_id, slide_id, question_id): + @blp.authorization(allowed_roles=ALL) + @blp.arguments(AlternativeAddArgsSchema) + @blp.response(http_codes.OK, QuestionAlternativeSchema) + @blp.alt_response(http_codes.CONFLICT, ErrorSchema, description="Could not add alternative") + def post(self, args, competition_id, slide_id, question_id): """ Posts a new question alternative to the specified question using the provided arguments. """ + return dbc.add.question_alternative(**args, question_id=question_id) - args = alternative_parser_add.parse_args(strict=True) - item = dbc.add.question_alternative(**args, question_id=question_id) - return item_response(schema.dump(item)) - -@api.route("/<alternative_id>") -@api.param("competition_id, slide_id, question_id, alternative_id") -class QuestionAlternatives(Resource): - @protect_route(allowed_roles=["*"], allowed_views=["*"]) +@blp.route("/<alternative_id>") +class QuestionAlternatives(MethodView): + @blp.authorization(allowed_roles=ALL, allowed_views=ALL) + @blp.response(http_codes.OK, QuestionAlternativeSchema) + @blp.alt_response(http_codes.NOT_FOUND, ErrorSchema, description="Could not find alternative") def get(self, competition_id, slide_id, question_id, alternative_id): """ Gets the specified question alternative. """ - - items = dbc.get.question_alternative( - competition_id, - slide_id, - question_id, - alternative_id, - ) - return item_response(schema.dump(items)) - - @protect_route(allowed_roles=["*"]) - def put(self, competition_id, slide_id, question_id, alternative_id): + return dbc.get.question_alternative(competition_id, slide_id, question_id, alternative_id) + + @blp.authorization(allowed_roles=ALL) + @blp.arguments(AlternativeEditArgsSchema) + @blp.response(http_codes.OK, QuestionAlternativeSchema) + @blp.alt_response( + http_codes.BAD_REQUEST, ErrorSchema, description="Paramters to edit alternative with is incorrect" + ) + @blp.alt_response(http_codes.NOT_FOUND, ErrorSchema, description="Could not find alternative") + @blp.alt_response(http_codes.CONFLICT, ErrorSchema, description="Could not edit alternative with the given values") + def put(self, args, competition_id, slide_id, question_id, alternative_id): """ Edits the specified question alternative using the provided arguments. """ - args = alternative_parser_edit.parse_args(strict=True) item = dbc.get.question_alternative( competition_id, slide_id, @@ -84,9 +91,12 @@ class QuestionAlternatives(Resource): ) new_alternative_order = args.pop("alternative_order") - if new_alternative_order is not sentinel and item.alternative_order != new_alternative_order: + if new_alternative_order is not None and item.alternative_order != new_alternative_order: if not (0 <= new_alternative_order < dbc.utils.count(QuestionAlternative, {"question_id": question_id})): - abort(codes.BAD_REQUEST, f"Cant change to invalid slide order '{new_alternative_order}'") + abort( + http_codes.BAD_REQUEST, + message=f"Kan inte ändra till ogiltigt sidordning '{new_alternative_order}'", + ) item_question = dbc.get.one(Question, question_id) dbc.utils.move_order( @@ -94,25 +104,20 @@ class QuestionAlternatives(Resource): ) new_correct_order = args.pop("correct_order") - if new_correct_order is not sentinel and item.correct_order != new_correct_order: + if new_correct_order is not None and item.correct_order != new_correct_order: if not (0 <= new_correct_order < dbc.utils.count(QuestionAlternative, {"question_id": question_id})): - abort(codes.BAD_REQUEST, f"Cant change to invalid slide order '{new_correct_order}'") + abort(http_codes.BAD_REQUEST, message=f"Kan inte ändra till ogiltigt sidordning '{new_correct_order}'") item_question = dbc.get.one(Question, question_id) dbc.utils.move_order(item_question.alternatives, "correct_order", item.correct_order, new_correct_order) - item = dbc.edit.default(item, **args) - return item_response(schema.dump(item)) + return dbc.edit.default(item, **args) - @protect_route(allowed_roles=["*"]) + @blp.authorization(allowed_roles=ALL) + @blp.response(http_codes.NO_CONTENT, None) + @blp.alt_response(http_codes.NOT_FOUND, ErrorSchema, description="Could not find alternative") + @blp.alt_response(http_codes.CONFLICT, ErrorSchema, description="Could not delete alternative") def delete(self, competition_id, slide_id, question_id, alternative_id): """ Deletes the specified question alternative. """ - - item = dbc.get.question_alternative( - competition_id, - slide_id, - question_id, - alternative_id, - ) - dbc.delete.default(item) - return {}, codes.NO_CONTENT + dbc.delete.default(dbc.get.question_alternative(competition_id, slide_id, question_id, alternative_id)) + return None diff --git a/server/app/apis/answers.py b/server/app/apis/answers.py index 190f74914c18be00c241f47e2f0a5053b8af13ee..15f99a1c2b7ce569938ee7794ec4679e52bfbe6c 100644 --- a/server/app/apis/answers.py +++ b/server/app/apis/answers.py @@ -4,57 +4,55 @@ Default route: /api/competitions/<competition_id>/teams/<team_id>/answers """ import app.database.controller as dbc -from app.apis import item_response, list_response, protect_route -from app.core.dto import QuestionAlternativeAnswerDTO, QuestionScoreDTO -from app.core.parsers import sentinel -from flask_restx import Resource, reqparse +from app.core import ma +from app.core.schemas import BaseSchema, QuestionAlternativeAnswerSchema +from app.database import models +from flask.views import MethodView -api = QuestionAlternativeAnswerDTO.api -schema = QuestionAlternativeAnswerDTO.schema -list_schema = QuestionAlternativeAnswerDTO.list_schema +from . import ALL, ExtendedBlueprint, http_codes -score_schema = QuestionScoreDTO.schema -score_list_schema = QuestionScoreDTO.list_schema +blp = ExtendedBlueprint( + "answer", + "answer", + url_prefix="/api/competitions/<competition_id>/teams/<team_id>/answers", + description="Adding, updating, deleting and copy answer", +) -answer_parser_add = reqparse.RequestParser() -answer_parser_add.add_argument("answer", type=str, required=True, location="json") +class AnswerAddArgsSchema(BaseSchema): + class Meta(BaseSchema.Meta): + model = models.QuestionAlternativeAnswer -answer_parser_edit = reqparse.RequestParser() -answer_parser_edit.add_argument("answer", type=str, default=sentinel, location="json") + answer = ma.auto_field(required=False) -@api.route("/question_alternatives") -@api.param("competition_id, team_id") -class QuestionAlternativeList(Resource): - @protect_route(allowed_roles=["*"], allowed_views=["*"]) +@blp.route("") +class QuestionAlternativeList(MethodView): + @blp.authorization(allowed_roles=ALL, allowed_views=ALL) + @blp.response(http_codes.OK, QuestionAlternativeAnswerSchema) def get(self, competition_id, team_id): """ Gets all question answers that the specified team has given. """ + return dbc.get.question_alternative_answer_list(competition_id, team_id) - items = dbc.get.question_alternative_answer_list(competition_id, team_id) - return list_response(list_schema.dump(items)) - -@api.route("/question_alternatives/<question_alternative_id>") -@api.param("competition_id, team_id, question_alternative_id") -class QuestionAlternativeAnswers(Resource): - @protect_route(allowed_roles=["*"], allowed_views=["*"]) - def get(self, competition_id, team_id, question_alternative_id): +@blp.route("/<answer_id>") +class QuestionAlternativeAnswers(MethodView): + @blp.authorization(allowed_roles=ALL, allowed_views=ALL) + @blp.response(http_codes.OK, QuestionAlternativeAnswerSchema) + def get(self, competition_id, team_id, answer_id): """ Gets the specified question answer. """ + return dbc.get.question_alternative_answer(competition_id, team_id, answer_id) - item = dbc.get.question_alternative_answer(competition_id, team_id, question_alternative_id) - return item_response(schema.dump(item)) - - @protect_route(allowed_roles=["*"], allowed_views=["*"]) - def put(self, competition_id, team_id, question_alternative_id): + @blp.authorization(allowed_roles=ALL, allowed_views=ALL) + @blp.arguments(AnswerAddArgsSchema) + @blp.response(http_codes.OK, QuestionAlternativeAnswerSchema) + def put(self, args, competition_id, team_id, answer_id): """ Add or edit specified quesiton_answer. """ - item = dbc.get.question_alternative_answer(competition_id, team_id, question_alternative_id, required=False) + item = dbc.get.question_alternative_answer(competition_id, team_id, answer_id, required=False) if item is None: - args = answer_parser_add.parse_args(strict=True) - item = dbc.add.question_alternative_answer(args.get("answer"), question_alternative_id, team_id) + item = dbc.add.question_alternative_answer(args.get("answer"), answer_id, team_id) else: - args = answer_parser_edit.parse_args(strict=True) item = dbc.edit.default(item, **args) - return item_response(schema.dump(item)) + return item diff --git a/server/app/apis/auth.py b/server/app/apis/auth.py index e2bd165dd82b77c96266c810774c84103b8e58a6..18fb5f5d716af23a08e0a126c1cd2ae3e5394da5 100644 --- a/server/app/apis/auth.py +++ b/server/app/apis/auth.py @@ -3,35 +3,37 @@ All API calls concerning question answers. Default route: /api/auth """ -from datetime import datetime, timedelta, timezone +from datetime import datetime, timedelta -import app.core.http_codes as codes import app.database.controller as dbc -from app.apis import item_response, protect_route, text_response +import marshmallow as ma from app.core.codes import verify_code -from app.core.dto import AuthDTO from app.core.sockets import is_active_competition -from app.database.models import User, Whitelist +from app.database.controller.delete import whitelist_to_blacklist +from app.database.models import Whitelist from flask import current_app, has_app_context -from flask_jwt_extended import create_access_token, get_jti, get_raw_jwt -from flask_jwt_extended.utils import get_jti -from flask_restx import Resource, inputs, reqparse +from flask.views import MethodView +from flask_jwt_extended import create_access_token, get_jti +from flask_jwt_extended.utils import get_jti, get_jwt +from flask_smorest import abort +from flask_smorest.error_handler import ErrorSchema -api = AuthDTO.api -schema = AuthDTO.schema -list_schema = AuthDTO.list_schema +from . import ALL, ExtendedBlueprint, http_codes -login_parser = reqparse.RequestParser() -login_parser.add_argument("email", type=inputs.email(), required=True, location="json") -login_parser.add_argument("password", type=str, required=True, location="json") +blp = ExtendedBlueprint( + "auth", "auth", url_prefix="/api/auth", description="Logging in as a user or with a code, and logging out" +) -create_user_parser = login_parser.copy() -create_user_parser.add_argument("city_id", type=int, required=True, location="json") -create_user_parser.add_argument("role_id", type=int, required=True, location="json") -create_user_parser.add_argument("name", type=str, required=False, location="json") -login_code_parser = reqparse.RequestParser() -login_code_parser.add_argument("code", type=str, required=True, location="json") +class UserLoginArgsSchema(ma.Schema): + email = ma.fields.Email(required=True) + password = ma.fields.String(required=True) + + +class UserLoginResponseSchema(ma.Schema): + id = ma.fields.Int() + access_token = ma.fields.String() + if has_app_context(): USER_LOGIN_LOCKED_ATTEMPTS = current_app.config["USER_LOGIN_LOCKED_ATTEMPTS"] @@ -39,13 +41,13 @@ if has_app_context(): def get_user_claims(item_user): - """ Gets user details for jwt-token. """ + """ Gets user details for jwt. """ return {"role": item_user.role.name, "city_id": item_user.city_id} def get_code_claims(item_code): - """ Gets code details for jwt-token. """ + """ Gets code details for jwt. """ return { "view": item_code.view_type.name, @@ -55,65 +57,29 @@ def get_code_claims(item_code): } -@api.route("/test") -class AuthSignup(Resource): - @protect_route(allowed_roles=["Admin"], allowed_views=["*"]) +@blp.route("/test") +class AuthSignup(MethodView): + @blp.authorization(allowed_roles=["Admin"], allowed_views=["*"]) + @blp.response(http_codes.NO_CONTENT, None) def get(self): - """ Tests that the user is an admin. """ + """ Tests that the user is admin or is in a competition. """ + return None - return "ok" +@blp.route("/login") +class AuthLogin(MethodView): + @blp.arguments(UserLoginArgsSchema) + @blp.response(http_codes.OK, UserLoginResponseSchema) + def post(self, args): + """ Logs in the specified user and creates a jwt. """ -@api.route("/signup") -class AuthSignup(Resource): - @protect_route(allowed_roles=["Admin"]) - def post(self): - """ Creates a new user if the user does not already exist. """ - - args = create_user_parser.parse_args(strict=True) - email = args.get("email") - - # Check if email is already used - if dbc.get.user_exists(email): - api.abort(codes.BAD_REQUEST, "User already exists") - - # Add user - item_user = dbc.add.user(**args) - return item_response(schema.dump(item_user)) - - -@api.route("/delete/<user_id>") -@api.param("user_id") -class AuthDelete(Resource): - @protect_route(allowed_roles=["Admin"]) - def delete(self, user_id): - """ Deletes the specified user and adds their token to the blacklist. """ - - item_user = dbc.get.one(User, user_id) - - # Blacklist all the whitelisted tokens - # in use for the user that will be deleted - dbc.delete.whitelist_to_blacklist(Whitelist.user_id == user_id) - - # Delete user - dbc.delete.default(item_user) - return text_response(f"User {user_id} deleted") - - -@api.route("/login") -class AuthLogin(Resource): - def post(self): - """ Logs in the specified user and creates a jwt-token. """ - - args = login_parser.parse_args(strict=True) email = args.get("email") password = args.get("password") - item_user = dbc.get.user_by_email(email) - # Login with unkown email + # Login with unknown email if not item_user: - api.abort(codes.UNAUTHORIZED, "Invalid email or password") + abort(http_codes.UNAUTHORIZED, "Ogiltigt användarnamn eller lösenord") now = datetime.now() @@ -127,15 +93,13 @@ class AuthLogin(Resource): item_user.locked = now + USER_LOGIN_LOCKED_EXPIRES dbc.utils.commit() - api.abort(codes.UNAUTHORIZED, "Invalid email or password") + abort(http_codes.UNAUTHORIZED, "Ogiltigt användarnamn eller lösenord") # Otherwise if login was successful but the user is locked if item_user.locked: - print(item_user.locked) - print(now) # Check if locked is greater than now if item_user.locked.timestamp() > now.timestamp(): - api.abort(codes.UNAUTHORIZED, f"Try again in {item_user.locked} hours.") + abort(http_codes.UNAUTHORIZED, f"Kontot låst, försök igen om {item_user.locked} timmar") else: item_user.locked = None @@ -144,79 +108,65 @@ class AuthLogin(Resource): dbc.utils.commit() # Create the jwt with user.id as the identifier - access_token = create_access_token(item_user.id, user_claims=get_user_claims(item_user)) - - # Login response includes the id and jwt for the user - response = {"id": item_user.id, "access_token": access_token} + access_token = create_access_token(item_user.id, additional_claims=get_user_claims(item_user)) # Whitelist the created jwt dbc.add.whitelist(get_jti(access_token), item_user.id) - return response + # Login response includes the id and jwt for the user + return {"id": item_user.id, "access_token": access_token} -@api.route("/login/code") -class AuthLoginCode(Resource): + +@blp.route("/logout") +class AuthLogout(MethodView): + @blp.authorization(allowed_roles=ALL, allowed_views=ALL) + @blp.response(http_codes.NO_CONTENT, None) def post(self): + """ Logs out. """ + whitelist_to_blacklist(Whitelist.jti == get_jwt()["jti"]) + return None + + +class CodeArgsSchema(ma.Schema): + code = ma.fields.String(required=True) + + +class CodeResponseSchema(ma.Schema): + competition_id = ma.fields.Int() + view = ma.fields.String() + team_id = ma.fields.Int() + access_token = ma.fields.String() + + +@blp.route("/code") +class AuthLoginCode(MethodView): + @blp.arguments(CodeArgsSchema) + @blp.response(http_codes.OK, CodeResponseSchema) + @blp.alt_response(http_codes.UNAUTHORIZED, ErrorSchema, description="Incorrect code or competition is not active") + @blp.alt_response(http_codes.NOT_FOUND, ErrorSchema, description="The code doesn't exist") + def post(self, args): """ Logs in using the provided competition code. """ - args = login_code_parser.parse_args() code = args["code"] - # Check so the code string is valid - if not verify_code(code): - api.abort(codes.UNAUTHORIZED, "Invalid code") + if not verify_code(code): # Check that code string is valid + abort(http_codes.UNAUTHORIZED, message="Felaktigt kod") item_code = dbc.get.code_by_code(code) - if item_code.view_type_id != 4: - if not is_active_competition(item_code.competition_id): - api.abort(codes.UNAUTHORIZED, "Competition not active") + # If joining client is not operator and competition is not active + if item_code.view_type_id != 4 and not is_active_competition(item_code.competition_id): + abort(http_codes.UNAUTHORIZED, message="Tävlingen är ej aktiv") # Create jwt that is only valid for 8 hours access_token = create_access_token( - item_code.id, user_claims=get_code_claims(item_code), expires_delta=timedelta(hours=8) + item_code.id, additional_claims=get_code_claims(item_code), expires_delta=timedelta(hours=8) ) + dbc.add.whitelist(get_jti(access_token), competition_id=item_code.competition_id) # Whitelist the created jwt - # Whitelist the created jwt - dbc.add.whitelist(get_jti(access_token), competition_id=item_code.competition_id) - response = { + return { "competition_id": item_code.competition_id, "view": item_code.view_type.name, "team_id": item_code.team_id, "access_token": access_token, } - return response - - -@api.route("/logout") -class AuthLogout(Resource): - @protect_route(allowed_roles=["*"], allowed_views=["*"]) - def post(self): - """ Logs out. """ - - jti = get_raw_jwt()["jti"] - - # Blacklist the token so the user cannot access the api anymore - dbc.add.blacklist(jti) - - # Remove the the token from the whitelist since it's blacklisted now - Whitelist.query.filter(Whitelist.jti == jti).delete() - - dbc.utils.commit() - return text_response("Logout") - - -""" -@api.route("/refresh") -class AuthRefresh(Resource): - @protect_route(allowed_roles=["*"]) - @jwt_refresh_token_required - def post(self): - old_jti = get_raw_jwt()["jti"] - - item_user = dbc.get.user(get_jwt_identity()) - access_token = create_access_token(item_user.id, user_claims=get_user_claims(item_user)) - dbc.add.blacklist(old_jti) - response = {"access_token": access_token} - return response -""" diff --git a/server/app/apis/codes.py b/server/app/apis/codes.py index 45f9578280e82c306ff716a646505dbffd62be3b..8ea7901763bf85e01417ecabadeb843c3b7d1dcc 100644 --- a/server/app/apis/codes.py +++ b/server/app/apis/codes.py @@ -4,35 +4,32 @@ Default route: /api/competitions/<competition_id>/codes """ import app.database.controller as dbc -from app.apis import item_response, list_response, protect_route -from app.core.dto import CodeDTO +from app.core.schemas import CodeSchema from app.database.models import Code -from flask_restx import Resource +from flask.views import MethodView +from flask_smorest.error_handler import ErrorSchema -api = CodeDTO.api -schema = CodeDTO.schema -list_schema = CodeDTO.list_schema +from . import ALL, ExtendedBlueprint, http_codes +blp = ExtendedBlueprint( + "code", "code", url_prefix="/api/competitions/<competition_id>/codes", description="Operations on codes" +) -@api.route("") -@api.param("competition_id") -class CodesList(Resource): - @protect_route(allowed_roles=["*"], allowed_views=["Operator"]) + +@blp.route("") +class CodesList(MethodView): + @blp.authorization(allowed_roles=ALL, allowed_views=["Operator"]) + @blp.response(http_codes.OK, CodeSchema(many=True)) def get(self, competition_id): """ Gets the all competition codes. """ - - items = dbc.get.code_list(competition_id) - return list_response(list_schema.dump(items), len(items)) + return dbc.get.code_list(competition_id) -@api.route("/<code_id>") -@api.param("competition_id, code_id") -class CodesById(Resource): - @protect_route(allowed_roles=["*"]) +@blp.route("/<code_id>") +class CodesById(MethodView): + @blp.authorization(allowed_roles=ALL) + @blp.response(http_codes.OK, CodeSchema) + @blp.alt_response(http_codes.NOT_FOUND, ErrorSchema, description="Code not found") def put(self, competition_id, code_id): """ Generates a new competition code. """ - - item = dbc.get.one(Code, code_id) - item.code = dbc.utils.generate_unique_code() - dbc.utils.commit_and_refresh(item) - return item_response(schema.dump(item)) + return dbc.edit.default(dbc.get.one(Code, code_id), code=dbc.utils.generate_unique_code()) diff --git a/server/app/apis/competitions.py b/server/app/apis/competitions.py index 2d381425c9e5d3f41bba347128aab7b659bf8885..4f31c8b6338d5335fc85e6b091694d08ba59fc4d 100644 --- a/server/app/apis/competitions.py +++ b/server/app/apis/competitions.py @@ -4,101 +4,106 @@ Default route: /api/competitions """ import app.database.controller as dbc -from app.apis import item_response, list_response, protect_route -from app.core.dto import CompetitionDTO -from app.core.parsers import search_parser, sentinel +from app.core import ma +from app.core.rich_schemas import CompetitionSchemaRich +from app.core.schemas import BaseSchema, CompetitionSchema +from app.database import models from app.database.models import Competition -from flask_restx import Resource, reqparse +from flask.views import MethodView +from flask_smorest.error_handler import ErrorSchema -api = CompetitionDTO.api -schema = CompetitionDTO.schema -rich_schema = CompetitionDTO.rich_schema -list_schema = CompetitionDTO.list_schema +from . import ALL, ExtendedBlueprint, http_codes -competition_parser_add = reqparse.RequestParser() -competition_parser_add.add_argument("name", type=str, required=True, location="json") -competition_parser_add.add_argument("year", type=int, required=True, location="json") -competition_parser_add.add_argument("city_id", type=int, required=True, location="json") +blp = ExtendedBlueprint( + "competitions", "competitions", url_prefix="/api/competitions", description="Operations competitions" +) -competition_parser_edit = reqparse.RequestParser() -competition_parser_edit.add_argument("name", type=str, default=sentinel, location="json") -competition_parser_edit.add_argument("year", type=int, default=sentinel, location="json") -competition_parser_edit.add_argument("city_id", type=int, default=sentinel, location="json") -competition_parser_edit.add_argument("background_image_id", default=sentinel, type=int, location="json") -competition_parser_search = search_parser.copy() -competition_parser_search.add_argument("name", type=str, default=sentinel, location="args") -competition_parser_search.add_argument("year", type=int, default=sentinel, location="args") -competition_parser_search.add_argument("city_id", type=int, default=sentinel, location="args") +class CompetitionAddArgsSchema(BaseSchema): + class Meta(BaseSchema.Meta): + model = models.Competition + name = ma.auto_field() + year = ma.auto_field() + city_id = ma.auto_field() -@api.route("") -class CompetitionsList(Resource): - @protect_route(allowed_roles=["*"]) - def post(self): - """ Posts a new competition. """ - args = competition_parser_add.parse_args(strict=True) +class CompetitionEditArgsSchema(BaseSchema): + class Meta(BaseSchema.Meta): + model = models.Competition - # Add competition - item = dbc.add.competition(**args) + name = ma.auto_field(required=False) + year = ma.auto_field(required=False) + city_id = ma.auto_field(required=False) + background_image_id = ma.auto_field(required=False) - # Add default slide - # dbc.add.slide(item.id) - return item_response(schema.dump(item)) +class CompetitionSearchArgsSchema(BaseSchema): + class Meta(BaseSchema.Meta): + model = models.Competition -@api.route("/<competition_id>") -@api.param("competition_id") -class Competitions(Resource): - @protect_route(allowed_roles=["*"], allowed_views=["*"]) - def get(self, competition_id): - """ Gets the specified competition. """ + name = ma.auto_field(required=False) + year = ma.auto_field(required=False) + city_id = ma.auto_field(required=False) + background_image_id = ma.auto_field(required=False) - item = dbc.get.competition(competition_id) - return item_response(rich_schema.dump(item)) - - @protect_route(allowed_roles=["*"]) - def put(self, competition_id): - """ Edits the specified competition with the specified arguments. """ +@blp.route("") +class Competitions(MethodView): + @blp.authorization(allowed_roles=ALL) + @blp.arguments(CompetitionAddArgsSchema) + @blp.response(http_codes.OK, CompetitionSchema) + @blp.alt_response(http_codes.CONFLICT, ErrorSchema, description="Competition could not be added") + def post(self, args): + """ Adds a new competition. """ + return dbc.add.competition(**args) - args = competition_parser_edit.parse_args(strict=True) - item = dbc.get.one(Competition, competition_id) - item = dbc.edit.default(item, **args) - return item_response(schema.dump(item)) +@blp.route("/<competition_id>") +class CompetitionById(MethodView): + @blp.authorization(allowed_roles=ALL, allowed_views=ALL) + @blp.response(http_codes.OK, CompetitionSchemaRich) + @blp.alt_response(http_codes.NOT_FOUND, ErrorSchema, description="Competition not found") + def get(self, competition_id): + """ Gets the specified competition. """ + return dbc.get.competition(competition_id) + + @blp.authorization(allowed_roles=ALL) + @blp.arguments(CompetitionEditArgsSchema) + @blp.response(http_codes.OK, CompetitionSchema) + @blp.alt_response(http_codes.NOT_FOUND, ErrorSchema, description="Competition not found") + @blp.alt_response(http_codes.CONFLICT, ErrorSchema, description="Competition could not be updated") + def put(self, args, competition_id): + """ Edits the specified competition with the specified arguments. """ + return dbc.edit.default(dbc.get.one(Competition, competition_id), **args) - @protect_route(allowed_roles=["*"]) + @blp.authorization(allowed_roles=ALL) + @blp.response(http_codes.NO_CONTENT, None) + @blp.alt_response(http_codes.NOT_FOUND, ErrorSchema, description="Competition not found") + @blp.alt_response(http_codes.CONFLICT, ErrorSchema, description="Competition could not be deleted") def delete(self, competition_id): """ Deletes the specified competition. """ + dbc.delete.competition(dbc.get.one(Competition, competition_id)) + return None - item = dbc.get.one(Competition, competition_id) - dbc.delete.competition(item) - return "deleted" - - -@api.route("/search") -class CompetitionSearch(Resource): - @protect_route(allowed_roles=["*"]) - def get(self): +@blp.route("/search") +class CompetitionSearch(MethodView): + @blp.authorization(allowed_roles=ALL) + @blp.arguments(CompetitionSearchArgsSchema, location="query") + @blp.paginate() + @blp.response(http_codes.OK, CompetitionSchema(many=True)) + def get(self, args, pagination_parameters): """ Finds a specific competition based on the provided arguments. """ - - args = competition_parser_search.parse_args(strict=True) - items, total = dbc.search.competition(**args) - return list_response(list_schema.dump(items), total) + return dbc.search.competition(pagination_parameters, **args) -@api.route("/<competition_id>/copy") -@api.param("competition_id") -class SlidesOrder(Resource): - @protect_route(allowed_roles=["*"]) +@blp.route("/<competition_id>/copy") +class SlidesOrder(MethodView): + @blp.authorization(allowed_roles=ALL) + @blp.response(http_codes.OK, CompetitionSchema) + @blp.alt_response(http_codes.NOT_FOUND, ErrorSchema, description="Competition not found") + @blp.alt_response(http_codes.CONFLICT, ErrorSchema, description="Competition could not be copied") def post(self, competition_id): """ Creates a deep copy of the specified competition. """ - - item_competition = dbc.get.competition(competition_id) - - item_competition_copy = dbc.copy.competition(item_competition) - - return item_response(schema.dump(item_competition_copy)) + return dbc.copy.competition(dbc.get.competition(competition_id)) diff --git a/server/app/apis/components.py b/server/app/apis/components.py index 39e1932824f6e6ab103f773402bb1c3feb331e43..b72053fa8b861dec25c658295f43de4fc862ad49 100644 --- a/server/app/apis/components.py +++ b/server/app/apis/components.py @@ -3,97 +3,100 @@ All API calls concerning competitions. Default route: /api/competitions/<competition_id>/slides/<slide_id>/components """ -import app.core.http_codes as codes import app.database.controller as dbc -from app.apis import item_response, list_response, protect_route -from app.core.dto import ComponentDTO -from app.core.parsers import sentinel -from flask_restx import Resource, reqparse - -api = ComponentDTO.api -schema = ComponentDTO.schema -list_schema = ComponentDTO.list_schema - -component_parser_add = reqparse.RequestParser() -component_parser_add.add_argument("x", type=int, default=0, location="json") -component_parser_add.add_argument("y", type=int, default=0, location="json") -component_parser_add.add_argument("w", type=int, default=1, location="json") -component_parser_add.add_argument("h", type=int, default=1, location="json") -component_parser_add.add_argument("type_id", type=int, required=True, location="json") -component_parser_add.add_argument("view_type_id", type=int, required=True, location="json") -component_parser_add.add_argument("text", type=str, default=None, location="json") -component_parser_add.add_argument("media_id", type=int, default=None, location="json") -component_parser_add.add_argument("question_id", type=int, default=None, location="json") - -component_parser_edit = reqparse.RequestParser() -component_parser_edit.add_argument("x", type=int, default=sentinel, location="json") -component_parser_edit.add_argument("y", type=int, default=sentinel, location="json") -component_parser_edit.add_argument("w", type=int, default=sentinel, location="json") -component_parser_edit.add_argument("h", type=int, default=sentinel, location="json") -component_parser_edit.add_argument("text", type=str, default=sentinel, location="json") -component_parser_edit.add_argument("media_id", type=int, default=sentinel, location="json") -component_parser_edit.add_argument("question_id", type=int, default=sentinel, location="json") - - -@api.route("") -@api.param("competition_id, slide_id") -class ComponentList(Resource): - @protect_route(allowed_roles=["*"], allowed_views=["*"]) +from app.core.schemas import BaseSchema, ComponentSchema +from flask.views import MethodView +from flask_smorest.error_handler import ErrorSchema +from marshmallow import fields + +from . import ALL, ExtendedBlueprint, http_codes + +blp = ExtendedBlueprint( + "component", + "component", + url_prefix="/api/competitions/<competition_id>/slides/<slide_id>/components", + description="Adding, updating, deleting and copy components", +) + + +class ComponentAddArgsSchema(BaseSchema): + + x = fields.Integer(required=False, missing=0) + y = fields.Integer(required=False, missing=0) + w = fields.Integer(required=False, missing=1) + h = fields.Integer(required=False, missing=1) + + type_id = fields.Integer(required=True) + view_type_id = fields.Integer(required=True) + + text = fields.String(required=False) + media_id = fields.Integer(required=False) + question_id = fields.Integer(required=False) + + +class ComponentEditArgsSchema(BaseSchema): + + x = fields.Integer(required=False) + y = fields.Integer(required=False) + w = fields.Integer(required=False) + h = fields.Integer(required=False) + + type_id = fields.Integer(required=False) + view_type_id = fields.Integer(required=False) + + text = fields.String(required=False) + media_id = fields.Integer(required=False) + question_id = fields.Integer(required=False) + + +@blp.route("") +class Components(MethodView): + @blp.authorization(allowed_roles=ALL, allowed_views=ALL) + @blp.response(http_codes.OK, ComponentSchema(many=True)) def get(self, competition_id, slide_id): """ Gets all components in the specified slide and competition. """ + return dbc.get.component_list(competition_id, slide_id) - items = dbc.get.component_list(competition_id, slide_id) - return list_response(list_schema.dump(items)) - - @protect_route(allowed_roles=["*"]) - def post(self, competition_id, slide_id): + @blp.authorization(allowed_roles=ALL) + @blp.arguments(ComponentAddArgsSchema) + @blp.response(http_codes.OK, ComponentSchema) + def post(self, args, competition_id, slide_id): """ Posts a new component to the specified slide. """ - - args = component_parser_add.parse_args() - item = dbc.add.component(slide_id=slide_id, **args) - return item_response(schema.dump(item)) + return dbc.add.component(slide_id=slide_id, **args) -@api.route("/<component_id>") -@api.param("competition_id, slide_id, component_id") -class ComponentByID(Resource): - @protect_route(allowed_roles=["*"], allowed_views=["*"]) +@blp.route("/<component_id>") +class ComponentById(MethodView): + @blp.authorization(allowed_roles=ALL, allowed_views=ALL) + @blp.response(http_codes.OK, ComponentSchema) + @blp.alt_response(http_codes.NOT_FOUND, ErrorSchema, description="Could not find component") def get(self, competition_id, slide_id, component_id): """ Gets the specified component. """ - - item = dbc.get.component(competition_id, slide_id, component_id) - return item_response(schema.dump(item)) - - @protect_route(allowed_roles=["*"]) - def put(self, competition_id, slide_id, component_id): + return dbc.get.component(competition_id, slide_id, component_id) + + @blp.authorization(allowed_roles=ALL) + @blp.arguments(ComponentEditArgsSchema) + @blp.response(http_codes.OK, ComponentSchema) + @blp.alt_response(http_codes.NOT_FOUND, ErrorSchema, description="Could not find component") + @blp.alt_response(http_codes.CONFLICT, ErrorSchema, description="Could not update component with given values") + def put(self, args, competition_id, slide_id, component_id): """ Edits the specified component using the provided arguments. """ + return dbc.edit.default(dbc.get.component(competition_id, slide_id, component_id), **args) - args = component_parser_edit.parse_args(strict=True) - item = dbc.get.component(competition_id, slide_id, component_id) - args_without_sentinel = {key: value for key, value in args.items() if value is not sentinel} - item = dbc.edit.default(item, **args_without_sentinel) - return item_response(schema.dump(item)) - - @protect_route(allowed_roles=["*"]) + @blp.authorization(allowed_roles=ALL) + @blp.response(http_codes.NO_CONTENT, None) + @blp.alt_response(http_codes.NOT_FOUND, ErrorSchema, description="Could not find component") + @blp.alt_response(http_codes.CONFLICT, ErrorSchema, description="Could not delete component") def delete(self, competition_id, slide_id, component_id): """ Deletes the specified component. """ + dbc.delete.component(dbc.get.component(competition_id, slide_id, component_id)) + return None - item = dbc.get.component(competition_id, slide_id, component_id) - dbc.delete.component(item) - return {}, codes.NO_CONTENT - -@api.route("/<component_id>/copy/<view_type_id>") -@api.param("competition_id, slide_id, component_id, view_type_id") -class ComponentList(Resource): - @protect_route(allowed_roles=["*"]) +@blp.route("/<component_id>/copy/<view_type_id>") +class ComponentCopy(MethodView): + @blp.authorization(allowed_roles=ALL) + @blp.response(http_codes.OK, ComponentSchema) def post(self, competition_id, slide_id, component_id, view_type_id): """ Creates a deep copy of the specified component. """ - - item_component = dbc.get.component( - competition_id, - slide_id, - component_id, - ) - item = dbc.copy.component(item_component, slide_id, view_type_id) - return item_response(schema.dump(item)) + return dbc.copy.component(dbc.get.component(competition_id, slide_id, component_id), slide_id, view_type_id) diff --git a/server/app/apis/media.py b/server/app/apis/media.py index 3d95bd370c49c9953898377fc1aabb16d74ab877..a20cd1b0bdba88518be7a430f5ea71f910d71adc 100644 --- a/server/app/apis/media.py +++ b/server/app/apis/media.py @@ -4,85 +4,94 @@ Default route: /api/media """ import app.core.files as files -import app.core.http_codes as codes import app.database.controller as dbc -from app.apis import item_response, list_response, protect_route -from app.core.dto import MediaDTO -from app.core.parsers import search_parser, sentinel -from app.database.models import Media +from app.core import ma +from app.core.schemas import BaseSchema, MediaSchema +from app.database import models from flask import request +from flask.views import MethodView from flask_jwt_extended import get_jwt_identity -from flask_restx import Resource +from flask_smorest import abort +from flask_smorest.error_handler import ErrorSchema from flask_uploads import UploadNotAllowed from sqlalchemy import exc -api = MediaDTO.api -image_set = MediaDTO.image_set -schema = MediaDTO.schema -list_schema = MediaDTO.list_schema +from . import ALL, ExtendedBlueprint, http_codes -media_parser_search = search_parser.copy() -media_parser_search.add_argument("filename", type=str, default=sentinel, location="args") +class ImageSearchArgsSchema(BaseSchema): + class Meta(BaseSchema.Meta): + model = models.Media + + filename = ma.auto_field(required=False) -@api.route("/images") -class ImageList(Resource): - @protect_route(allowed_roles=["*"]) - def get(self): - """ Gets a list of all images with the specified filename. """ - args = media_parser_search.parse_args(strict=True) - items, total = dbc.search.image(**args) - return list_response(list_schema.dump(items), total) +blp = ExtendedBlueprint("media", "media", url_prefix="/api/media", description="Operations on media") - @protect_route(allowed_roles=["*"]) + +@blp.route("/images") +class Images(MethodView): + @blp.authorization(allowed_roles=ALL) + @blp.arguments(ImageSearchArgsSchema, location="query") + @blp.paginate() + @blp.response(http_codes.OK, MediaSchema(many=True)) + def get(self, args, pagination_parameters): + """ Gets a list of all images with the specified filename. """ + return dbc.search.image(pagination_parameters, **args) + + @blp.authorization(allowed_roles=ALL) + @blp.response(http_codes.OK, MediaSchema) + @blp.alt_response(http_codes.BAD_REQUEST, ErrorSchema, description="Could not save image") + @blp.alt_response(http_codes.INTERNAL_SERVER_ERROR, ErrorSchema, description="Could not save image") def post(self): """ Posts the specified image. """ if "image" not in request.files: - api.abort(codes.BAD_REQUEST, "Missing image in request.files") + abort(http_codes.BAD_REQUEST, message="Missing image in request.files") try: filename = files.save_image_with_thumbnail(request.files["image"]) - item = Media.query.filter(Media.filename == filename).first() + item = models.Media.query.filter(models.Media.filename == filename).first() if not item: item = dbc.add.image(filename, get_jwt_identity()) - return item_response(schema.dump(item)) + return item except UploadNotAllowed: - api.abort(codes.BAD_REQUEST, "Could not save the image") + abort(http_codes.BAD_REQUEST, message="Could not save the image") except: - api.abort(codes.INTERNAL_SERVER_ERROR, "Something went wrong when trying to save image") + abort(http_codes.INTERNAL_SERVER_ERROR, message="Something went wrong when trying to save image") -@api.route("/images/<media_id>") -@api.param("media_id") -class ImageList(Resource): - @protect_route(allowed_roles=["*"], allowed_views=["*"]) +@blp.route("/images/<media_id>") +class ImageById(MethodView): + @blp.authorization(allowed_roles=ALL, allowed_views=ALL) + @blp.response(http_codes.OK, MediaSchema) + @blp.alt_response(http_codes.NOT_FOUND, MediaSchema, description="Could not find image") def get(self, media_id): """ Gets the specified image. """ + return dbc.get.one(models.Media, media_id) - item = dbc.get.one(Media, media_id) - return item_response(schema.dump(item)) - - @protect_route(allowed_roles=["*"]) + @blp.authorization(allowed_roles=ALL) + @blp.response(http_codes.NO_CONTENT, None) + @blp.response(http_codes.CONFLICT, None, description="Could not delete image it is used by something") + @blp.response(http_codes.BAD_REQUEST, None, description="Failed to delete image") + @blp.response(http_codes.INTERNAL_SERVER_ERROR, ErrorSchema, description="Somehting very serious went wrong") def delete(self, media_id): """ Deletes the specified image. """ - - item = dbc.get.one(Media, media_id) + item = dbc.get.one(models.Media, media_id) if len(item.image_components) > 0: - api.abort(codes.CONFLICT, "Component depends on this Image") + abort(http_codes.CONFLICT, "Component depends on this Image") if len(item.competition_background_images) > 0: - api.abort(codes.CONFLICT, "Competition background image depends on this Image") + abort(http_codes.CONFLICT, "Competition background image depends on this Image") if len(item.slide_background_images) > 0: - api.abort(codes.CONFLICT, "Slide background image depends on this Image") + abort(http_codes.CONFLICT, "Slide background image depends on this Image") try: files.delete_image_and_thumbnail(item.filename) dbc.delete.default(item) - return {}, codes.NO_CONTENT + return None except OSError: - api.abort(codes.BAD_REQUEST, "Could not delete the file image") + abort(http_codes.BAD_REQUEST, "Could not delete the file image") except exc.SQLAlchemyError: - api.abort(codes.INTERNAL_SERVER_ERROR, "Something went wrong when trying to delete image") + abort(http_codes.INTERNAL_SERVER_ERROR, "Something went wrong when trying to delete image") diff --git a/server/app/apis/misc.py b/server/app/apis/misc.py index 0f4aba74c8c5edb467f08b1c66e0e4801aa5506b..713a707db73f56cf5d8e332f615c4a3c558ba52d 100644 --- a/server/app/apis/misc.py +++ b/server/app/apis/misc.py @@ -4,100 +4,116 @@ Default route: /api/misc """ import app.database.controller as dbc -from app.apis import list_response, protect_route -from app.core import http_codes -from app.core.dto import MiscDTO +import marshmallow as ma +from app.core.schemas import ( + BaseSchema, + CitySchema, + ComponentTypeSchema, + MediaTypeSchema, + QuestionTypeSchema, + RoleSchema, + ViewTypeSchema, +) +from app.database import models from app.database.models import City, Competition, ComponentType, MediaType, QuestionType, Role, User, ViewType -from flask_restx import Resource, reqparse +from flask.views import MethodView +from flask_smorest.error_handler import ErrorSchema +from marshmallow_sqlalchemy import auto_field -api = MiscDTO.api +from . import ALL, ExtendedBlueprint, http_codes -question_type_schema = MiscDTO.question_type_schema -media_type_schema = MiscDTO.media_type_schema -component_type_schema = MiscDTO.component_type_schema -view_type_schema = MiscDTO.view_type_schema +blp = ExtendedBlueprint("misc", "misc", url_prefix="/api/misc", description="Roles, regions, types and statistics") -role_schema = MiscDTO.role_schema -city_schema = MiscDTO.city_schema +class TypesResponseSchema(BaseSchema): + media_types = ma.fields.Nested(MediaTypeSchema, many=True) + component_types = ma.fields.Nested(ComponentTypeSchema, many=True) + question_types = ma.fields.Nested(QuestionTypeSchema, many=True) + view_types = ma.fields.Nested(ViewTypeSchema, many=True) -name_parser = reqparse.RequestParser() -name_parser.add_argument("name", type=str, required=True, location="json") - -@api.route("/types") -class TypesList(Resource): +@blp.route("/types") +class Types(MethodView): + @blp.response(http_codes.OK, TypesResponseSchema) def get(self): - """ Gets a list of all types. """ - - result = {} - result["media_types"] = media_type_schema.dump(dbc.get.all(MediaType)) - result["component_types"] = component_type_schema.dump(dbc.get.all(ComponentType)) - result["question_types"] = question_type_schema.dump(dbc.get.all(QuestionType)) - result["view_types"] = view_type_schema.dump(dbc.get.all(ViewType)) - return result - - -@api.route("/roles") -class RoleList(Resource): - @protect_route(allowed_roles=["*"]) + """ Gets a list of all types """ + return dict( + media_types=dbc.get.all(MediaType), + component_types=dbc.get.all(ComponentType), + question_types=dbc.get.all(QuestionType), + view_types=dbc.get.all(ViewType), + ) + + +@blp.route("/roles") +class RoleList(MethodView): + @blp.authorization(allowed_roles=ALL) + @blp.response(http_codes.OK, RoleSchema(many=True)) def get(self): """ Gets a list of all roles. """ + return dbc.get.all(Role) - items = dbc.get.all(Role) - return list_response(role_schema.dump(items)) +class CityAddArgsSchema(BaseSchema): + class Meta(BaseSchema.Meta): + model = models.City -@api.route("/cities") -class CitiesList(Resource): - @protect_route(allowed_roles=["*"]) + name = auto_field() + + +@blp.route("/cities") +class CitiesList(MethodView): + @blp.authorization(allowed_roles=ALL) + @blp.response(http_codes.OK, CitySchema(many=True)) def get(self): """ Gets a list of all cities. """ + return dbc.get.all(City) - items = dbc.get.all(City) - return list_response(city_schema.dump(items)) - - @protect_route(allowed_roles=["Admin"]) - def post(self): + @blp.authorization(allowed_roles=["Admin"]) + @blp.arguments(CitySchema) + @blp.response(http_codes.OK, CitySchema(many=True)) + def post(self, args): """ Posts the specified city. """ - - args = name_parser.parse_args(strict=True) - dbc.add.city(args["name"]) - items = dbc.get.all(City) - return list_response(city_schema.dump(items)) - - -@api.route("/cities/<ID>") -@api.param("ID") -class Cities(Resource): - @protect_route(allowed_roles=["Admin"]) - def put(self, ID): + dbc.add.city(**args) + return dbc.get.all(City) + + +@blp.route("/cities/<city_id>") +class Cities(MethodView): + @blp.authorization(allowed_roles=["Admin"]) + @blp.arguments(CitySchema) + @blp.response(http_codes.OK, CitySchema(many=True)) + @blp.alt_response(http_codes.NOT_FOUND, ErrorSchema, description="City not found") + @blp.alt_response( + http_codes.CONFLICT, ErrorSchema, description="The city can't be updated with the provided values" + ) + def put(self, args, city_id): """ Edits the specified city with the provided arguments. """ - - item = dbc.get.one(City, ID) - args = name_parser.parse_args(strict=True) - item.name = args["name"] - dbc.utils.commit_and_refresh(item) - items = dbc.get.all(City) - return list_response(city_schema.dump(items)) - - @protect_route(allowed_roles=["Admin"]) - def delete(self, ID): + dbc.edit.default(dbc.get.one(City, city_id), **args) + return dbc.get.all(City) + + @blp.authorization(allowed_roles=["Admin"]) + @blp.response(http_codes.OK, CitySchema(many=True)) + @blp.alt_response(http_codes.NOT_FOUND, ErrorSchema, description="City not found") + @blp.alt_response( + http_codes.CONFLICT, ErrorSchema, description="The city can't be updated with the provided values" + ) + def delete(self, city_id): """ Deletes the specified city. """ + dbc.delete.default(dbc.get.one(City, city_id)) + return dbc.get.all(City) + - item = dbc.get.one(City, ID) - dbc.delete.default(item) - items = dbc.get.all(City) - return list_response(city_schema.dump(items)) +class StatisticsResponseSchema(BaseSchema): + users = ma.fields.Int() + competitions = ma.fields.Int() + regions = ma.fields.Int() -@api.route("/statistics") -class Statistics(Resource): - @protect_route(allowed_roles=["*"]) +@blp.route("/statistics") +class Statistics(MethodView): + @blp.authorization(allowed_roles=ALL) + @blp.response(http_codes.OK, StatisticsResponseSchema) def get(self): """ Gets statistics. """ - - user_count = dbc.utils.count(User) - competition_count = dbc.utils.count(Competition) - region_count = dbc.utils.count(City) - return {"users": user_count, "competitions": competition_count, "regions": region_count}, http_codes.OK + return {"users": User.query.count(), "competitions": Competition.query.count(), "regions": City.query.count()} diff --git a/server/app/apis/questions.py b/server/app/apis/questions.py index 6ba32382fa524180c76eef35bd02a68382e552c6..092c91d5460fbc877d05d2f67bf00130151eb4e3 100644 --- a/server/app/apis/questions.py +++ b/server/app/apis/questions.py @@ -3,87 +3,85 @@ All API calls concerning question answers. Default route: /api/competitions/<competition_id> """ -import app.core.http_codes as codes import app.database.controller as dbc -from app.apis import item_response, list_response, protect_route -from app.core.dto import QuestionDTO -from app.core.parsers import sentinel -from flask_restx import Resource, reqparse - -api = QuestionDTO.api -schema = QuestionDTO.schema -list_schema = QuestionDTO.list_schema - -question_parser_add = reqparse.RequestParser() -question_parser_add.add_argument("name", type=str, default=None, location="json") -question_parser_add.add_argument("total_score", type=int, default=None, location="json") -question_parser_add.add_argument("type_id", type=int, required=True, location="json") -question_parser_add.add_argument("correcting_instructions", type=str, default=None, location="json") - -question_parser_edit = reqparse.RequestParser() -question_parser_edit.add_argument("name", type=str, default=sentinel, location="json") -question_parser_edit.add_argument("total_score", type=int, default=sentinel, location="json") -question_parser_edit.add_argument("type_id", type=int, default=sentinel, location="json") -question_parser_edit.add_argument("correcting_instructions", type=str, default=sentinel, location="json") - - -@api.route("/questions") -@api.param("competition_id") -class QuestionList(Resource): - @protect_route(allowed_roles=["*"]) - def get(self, competition_id): - """ Gets all questions in the specified competition. """ - - items = dbc.get.question_list_for_competition(competition_id) - return list_response(list_schema.dump(items)) - - -@api.route("/slides/<slide_id>/questions") -@api.param("competition_id, slide_id") -class QuestionListForSlide(Resource): - @protect_route(allowed_roles=["*"]) +from app.core import ma +from app.core.schemas import BaseSchema, QuestionSchema +from app.database import models +from flask.views import MethodView +from flask_smorest.error_handler import ErrorSchema + +from . import ALL, ExtendedBlueprint, http_codes + +blp = ExtendedBlueprint( + "question", + "question", + url_prefix="/api/competitions/<competition_id>/slides/<slide_id>/questions", + description="Adding, updating and deleting questions", +) + + +class QuestionAddArgsSchema(BaseSchema): + class Meta(BaseSchema.Meta): + model = models.Question + + name = ma.auto_field(required=False, missing="") + total_score = ma.auto_field(required=False, missing=None) + type_id = ma.auto_field(required=True) + correcting_instructions = ma.auto_field(required=False, missing=None) + + +class QuestionEditArgsSchema(BaseSchema): + class Meta(BaseSchema.Meta): + model = models.Question + + name = ma.auto_field(required=False) + total_score = ma.auto_field(required=False) + type_id = ma.auto_field(required=False) + correcting_instructions = ma.auto_field(required=False) + + +@blp.route("") +class Questions(MethodView): + @blp.authorization(allowed_roles=ALL) + @blp.response(http_codes.OK, QuestionSchema(many=True)) def get(self, competition_id, slide_id): """ Gets all questions in the specified competition and slide. """ + return dbc.get.question_list(competition_id, slide_id) - items = dbc.get.question_list(competition_id, slide_id) - return list_response(list_schema.dump(items)) - - @protect_route(allowed_roles=["*"]) - def post(self, competition_id, slide_id): + @blp.authorization(allowed_roles=ALL) + @blp.arguments(QuestionAddArgsSchema) + @blp.response(http_codes.OK, QuestionSchema) + @blp.alt_response(http_codes.CONFLICT, ErrorSchema, description="Could not add question") + def post(self, args, competition_id, slide_id): """ Posts a new question to the specified slide using the provided arguments. """ - - args = question_parser_add.parse_args(strict=True) - item = dbc.add.question(slide_id=slide_id, **args) - return item_response(schema.dump(item)) + return dbc.add.question(slide_id=slide_id, **args) -@api.route("/slides/<slide_id>/questions/<question_id>") -@api.param("competition_id, slide_id, question_id") -class QuestionById(Resource): - @protect_route(allowed_roles=["*"]) +@blp.route("/<question_id>") +class QuestionById(MethodView): + @blp.authorization(allowed_roles=ALL) + @blp.response(http_codes.OK, QuestionSchema) + @blp.alt_response(http_codes.NOT_FOUND, ErrorSchema, description="Could not find question") def get(self, competition_id, slide_id, question_id): """ Gets the specified question using the specified competition and slide. """ - - item_question = dbc.get.question(competition_id, slide_id, question_id) - return item_response(schema.dump(item_question)) - - @protect_route(allowed_roles=["*"]) - def put(self, competition_id, slide_id, question_id): + return dbc.get.question(competition_id, slide_id, question_id) + + @blp.authorization(allowed_roles=ALL) + @blp.arguments(QuestionEditArgsSchema) + @blp.response(http_codes.OK, QuestionSchema) + @blp.alt_response(http_codes.NOT_FOUND, ErrorSchema, description="Could not find question") + @blp.alt_response(http_codes.CONFLICT, ErrorSchema, description="Could not edit question") + def put(self, args, competition_id, slide_id, question_id): """ Edits the specified question with the provided arguments. """ + return dbc.edit.default(dbc.get.question(competition_id, slide_id, question_id), **args) - args = question_parser_edit.parse_args(strict=True) - - item_question = dbc.get.question(competition_id, slide_id, question_id) - item_question = dbc.edit.default(item_question, **args) - - return item_response(schema.dump(item_question)) - - @protect_route(allowed_roles=["*"]) + @blp.authorization(allowed_roles=ALL) + @blp.response(http_codes.NO_CONTENT, None) + @blp.alt_response(http_codes.NOT_FOUND, ErrorSchema, description="Could not find question") + @blp.alt_response(http_codes.CONFLICT, ErrorSchema, description="Could not delete question") def delete(self, competition_id, slide_id, question_id): """ Deletes the specified question. """ - - item_question = dbc.get.question(competition_id, slide_id, question_id) - dbc.delete.question(item_question) - return {}, codes.NO_CONTENT + dbc.delete.question(dbc.get.question(competition_id, slide_id, question_id)) + return None diff --git a/server/app/apis/scores.py b/server/app/apis/scores.py index 2d303e1c734b5f776334ea2775fdb1b165db3480..8e7208d685538b0e83970cb2accff3f5caa93938 100644 --- a/server/app/apis/scores.py +++ b/server/app/apis/scores.py @@ -4,53 +4,59 @@ Default route: /api/competitions/<competition_id>/teams/<team_id>/answers/quesit """ import app.database.controller as dbc -from app.apis import item_response, list_response, protect_route -from app.core.dto import QuestionScoreDTO -from app.core.parsers import sentinel -from flask_restx import Resource, reqparse +from app.core import ma +from app.core.schemas import BaseSchema, QuestionScoreSchema +from app.database import models +from flask.views import MethodView +from flask_smorest.error_handler import ErrorSchema -api = QuestionScoreDTO.api -schema = QuestionScoreDTO.schema -list_schema = QuestionScoreDTO.list_schema +from . import ALL, ExtendedBlueprint, http_codes -score_parser_add = reqparse.RequestParser() -score_parser_add.add_argument("score", type=int, required=False, location="json") +blp = ExtendedBlueprint( + "score", + "score", + url_prefix="/api/competitions/<competition_id>/teams/<team_id>/scores", + description="Operations on scores", +) -score_parser_edit = reqparse.RequestParser() -score_parser_edit.add_argument("score", type=int, default=sentinel, location="json") +class ScoreAddArgsSchema(BaseSchema): + class Meta(BaseSchema.Meta): + model = models.QuestionScore -@api.route("/") -@api.param("competition_id, team_id") -class QuestionScoreList(Resource): - @protect_route(allowed_roles=["*"], allowed_views=["*"]) + score = ma.auto_field(required=False) + + +@blp.route("") +class QuestionScoreList(MethodView): + @blp.authorization(allowed_roles=ALL, allowed_views=ALL) + @blp.response(http_codes.OK, QuestionScoreSchema(many=True)) def get(self, competition_id, team_id): """ Gets all question answers that the specified team has given. """ - - items = dbc.get.question_score_list(competition_id, team_id) - return list_response(list_schema.dump(items)) + return dbc.get.question_score_list(competition_id, team_id) -@api.route("/<question_id>") -@api.param("competition_id, team_id, question_id") -class QuestionScores(Resource): - @protect_route(allowed_roles=["*"], allowed_views=["*"]) +@blp.route("/<question_id>") +class QuestionScores(MethodView): + @blp.authorization(allowed_roles=ALL, allowed_views=ALL) + @blp.response(http_codes.OK, QuestionScoreSchema) + @blp.alt_response(http_codes.NOT_FOUND, ErrorSchema, description="Cant find answer") def get(self, competition_id, team_id, question_id): - """ Gets the specified question answer. """ - - item = dbc.get.question_score(competition_id, team_id, question_id) - return item_response(schema.dump(item)) - - @protect_route(allowed_roles=["*"], allowed_views=["*"]) - def put(self, competition_id, team_id, question_id): + """ Gets the score for the provided team on the provided question. """ + return dbc.get.question_score(competition_id, team_id, question_id) + + @blp.authorization(allowed_roles=ALL, allowed_views=ALL) + @blp.arguments(ScoreAddArgsSchema) + @blp.response(http_codes.OK, QuestionScoreSchema) + @blp.alt_response(http_codes.NOT_FOUND, ErrorSchema, description="Cant find score") + @blp.alt_response(http_codes.CONFLICT, ErrorSchema, description="Can't add or edit score with provided values") + def put(self, args, competition_id, team_id, question_id): """ Add or edit specified quesiton_answer. """ item = dbc.get.question_score(competition_id, team_id, question_id, required=False) if item is None: - args = score_parser_add.parse_args(strict=True) item = dbc.add.question_score(args.get("score"), question_id, team_id) else: - args = score_parser_edit.parse_args(strict=True) item = dbc.edit.default(item, **args) - return item_response(schema.dump(item)) + return item diff --git a/server/app/apis/slides.py b/server/app/apis/slides.py index 654ee49f6ce02d8e56d6d5529fcbbcf0c0b12af2..86854efc9cf764250bc34d7791503c2362de8a5f 100644 --- a/server/app/apis/slides.py +++ b/server/app/apis/slides.py @@ -3,93 +3,95 @@ All API calls concerning question alternatives. Default route: /api/competitions/<competition_id>/slides """ -import app.core.http_codes as codes import app.database.controller as dbc -from app.apis import item_response, list_response, protect_route -from app.core.dto import SlideDTO -from app.core.parsers import sentinel +from app.core import ma +from app.core.schemas import BaseSchema, SlideSchema +from app.database import models from app.database.models import Competition, Slide -from flask_restx import Resource, reqparse -from flask_restx.errors import abort +from flask.views import MethodView +from flask_smorest import abort +from flask_smorest.error_handler import ErrorSchema -api = SlideDTO.api -schema = SlideDTO.schema -list_schema = SlideDTO.list_schema +from . import ALL, ExtendedBlueprint, http_codes -slide_parser_edit = reqparse.RequestParser() -slide_parser_edit.add_argument("order", type=int, default=sentinel, location="json") -slide_parser_edit.add_argument("title", type=str, default=sentinel, location="json") -slide_parser_edit.add_argument("timer", type=int, default=sentinel, location="json") -slide_parser_edit.add_argument("order", type=int, default=sentinel, location="json") -slide_parser_edit.add_argument("background_image_id", default=sentinel, type=int, location="json") +blp = ExtendedBlueprint( + "slide", + "slide", + url_prefix="/api/competitions/<competition_id>/slides", + description="Adding, updating, deleting and copy slide", +) -@api.route("") -@api.param("competition_id") -class SlidesList(Resource): - @protect_route(allowed_roles=["*"]) +class SlideEditArgsSchema(BaseSchema): + class Meta(BaseSchema.Meta): + model = models.Slide + + title = ma.auto_field(required=False) + timer = ma.auto_field(required=False) + order = ma.auto_field(required=False, missing=None) + background_image_id = ma.auto_field(required=False) + + +@blp.route("") +class Slides(MethodView): + @blp.authorization(allowed_roles=ALL) + @blp.response(http_codes.OK, SlideSchema(many=True)) def get(self, competition_id): """ Gets all slides from the specified competition. """ + return dbc.get.slide_list(competition_id) - items = dbc.get.slide_list(competition_id) - return list_response(list_schema.dump(items)) - - @protect_route(allowed_roles=["*"]) + @blp.authorization(allowed_roles=ALL) + @blp.response(http_codes.OK, SlideSchema) + @blp.alt_response(http_codes.CONFLICT, ErrorSchema, description="Can't add slide") def post(self, competition_id): """ Posts a new slide to the specified competition. """ + return dbc.add.slide(competition_id) - item_slide = dbc.add.slide(competition_id) - return item_response(schema.dump(item_slide)) - -@api.route("/<slide_id>") -@api.param("competition_id, slide_id") -class Slides(Resource): - @protect_route(allowed_roles=["*"]) +@blp.route("/<slide_id>") +class Slides(MethodView): + @blp.authorization(allowed_roles=ALL) + @blp.response(http_codes.OK, SlideSchema) + @blp.alt_response(http_codes.NOT_FOUND, ErrorSchema) def get(self, competition_id, slide_id): """ Gets the specified slide. """ - - item_slide = dbc.get.slide(competition_id, slide_id) - return item_response(schema.dump(item_slide)) - - @protect_route(allowed_roles=["*"]) - def put(self, competition_id, slide_id): + return dbc.get.slide(competition_id, slide_id) + + @blp.authorization(allowed_roles=ALL) + @blp.arguments(SlideEditArgsSchema) + @blp.response(http_codes.OK, SlideSchema) + @blp.alt_response(http_codes.CONFLICT, ErrorSchema, description="Can't edit slide") + @blp.alt_response(http_codes.BAD_REQUEST, ErrorSchema, description="Can't edit slide with the provided arguments") + def put(self, args, competition_id, slide_id): """ Edits the specified slide using the provided arguments. """ - args = slide_parser_edit.parse_args(strict=True) - item_slide = dbc.get.slide(competition_id, slide_id) new_order = args.pop("order") - if new_order is not sentinel and item_slide.order != new_order: + if new_order is not None and item_slide.order != new_order: if not (0 <= new_order < dbc.utils.count(Slide, {"competition_id": competition_id})): - abort(codes.BAD_REQUEST, f"Cant change to invalid slide order '{new_order}'") + abort(http_codes.BAD_REQUEST, f"Cant change to invalid slide order '{new_order}'") item_competition = dbc.get.one(Competition, competition_id) dbc.utils.move_order(item_competition.slides, "order", item_slide.order, new_order) - item_slide = dbc.edit.default(item_slide, **args) - - return item_response(schema.dump(item_slide)) + return dbc.edit.default(item_slide, **args) - @protect_route(allowed_roles=["*"]) + @blp.authorization(allowed_roles=ALL) + @blp.response(http_codes.NO_CONTENT, None) + @blp.alt_response(http_codes.NOT_FOUND, ErrorSchema, description="Slide not found") + @blp.alt_response(http_codes.CONFLICT, ErrorSchema, description="Can't delete slide") def delete(self, competition_id, slide_id): """ Deletes the specified slide. """ + dbc.delete.slide(dbc.get.slide(competition_id, slide_id)) + return None - item_slide = dbc.get.slide(competition_id, slide_id) - - dbc.delete.slide(item_slide) - return {}, codes.NO_CONTENT - -@api.route("/<slide_id>/copy") -@api.param("competition_id,slide_id") -class SlideCopy(Resource): - @protect_route(allowed_roles=["*"]) +@blp.route("/<slide_id>/copy") +class SlideCopy(MethodView): + @blp.authorization(allowed_roles=ALL) + @blp.response(http_codes.OK, SlideSchema) + @blp.alt_response(http_codes.NOT_FOUND, ErrorSchema, description="Can't find slide") def post(self, competition_id, slide_id): """ Creates a deep copy of the specified slide. """ - - item_slide = dbc.get.slide(competition_id, slide_id) - item_slide_copy = dbc.copy.slide(item_slide) - - return item_response(schema.dump(item_slide_copy)) + return dbc.copy.slide(dbc.get.slide(competition_id, slide_id)) diff --git a/server/app/apis/teams.py b/server/app/apis/teams.py index 913deeb789340939c3dcb37f4a3edefbd7d06e95..7a1b4841f1a327f006abf5675a326b13f12f4028 100644 --- a/server/app/apis/teams.py +++ b/server/app/apis/teams.py @@ -3,70 +3,75 @@ All API calls concerning question alternatives. Default route: /api/competitions/<competition_id>/teams """ -import app.core.http_codes as codes +from flask_smorest.error_handler import ErrorSchema import app.database.controller as dbc -from app.apis import item_response, list_response, protect_route -from app.core.dto import TeamDTO -from app.core.parsers import sentinel -from flask_restx import Resource, reqparse +from app.core import ma +from app.core.schemas import BaseSchema, TeamSchema +from app.database import models +from flask.views import MethodView -api = TeamDTO.api -schema = TeamDTO.schema -list_schema = TeamDTO.list_schema +from . import ALL, ExtendedBlueprint, http_codes -team_parser_add = reqparse.RequestParser() -team_parser_add.add_argument("name", type=str, required=True, location="json") +blp = ExtendedBlueprint( + "team", + "team", + url_prefix="/api/competitions/<competition_id>/teams", + description="Operations on teams", +) -team_parser_edit = reqparse.RequestParser() -team_parser_edit.add_argument("name", type=str, default=sentinel, location="json") +class TeamAddArgsSchema(BaseSchema): + class Meta(BaseSchema.Meta): + model = models.Team -@api.route("") -@api.param("competition_id") -class TeamsList(Resource): - @protect_route(allowed_roles=["*"]) + name = ma.auto_field(required=True) + + +class TeamEditArgsSchema(BaseSchema): + class Meta(BaseSchema.Meta): + model = models.Team + + name = ma.auto_field(required=False) + + +@blp.route("") +class Teams(MethodView): + @blp.authorization(allowed_roles=ALL) + @blp.response(http_codes.OK, TeamSchema(many=True)) def get(self, competition_id): """ Gets all teams to the specified competition. """ + return dbc.get.team_list(competition_id) - items = dbc.get.team_list(competition_id) - return list_response(list_schema.dump(items)) - - @protect_route(allowed_roles=["*"]) - def post(self, competition_id): + @blp.authorization(allowed_roles=ALL) + @blp.arguments(TeamAddArgsSchema) + @blp.response(http_codes.OK, TeamSchema) + @blp.alt_response(http_codes.CONFLICT, ErrorSchema, description="Could not add team") + def post(self, args, competition_id): """ Posts a new team to the specified competition. """ - - args = team_parser_add.parse_args(strict=True) - item_team = dbc.add.team(args["name"], competition_id) - return item_response(schema.dump(item_team)) + return dbc.add.team(args["name"], competition_id) -@api.route("/<team_id>") -@api.param("competition_id,team_id") -class Teams(Resource): - @protect_route(allowed_roles=["*"]) +@blp.route("/<team_id>") +class TeamsById(MethodView): + @blp.authorization(allowed_roles=ALL) + @blp.response(http_codes.OK, TeamSchema) + @blp.alt_response(http_codes.NOT_FOUND, ErrorSchema, description="Could not find team") def get(self, competition_id, team_id): """ Gets the specified team. """ + return dbc.get.team(competition_id, team_id) - item = dbc.get.team(competition_id, team_id) - return item_response(schema.dump(item)) - - @protect_route(allowed_roles=["*"]) - def put(self, competition_id, team_id): + @blp.authorization(allowed_roles=ALL) + @blp.arguments(TeamEditArgsSchema) + @blp.response(http_codes.OK, TeamSchema) + @blp.alt_response(http_codes.NOT_FOUND, ErrorSchema, description="Could not find team") + def put(self, args, competition_id, team_id): """ Edits the specified team using the provided arguments. """ + return dbc.edit.default(dbc.get.team(competition_id, team_id), **args) - args = team_parser_edit.parse_args(strict=True) - name = args.get("name") - - item_team = dbc.get.team(competition_id, team_id) - - item_team = dbc.edit.default(item_team, name=name, competition_id=competition_id) - return item_response(schema.dump(item_team)) - - @protect_route(allowed_roles=["*"]) + @blp.authorization(allowed_roles=ALL) + @blp.response(http_codes.NO_CONTENT, None) + @blp.alt_response(http_codes.NOT_FOUND, ErrorSchema, description="Could not find team") def delete(self, competition_id, team_id): """ Deletes the specified team. """ - - item_team = dbc.get.team(competition_id, team_id) - - dbc.delete.team(item_team) - return {}, codes.NO_CONTENT + dbc.delete.team(dbc.get.team(competition_id, team_id)) + return None diff --git a/server/app/apis/users.py b/server/app/apis/users.py index bf393a89abcc0f85d4885e8333afab1e0df8b56f..9617cd4543bbe0ff97eead8d0a34d9f2bfed86fe 100644 --- a/server/app/apis/users.py +++ b/server/app/apis/users.py @@ -3,92 +3,131 @@ All API calls concerning question alternatives. Default route: /api/users """ -import app.core.http_codes as codes + import app.database.controller as dbc -from app.apis import item_response, list_response, protect_route -from app.core.dto import UserDTO -from app.core.parsers import search_parser, sentinel -from app.database.models import User -from flask_jwt_extended import get_jwt_identity -from flask_restx import Resource, inputs, reqparse - -api = UserDTO.api -schema = UserDTO.schema -list_schema = UserDTO.list_schema - -user_parser_edit = reqparse.RequestParser() -user_parser_edit.add_argument("email", type=inputs.email(), default=sentinel, location="json") -user_parser_edit.add_argument("name", type=str, default=sentinel, location="json") -user_parser_edit.add_argument("city_id", type=int, default=sentinel, location="json") -user_parser_edit.add_argument("role_id", type=int, default=sentinel, location="json") - -user_search_parser = search_parser.copy() -user_search_parser.add_argument("name", type=str, default=sentinel, location="args") -user_search_parser.add_argument("email", type=str, default=sentinel, location="args") -user_search_parser.add_argument("city_id", type=int, default=sentinel, location="args") -user_search_parser.add_argument("role_id", type=int, default=sentinel, location="args") +from app.core import ma +from app.core.schemas import BaseSchema, UserSchema +from app.database import models +from app.database.models import User, Whitelist +from flask.views import MethodView +from flask_jwt_extended.utils import get_jwt_identity +from flask_smorest import abort +from flask_smorest.error_handler import ErrorSchema +from marshmallow import fields +from . import ALL, ExtendedBlueprint, http_codes -def _edit_user(item_user, args): - """ Edits a user using the provided arguments. """ +blp = ExtendedBlueprint( + "users", "users", url_prefix="/api/users", description="Adding, updating, deleting and searching for users" +) - email = args.get("email") - name = args.get("name") - if email: - if dbc.get.user_exists(email): - api.abort(codes.BAD_REQUEST, "Email is already in use") - if name: - args["name"] = args["name"].title() +class UserAddArgsSchema(BaseSchema): + class Meta(BaseSchema.Meta): + model = models.User - return dbc.edit.default(item_user, **args) + name = ma.auto_field() + password = fields.String(required=True) + email = ma.auto_field(required=True) + role_id = ma.auto_field(required=True) + city_id = ma.auto_field(required=True) -@api.route("") -class UsersList(Resource): - @protect_route(allowed_roles=["*"]) - def get(self): - """ Gets all users. """ +class UserEditArgsSchema(BaseSchema): + class Meta(BaseSchema.Meta): + model = models.User - item = dbc.get.one(User, get_jwt_identity()) - return item_response(schema.dump(item)) + name = ma.auto_field(required=False) + email = ma.auto_field(required=False) + role_id = ma.auto_field(required=False) + city_id = ma.auto_field(required=False) - @protect_route(allowed_roles=["*"]) - def put(self): - """ Posts a new user using the specified arguments. """ - args = user_parser_edit.parse_args(strict=True) - item = dbc.get.one(User, get_jwt_identity()) - item = _edit_user(item, args) - return item_response(schema.dump(item)) +class UserSearchArgsSchema(BaseSchema): + class Meta(BaseSchema.Meta): + model = models.User + name = ma.auto_field(required=False) + email = ma.auto_field(required=False) + role_id = ma.auto_field(required=False) + city_id = ma.auto_field(required=False) -@api.route("/<ID>") -@api.param("ID") -class Users(Resource): - @protect_route(allowed_roles=["*"]) - def get(self, ID): - """ Gets the specified user. """ - item = dbc.get.one(User, ID) - return item_response(schema.dump(item)) +@blp.route("") +class Users(MethodView): + @blp.authorization(allowed_roles=ALL) + @blp.response(http_codes.OK, UserSchema) + def get(self): + """ Get currently logged in user. """ + return dbc.get.one(User, get_jwt_identity()) + + @blp.authorization(allowed_roles=ALL) + @blp.arguments(UserEditArgsSchema) + @blp.response(http_codes.OK, UserSchema) + def put(self, args): + """ Edit current user. """ + return _edit_user(dbc.get.one(User, get_jwt_identity()), args) + + @blp.authorization(allowed_roles=["Admin"]) + @blp.arguments(UserAddArgsSchema) + @blp.response(http_codes.OK, UserSchema) + def post(self, args): + """ Creates a new user if the user does not already exist. """ + return dbc.add.user(**args) + + +@blp.route("/<user_id>") +class UsersById(MethodView): + @blp.authorization(allowed_roles=ALL) + @blp.response(http_codes.OK, UserSchema) + @blp.alt_response(http_codes.NOT_FOUND, ErrorSchema, description="User not found") + def get(self, user_id): + """ Get user with <user_id> """ + return dbc.get.one(User, user_id) + + @blp.authorization(allowed_roles=["Admin"]) + @blp.arguments(UserEditArgsSchema) + @blp.response(http_codes.OK, UserSchema) + @blp.alt_response(http_codes.NOT_FOUND, ErrorSchema, description="User not found") + @blp.alt_response( + http_codes.CONFLICT, ErrorSchema, description="The user can't be updated with the provided values" + ) + def put(self, args, user_id): + """ Edits user with <user_id> """ + return _edit_user(dbc.get.one(User, user_id), args) + + @blp.authorization(allowed_roles=["Admin"]) + @blp.response(http_codes.NO_CONTENT, None) + @blp.alt_response(http_codes.NOT_FOUND, ErrorSchema, description="User not found") + @blp.alt_response(http_codes.CONFLICT, ErrorSchema, description="The user can't be deleted") + def delete(self, user_id): + """ Deletes the specified user and adds their token to the blacklist. """ + item_user = dbc.get.one(User, user_id) + dbc.delete.whitelist_to_blacklist(Whitelist.user_id == user_id) # Blacklist all the whitelisted tokens + dbc.delete.default(item_user) + return None + + +@blp.route("/search") +class UserSearch(MethodView): + @blp.authorization(allowed_roles=ALL) + @blp.arguments(UserSearchArgsSchema, location="query") + @blp.paginate() + @blp.response(http_codes.OK, UserSchema(many=True)) + def get(self, args, pagination_parameters): + """ Finds a specific user based on the provided arguments. """ + return dbc.search.user(pagination_parameters, **args) - @protect_route(allowed_roles=["Admin"]) - def put(self, ID): - """ Edits the specified team using the provided arguments. """ - args = user_parser_edit.parse_args(strict=True) - item = dbc.get.one(User, ID) - item = _edit_user(item, args) - return item_response(schema.dump(item)) +def _edit_user(item_user, args): + """ Edits a user using the provided arguments. """ + email = args.get("email") + name = args.get("name") -@api.route("/search") -class UserSearch(Resource): - @protect_route(allowed_roles=["*"]) - def get(self): - """ Finds a specific user based on the provided arguments. """ + if email and dbc.get.user_exists(email): + abort(http_codes.CONFLICT, message="En användare med den mejladressen finns redan") + if name: + args["name"] = args["name"].title() - args = user_search_parser.parse_args(strict=True) - items, total = dbc.search.user(**args) - return list_response(list_schema.dump(items), total) + return dbc.edit.default(item_user, **args) diff --git a/server/app/core/__init__.py b/server/app/core/__init__.py index 091a76c349930355887a34a72915b14ad2950ed4..b65b3dc0df1360ea285e5820c7c18ee669c99551 100644 --- a/server/app/core/__init__.py +++ b/server/app/core/__init__.py @@ -16,15 +16,12 @@ jwt = JWTManager() ma = Marshmallow() -@jwt.token_in_blacklist_loader -def check_if_token_in_blacklist(decrypted_token): +@jwt.token_in_blocklist_loader +def check_if_token_in_blacklist(jwt_headers, jwt_data): """ An extension method with flask_jwt_extended that will execute when jwt verifies Check if the token is blacklisted in the database - :param decrypted_token: jti or string of the jwt - :type decrypted_token: str - :return: True if token is blacklisted - :rtype: bool """ - jti = decrypted_token["jti"] + + jti = jwt_data["jti"] return models.Blacklist.query.filter_by(jti=jti).first() is not None diff --git a/server/app/core/dto.py b/server/app/core/dto.py deleted file mode 100644 index e4e0bf0d34feb93e93597460c19a422501b4dba4..0000000000000000000000000000000000000000 --- a/server/app/core/dto.py +++ /dev/null @@ -1,93 +0,0 @@ -""" -The DTO module (short for Data Transfer Object) connects the namespace of an -API and its related schemas. -""" - -import app.core.rich_schemas as rich_schemas -import app.core.schemas as schemas -from flask_restx import Namespace -from flask_uploads import IMAGES, UploadSet - - -class ComponentDTO: - api = Namespace("component") - schema = schemas.ComponentSchema(many=False) - list_schema = schemas.ComponentSchema(many=True) - - -class MediaDTO: - api = Namespace("media") - image_set = UploadSet("photos", IMAGES) - schema = schemas.MediaSchema(many=False) - list_schema = schemas.MediaSchema(many=True) - - -class AuthDTO: - api = Namespace("auth") - schema = schemas.UserSchema(many=False) - list_schema = schemas.UserSchema(many=True) - - -class UserDTO: - api = Namespace("users") - schema = schemas.UserSchema(many=False) - list_schema = schemas.UserSchema(many=True) - - -class CompetitionDTO: - api = Namespace("competitions") - schema = schemas.CompetitionSchema(many=False) - list_schema = schemas.CompetitionSchema(many=True) - rich_schema = rich_schemas.CompetitionSchemaRich(many=False) - - -class CodeDTO: - api = Namespace("codes") - schema = schemas.CodeSchema(many=False) - list_schema = schemas.CodeSchema(many=True) - - -class SlideDTO: - api = Namespace("slides") - schema = schemas.SlideSchema(many=False) - list_schema = schemas.SlideSchema(many=True) - - -class TeamDTO: - api = Namespace("teams") - schema = schemas.TeamSchema(many=False) - list_schema = schemas.TeamSchema(many=True) - - -class MiscDTO: - api = Namespace("misc") - role_schema = schemas.RoleSchema(many=True) - question_type_schema = schemas.QuestionTypeSchema(many=True) - media_type_schema = schemas.MediaTypeSchema(many=True) - component_type_schema = schemas.ComponentTypeSchema(many=True) - view_type_schema = schemas.ViewTypeSchema(many=True) - city_schema = schemas.CitySchema(many=True) - - -class QuestionDTO: - api = Namespace("questions") - schema = schemas.QuestionSchema(many=False) - list_schema = schemas.QuestionSchema(many=True) - - -class QuestionAlternativeDTO: - api = Namespace("alternatives") - schema = schemas.QuestionAlternativeSchema(many=False) - list_schema = schemas.QuestionAlternativeSchema(many=True) - - -class QuestionAlternativeAnswerDTO: - api = Namespace("answers") - schema = schemas.QuestionAlternativeAnswerSchema(many=False) - list_schema = schemas.QuestionAlternativeAnswerSchema(many=True) - - -class QuestionScoreDTO: - api = Namespace("answers") - schema = schemas.QuestionScoreSchema(many=False) - list_schema = schemas.QuestionScoreSchema(many=True) diff --git a/server/app/core/http_codes.py b/server/app/core/http_codes.py deleted file mode 100644 index 1f6f5524e54d1fed15d989ce6407a3ed1b4ddc42..0000000000000000000000000000000000000000 --- a/server/app/core/http_codes.py +++ /dev/null @@ -1,14 +0,0 @@ -""" -This module defines all the http status codes thats used in the api. -""" - -OK = 200 -NO_CONTENT = 204 -BAD_REQUEST = 400 -UNAUTHORIZED = 401 -FORBIDDEN = 403 -NOT_FOUND = 404 -CONFLICT = 409 -GONE = 410 -INTERNAL_SERVER_ERROR = 500 -SERVICE_UNAVAILABLE = 503 diff --git a/server/app/core/parsers.py b/server/app/core/parsers.py deleted file mode 100644 index 3b541cf2778e4a3c153c950c96cd911ba3e04323..0000000000000000000000000000000000000000 --- a/server/app/core/parsers.py +++ /dev/null @@ -1,29 +0,0 @@ -""" -This module contains the parsers used to parse the data gotten in api requests. -""" - -from flask_restx import reqparse - - -class Sentinel: - """ - Sentinel is used as default argument to parsers if it isn't necessary to - supply a value. This is used instead of None so that None can be supplied - as value. - """ - - def __repr__(self): - return "Sentinel" - - def __bool__(self): - return False - - -sentinel = Sentinel() - -###SEARCH#### -search_parser = reqparse.RequestParser() -search_parser.add_argument("page", type=int, default=0, location="args") -search_parser.add_argument("page_size", type=int, default=15, location="args") -search_parser.add_argument("order", type=int, default=1, location="args") -search_parser.add_argument("order_by", type=str, default=None, location="args") diff --git a/server/app/core/sockets.py b/server/app/core/sockets.py index e2e6037e0ec018e81c3a49c6b15ed80dc8789015..b7a7e53c24e2a1cff90392cb5f70cb69f028eb69 100644 --- a/server/app/core/sockets.py +++ b/server/app/core/sockets.py @@ -3,11 +3,11 @@ Contains all functionality related sockets. That is starting, joining, ending, disconnecting from and syncing active competitions. """ import logging +from functools import wraps -from decorator import decorator from flask.globals import request from flask_jwt_extended import verify_jwt_in_request -from flask_jwt_extended.utils import get_jwt_claims +from flask_jwt_extended.utils import get_jwt from flask_socketio import SocketIO, emit, join_room logger = logging.getLogger(__name__) @@ -30,8 +30,8 @@ def _unpack_claims(): :rtype: tuple """ - claims = get_jwt_claims() - return claims["competition_id"], claims["view"] + jwt = get_jwt() + return jwt["competition_id"], jwt["view"] def is_active_competition(competition_id): @@ -55,8 +55,7 @@ def _get_sync_variables(active_competition, sync_values): return {key: value for key, value in active_competition.items() if key in sync_values} -@decorator -def authorize_client(f, allowed_views=None, require_active_competition=True, *args, **kwargs): +def authorization(allowed_views=None, require_active_competition=True): """ Decorator used to authorize a client that sends socket events. Check that the client has authorization headers, that client view gotten from claims @@ -64,31 +63,41 @@ def authorize_client(f, allowed_views=None, require_active_competition=True, *ar if require_active_competition is True. """ - try: - verify_jwt_in_request() - except: - logger.error(f"Won't call function '{f.__name__}': Missing Authorization Header") - return + def decorator(func): + @wraps(func) + def wrapper(*args, **kwargs): + try: + verify_jwt_in_request() + except: + logger.error(f"Won't call function '{func.__name__}': Missing Authorization Header") + return - def _is_allowed(allowed, actual): - return actual and "*" in allowed or actual in allowed + def _is_allowed(allowed, actual): + return actual and "*" in allowed or actual in allowed - competition_id, view = _unpack_claims() + competition_id, view = _unpack_claims() + + if require_active_competition and not is_active_competition(competition_id): + logger.error(f"Won't call function '{func.__name__}': Competition '{competition_id}' is not active") + return + + nonlocal allowed_views + allowed_views = allowed_views or [] + if not _is_allowed(allowed_views, view): + logger.error( + f"Won't call function '{func.__name__}': View '{view}' is not '{' or '.join(allowed_views)}'" + ) + return - if require_active_competition and not is_active_competition(competition_id): - logger.error(f"Won't call function '{f.__name__}': Competition '{competition_id}' is not active") - return + return func(*args, **kwargs) - allowed_views = allowed_views or [] - if not _is_allowed(allowed_views, view): - logger.error(f"Won't call function '{f.__name__}': View '{view}' is not '{' or '.join(allowed_views)}'") - return + return wrapper - return f(*args, **kwargs) + return decorator @sio.event -@authorize_client(require_active_competition=False, allowed_views=["*"]) +@authorization(require_active_competition=False, allowed_views=["*"]) def connect() -> None: """ Connect to a active competition. If competition with competition_id is not active, @@ -122,7 +131,7 @@ def connect() -> None: @sio.event -@authorize_client(allowed_views=["*"]) +@authorization(allowed_views=["*"]) def disconnect() -> None: """ Remove client from the active_competition it was in. Delete active_competition if no @@ -139,7 +148,7 @@ def disconnect() -> None: @sio.event -@authorize_client(allowed_views=["Operator"]) +@authorization(allowed_views=["Operator"]) def end_presentation() -> None: """ End a presentation by sending end_presentation to all connected clients. @@ -150,7 +159,7 @@ def end_presentation() -> None: @sio.event -@authorize_client(allowed_views=["Operator"]) +@authorization(allowed_views=["Operator"]) def sync(data) -> None: """ Update all values from data thats in an active_competitions. Also sync all diff --git a/server/app/database/__init__.py b/server/app/database/__init__.py index 47fd3079c10314adc7391f4282fbec3b2ebe36e8..92e6dc8eff7a09ed2dbef788238693e5dde51190 100644 --- a/server/app/database/__init__.py +++ b/server/app/database/__init__.py @@ -3,7 +3,11 @@ The database submodule contaisn all functionality that has to do with the database. It can add, get, delete, edit, search and copy items. """ -from flask_restx import abort +from app.apis import http_codes +from flask_smorest import abort +from flask_smorest.pagination import PaginationParameters + +# from flask_restx import abort from flask_sqlalchemy import BaseQuery from flask_sqlalchemy.model import Model from sqlalchemy import Column, DateTime @@ -25,7 +29,7 @@ class ExtendedQuery(BaseQuery): Extensions to a regular query which makes using the database more convenient. """ - def first_api(self, required=True, error_message=None, error_code=404): + def first_api(self, required=True, error_message=None, error_code=http_codes.NOT_FOUND): """ Extensions of the first() functions otherwise used on queries. Abort if no item was found and it was required. @@ -43,12 +47,11 @@ class ExtendedQuery(BaseQuery): item = self.first() if required and not item: - error_message = error_message or "Object not found" - abort(error_code, error_message) + abort(error_code, message=error_message or "Objektet hittades inte") return item - def pagination(self, page=0, page_size=15, order_column=None, order=1): + def paginate_api(self, pagination_parameters, order_column=None, order=1): """ When looking for lists of items this is used to only return a few of them to allow for pagination. @@ -64,30 +67,11 @@ class ExtendedQuery(BaseQuery): :rtype: list, int """ - query = self - if order_column: - if order == 1: - query = query.order_by(order_column) - else: - query = query.order_by(order_column.desc()) - - total = query.count() - query = query.limit(page_size).offset(page * page_size) - items = query.all() - return items, total - + pagination_parameters = pagination_parameters or PaginationParameters(page=1, page_size=10) -# class Dictionary(TypeDecorator): - -# impl = Text - -# def process_bind_param(self, value, dialect): -# if value is not None: -# value = json.dumps(value) - -# return value + if order_column: + self = self.order_by(order_column if order == 1 else order_column.desc()) -# def process_result_value(self, value, dialect): -# if value is not None: -# value = json.loads(value) -# return value + pagination = self.paginate(page=pagination_parameters.page, per_page=pagination_parameters.page_size) + pagination_parameters.item_count = pagination.total + return pagination.items diff --git a/server/app/database/controller/add.py b/server/app/database/controller/add.py index 1f5ebca00eeaa96d4bdd722819cba2aa79a8f79f..9031eded1b37000485a3f8e8b7c76d7342369f70 100644 --- a/server/app/database/controller/add.py +++ b/server/app/database/controller/add.py @@ -4,21 +4,36 @@ This file contains functionality to add data to the database. import os -import app.core.http_codes as codes import app.database.controller as dbc +from app.apis import http_codes from app.core import db -from app.database.models import (Blacklist, City, Code, Competition, - ComponentType, ImageComponent, Media, - MediaType, Question, QuestionAlternative, - QuestionAlternativeAnswer, QuestionComponent, - QuestionScore, QuestionType, Role, Slide, - Team, TextComponent, User, ViewType, - Whitelist) -from app.database.types import (IMAGE_COMPONENT_ID, QUESTION_COMPONENT_ID, - TEXT_COMPONENT_ID) +from app.database.models import ( + Blacklist, + City, + Code, + Competition, + ComponentType, + ImageComponent, + Media, + MediaType, + Question, + QuestionAlternative, + QuestionAlternativeAnswer, + QuestionComponent, + QuestionScore, + QuestionType, + Role, + Slide, + Team, + TextComponent, + User, + ViewType, + Whitelist, +) +from app.database.types import IMAGE_COMPONENT_ID, QUESTION_COMPONENT_ID, TEXT_COMPONENT_ID from flask import current_app from flask.globals import current_app -from flask_restx import abort +from flask_smorest import abort from PIL import Image from sqlalchemy import exc @@ -33,21 +48,16 @@ def db_add(item): db.session.commit() db.session.refresh(item) except (exc.IntegrityError): - abort(codes.CONFLICT, f"Item of type {type(item)} cannot be added due to an Integrity Constraint") + db.session.rollback() + abort(http_codes.CONFLICT, message=f"Kunde inte lägga objektet") except (exc.SQLAlchemyError, exc.DBAPIError): db.session.rollback() # SQL errors such as item already exists - abort( - codes.INTERNAL_SERVER_ERROR, - f"Item of type {type(item)} could not be created", - ) + abort(http_codes.INTERNAL_SERVER_ERROR, message=f"Kunde inte lägga objektet") except: db.session.rollback() # Catching other errors - abort( - codes.INTERNAL_SERVER_ERROR, - f"Something went wrong when creating {type(item)}", - ) + abort(http_codes.INTERNAL_SERVER_ERROR, message=f"Kunde lägga till objektet") return item @@ -92,7 +102,7 @@ def component(type_id, slide_id, view_type_id, x=0, y=0, w=0, h=0, copy=False, * ) item.question_id = data.get("question_id") else: - abort(codes.BAD_REQUEST, f"Invalid type_id{type_id}") + abort(http_codes.BAD_REQUEST, f"Ogiltigt typ_id '{type_id}'") item = dbc.utils.commit_and_refresh(item) return item @@ -256,7 +266,7 @@ def question_alternative(alternative, correct, question_id): def question_score(score, question_id, team_id): """ - Adds a question answer to the specified team + Adds a question score to the specified team and question using the provided arguments. """ diff --git a/server/app/database/controller/copy.py b/server/app/database/controller/copy.py index d64d8a5bb43b91e3eff9935df74ed6d3a48901fe..f16ce3feb54b300aed46e8575cb1a78d8c82a967 100644 --- a/server/app/database/controller/copy.py +++ b/server/app/database/controller/copy.py @@ -3,7 +3,7 @@ This file contains functionality to copy and duplicate data to the database. """ from app.database.controller import add, get, search, utils -from app.database.models import Question +from app.database.models import Competition, Question from app.database.types import IMAGE_COMPONENT_ID, QUESTION_COMPONENT_ID, TEXT_COMPONENT_ID @@ -115,9 +115,9 @@ def competition(item_competition_old): """ name = "Kopia av " + item_competition_old.name - item_competition, total = search.competition(name=name) - if item_competition: - name = "Kopia av " + item_competition[total - 1].name + + while item_competition := Competition.query.filter(Competition.name == name).first(): + name = "Kopia av " + item_competition.name item_competition_new = add._competition_no_slides( name, diff --git a/server/app/database/controller/delete.py b/server/app/database/controller/delete.py index d3f017bade09890b9820f80fd0ba8300a8fe317e..3a617d3c7df5d0b96fee21261f0694cd6bb9b27c 100644 --- a/server/app/database/controller/delete.py +++ b/server/app/database/controller/delete.py @@ -2,11 +2,13 @@ This file contains functionality to delete data to the database. """ -import app.core.http_codes as codes import app.database.controller as dbc +from app.apis import http_codes from app.core import db from app.database.models import QuestionAlternativeAnswer, QuestionScore, Whitelist -from flask_restx import abort +from flask_smorest import abort + +# from flask_restx import abort from sqlalchemy.exc import IntegrityError @@ -18,13 +20,10 @@ def default(item): db.session.commit() except IntegrityError: db.session.rollback() - abort(codes.CONFLICT, f"Item of type {type(item)} cannot be deleted due to an Integrity Constraint") + abort(http_codes.CONFLICT, message=f"Kunde inte ta bort objektet") except: db.session.rollback() - abort( - codes.INTERNAL_SERVER_ERROR, - f"Item of type {type(item)} could not be deleted", - ) + abort(http_codes.INTERNAL_SERVER_ERROR, message=f"Kunde inte ta bort objektet") def whitelist_to_blacklist(filters): diff --git a/server/app/database/controller/edit.py b/server/app/database/controller/edit.py index 9f54df3e088b9c13f942f898453ab54741df521c..a5e35352163b04729851b53f62ed839dacdb93f7 100644 --- a/server/app/database/controller/edit.py +++ b/server/app/database/controller/edit.py @@ -2,10 +2,11 @@ This file contains functionality to get data from the database. """ -import app.core.http_codes as codes +from app.apis import http_codes from app.core import db -from app.core.parsers import sentinel -from flask_restx.errors import abort +from flask_smorest import abort + +# from flask_restx.errors import abort from sqlalchemy import exc @@ -29,12 +30,12 @@ def default(item, **kwargs): for key, value in kwargs.items(): if not hasattr(item, key): raise AttributeError(f"Item of type {type(item)} has no attribute '{key}'") - if value is not sentinel: - setattr(item, key, value) + setattr(item, key, value) try: db.session.commit() except exc.IntegrityError: - abort(codes.CONFLICT, f"Item of type {type(item)} cannot be edited due to an Integrity Constraint") + db.session.rollback() + abort(http_codes.CONFLICT, f"Kunde inte utföra ändringen") db.session.refresh(item) return item diff --git a/server/app/database/controller/get.py b/server/app/database/controller/get.py index 3c982eec5afcd12b621bbe5b8e4abd5d4aa9522d..bcaece403f8436f93bec5c9335c56a9efb763e3d 100644 --- a/server/app/database/controller/get.py +++ b/server/app/database/controller/get.py @@ -3,8 +3,8 @@ This file contains functionality to get data from the database. """ import app.database.controller as dbc +from app.apis import http_codes from app.core import db -from app.core import http_codes as codes from app.database.models import ( Code, Competition, @@ -63,8 +63,7 @@ def user_exists(email): def user_by_email(email): """ Gets the user object associated with the provided email. """ - - return User.query.filter(User.email == email).first_api(error_code=codes.UNAUTHORIZED) + return User.query.filter(User.email == email).first_api() ### Slides ### @@ -297,7 +296,7 @@ def component_list(competition_id, slide_id): ### Competitions ### -def competition(competition_id): +def competition(competition_id, required=True): """ Get Competition and all it's sub-entities. """ join_component = joinedload(Competition.slides).subqueryload(Slide.components) @@ -310,5 +309,5 @@ def competition(competition_id): .options(join_alternatives) .options(join_question_alternative_answer) .options(join_question_score) - .first() + .first_api(required, "Tävlingen kunde inte hittas") ) diff --git a/server/app/database/controller/search.py b/server/app/database/controller/search.py index 4d112a5f4b9d339b48d0add3e1382e81bc700c80..1be280e4230f43d5cdbf95456bc1b849c8a23398 100644 --- a/server/app/database/controller/search.py +++ b/server/app/database/controller/search.py @@ -5,27 +5,24 @@ This file contains functionality to find data to the database. from app.database.models import Competition, Media, Question, Slide, User -def image(filename, page=0, page_size=15, order=1, order_by=None): +def image(pagination_parameters=None, filename=None, order=1, order_by=None): """ Finds and returns an image from the file name. """ query = Media.query.filter(Media.type_id == 1) if filename: query = query.filter(Media.filename.like(f"%{filename}%")) - return query.pagination(page, page_size, None, None) + return query.paginate_api(pagination_parameters) def user( + pagination_parameters=None, email=None, name=None, city_id=None, role_id=None, - page=0, - page_size=15, - order=1, - order_by=None, ): - """ Finds and returns a user from the provided parameters. """ + """ Finds and returns any number of users from the provided parameters. """ query = User.query if name: @@ -37,21 +34,14 @@ def user( if role_id: query = query.filter(User.role_id == role_id) - order_column = User.id # Default order_by - if order_by: - order_column = getattr(User.__table__.c, order_by) - - return query.pagination(page, page_size, order_column, order) + return query.paginate_api(pagination_parameters) def competition( + pagination_parameters=None, name=None, year=None, city_id=None, - page=0, - page_size=15, - order=1, - order_by=None, ): """ Finds and returns a competition from the provided parameters. """ @@ -63,21 +53,15 @@ def competition( if city_id: query = query.filter(Competition.city_id == city_id) - order_column = Competition.year # Default order_by - if order_by: - order_column = getattr(Competition.columns, order_by) - - return query.pagination(page, page_size, order_column, order) + return query.paginate_api(pagination_parameters) def slide( + pagination_paramters=None, slide_order=None, title=None, body=None, competition_id=None, - page=0, - page_size=15, - order=1, order_by=None, ): """ Finds and returns a slide from the provided parameters. """ @@ -92,11 +76,7 @@ def slide( if competition_id: query = query.filter(Slide.competition_id == competition_id) - order_column = Slide.id # Default order_by - if order_by: - order_column = getattr(Slide.__table__.c, order_by) - - return query.pagination(page, page_size, order_column, order) + return query.paginate_api(pagination_paramters) def questions( diff --git a/server/app/database/controller/utils.py b/server/app/database/controller/utils.py index f71b61e765037fa04f1bc10e0e0ae6c541d9d8d0..acfddc136a3e4c58135dede0839885ee1c93974c 100644 --- a/server/app/database/controller/utils.py +++ b/server/app/database/controller/utils.py @@ -2,11 +2,13 @@ This file contains some miscellaneous functionality. """ -import app.core.http_codes as codes +from app.apis import http_codes from app.core import db from app.core.codes import generate_code_string from app.database.models import Code -from flask_restx import abort +from flask_smorest import abort + +# from flask_restx import abort def move_order(orders, order_key, from_order, to_order): @@ -106,7 +108,7 @@ def refresh(item): try: db.session.refresh(item) except Exception as e: - abort(codes.INTERNAL_SERVER_ERROR, f"Refresh failed!\n{str(e)}") + abort(http_codes.INTERNAL_SERVER_ERROR, f"Refresh failed!\n{str(e)}") return item @@ -118,7 +120,7 @@ def commit(): db.session.commit() except Exception as e: db.session.rollback() - abort(codes.INTERNAL_SERVER_ERROR, f"Commit failed!\n{str(e)}") + abort(http_codes.INTERNAL_SERVER_ERROR, f"Commit failed!\n{str(e)}") def commit_and_refresh(item): diff --git a/server/configmodule.py b/server/configmodule.py index e5df0ea77fbc7a042054f76488ea3b7be0fe3243..390b4ea74978671b6c04c3822d02c9440ca3357c 100644 --- a/server/configmodule.py +++ b/server/configmodule.py @@ -46,6 +46,14 @@ class Config: USER_LOGIN_LOCKED_ATTEMPTS = 12 USER_LOGIN_LOCKED_EXPIRES = timedelta(hours=3) + # Configure flask_smorest + API_TITLE = "Teknikåttan" + API_VERSION = "v1.0" + OPENAPI_VERSION = "3.0.3" + OPENAPI_URL_PREFIX = "/" + OPENAPI_SWAGGER_UI_PATH = "/" + OPENAPI_SWAGGER_UI_URL = "https://cdn.jsdelivr.net/npm/swagger-ui-dist/" + class DevelopmentConfig(Config, LiteDevDbConfig): DEBUG = True diff --git a/server/requirements.txt b/server/requirements.txt index 472b12cdf8229664d6d4bc679e6a85cb941341d7..bcb325f5f3a16a5ee0e655435923c38d8926b807 100644 Binary files a/server/requirements.txt and b/server/requirements.txt differ diff --git a/server/tests/__init__.py b/server/tests/__init__.py index c5b8f20d24cbfabe4d6c87f66a3e9b893a51d23d..2bf699418f230976e4ac28827e6b7d2f08030ec3 100644 --- a/server/tests/__init__.py +++ b/server/tests/__init__.py @@ -1,6 +1,8 @@ import pytest from app import create_app, db +DISABLE_TESTS = False + @pytest.fixture def app(): diff --git a/server/tests/test_app.py b/server/tests/test_app.py index 48fbaef0ed13ebda3bdc721424beb23b07b225bc..1bf7663df8f0aac1ad005b58c892e6cf8dc28cd0 100644 --- a/server/tests/test_app.py +++ b/server/tests/test_app.py @@ -4,46 +4,47 @@ This file tests the api function calls. import time -import app.core.http_codes as codes import pytest +from app.apis import http_codes from app.core import sockets -from tests import app, client, db +from tests import DISABLE_TESTS, app, client, db from tests.test_helpers import add_default_values, change_order_test, delete, get, post, put -# @pytest.mark.skip(reason="Takes long time") +@pytest.mark.skipif(DISABLE_TESTS, reason="Only run when DISABLE_TESTS is False") def test_locked_api(client): add_default_values() # Login in with default user but wrong password until blocked for i in range(4): response, body = post(client, "/api/auth/login", {"email": "test@test.se", "password": "password1"}) - assert response.status_code == codes.UNAUTHORIZED + assert response.status_code == http_codes.UNAUTHORIZED # Login with right password, user should be locked response, body = post(client, "/api/auth/login", {"email": "test@test.se", "password": "password"}) - assert response.status_code == codes.UNAUTHORIZED + assert response.status_code == http_codes.UNAUTHORIZED # Sleep for 4 secounds time.sleep(4) # Check so the user is no longer locked response, body = post(client, "/api/auth/login", {"email": "test@test.se", "password": "password"}) - assert response.status_code == codes.OK + assert response.status_code == http_codes.OK +@pytest.mark.skipif(DISABLE_TESTS, reason="Only run when DISABLE_TESTS is False") def test_misc_api(client): add_default_values() # Login in with default user response, body = post(client, "/api/auth/login", {"email": "test@test.se", "password": "password"}) - assert response.status_code == codes.OK + assert response.status_code == http_codes.OK headers = {"Authorization": "Bearer " + body["access_token"]} # Get types response, body = get(client, "/api/misc/types", headers=headers) - assert response.status_code == codes.OK + assert response.status_code == http_codes.OK assert len(body["media_types"]) >= 2 assert len(body["question_types"]) >= 3 assert len(body["component_types"]) >= 2 @@ -51,143 +52,146 @@ def test_misc_api(client): ## Get misc response, body = get(client, "/api/misc/roles", headers=headers) - assert response.status_code == codes.OK - assert body["count"] >= 2 + assert response.status_code == http_codes.OK + assert len(body) == 2 # There are currently two roles response, body = get(client, "/api/misc/cities", headers=headers) - assert response.status_code == codes.OK - assert body["count"] >= 2 - assert body["items"][0]["name"] == "Linköping" and body["items"][1]["name"] == "Testköping" + assert response.status_code == http_codes.OK + assert len(body) >= 2 + assert body[0]["name"] == "Linköping" and body[1]["name"] == "Testköping" ## Cities response, body = post(client, "/api/misc/cities", {"name": "Göteborg"}, headers=headers) - assert response.status_code == codes.OK - assert body["count"] >= 2 and body["items"][2]["name"] == "Göteborg" + assert response.status_code == http_codes.OK + assert len(body) >= 2 and body[2]["name"] == "Göteborg" # Rename city response, body = put(client, "/api/misc/cities/3", {"name": "Gbg"}, headers=headers) - assert response.status_code == codes.OK - assert body["count"] >= 2 and body["items"][2]["name"] == "Gbg" + assert response.status_code == http_codes.OK + assert len(body) >= 2 and body[2]["name"] == "Gbg" # Delete city # First checks current cities response, body = get(client, "/api/misc/cities", headers=headers) - assert response.status_code == codes.OK - assert body["count"] >= 3 - assert body["items"][0]["name"] == "Linköping" - assert body["items"][1]["name"] == "Testköping" - assert body["items"][2]["name"] == "Gbg" + assert response.status_code == http_codes.OK + assert len(body) >= 3 + assert body[0]["name"] == "Linköping" + assert body[1]["name"] == "Testköping" + assert body[2]["name"] == "Gbg" # Deletes city response, body = delete(client, "/api/misc/cities/3", headers=headers) - assert response.status_code == codes.OK - assert body["count"] >= 2 - assert body["items"][0]["name"] == "Linköping" and body["items"][1]["name"] == "Testköping" + assert response.status_code == http_codes.OK + assert len(body) >= 2 + assert body[0]["name"] == "Linköping" and body[1]["name"] == "Testköping" +@pytest.mark.skipif(DISABLE_TESTS, reason="Only run when DISABLE_TESTS is False") def test_competition_api(client): add_default_values() # Login in with default user response, body = post(client, "/api/auth/login", {"email": "test@test.se", "password": "password"}) - assert response.status_code == codes.OK + assert response.status_code == http_codes.OK headers = {"Authorization": "Bearer " + body["access_token"]} # Create competition data = {"name": "c1", "year": 2020, "city_id": 1} response, body = post(client, "/api/competitions", data, headers=headers) - assert response.status_code == codes.OK + assert response.status_code == http_codes.OK assert body["name"] == "c1" competition_id = body["id"] # Get competition response, body = get(client, f"/api/competitions/{competition_id}", headers=headers) - assert response.status_code == codes.OK + assert response.status_code == http_codes.OK assert body["name"] == "c1" response, body = post(client, f"/api/competitions/{competition_id}/slides", headers=headers) - assert response.status_code == codes.OK + assert response.status_code == http_codes.OK response, body = get(client, f"/api/competitions/{competition_id}/slides", headers=headers) - assert response.status_code == codes.OK - assert len(body["items"]) == 2 + assert response.status_code == http_codes.OK + assert len(body) == 2 """ response, body = put(client, f"/api/competitions/{competition_id}/slides/{2}/order", {"order": 1}, headers=headers) - assert response.status_code == codes.OK + assert response.status_code == http_codes.OK """ response, body = post(client, f"/api/competitions/{competition_id}/teams", {"name": "t1"}, headers=headers) - assert response.status_code == codes.OK + assert response.status_code == http_codes.OK response, body = get(client, f"/api/competitions/{competition_id}/teams", headers=headers) - assert response.status_code == codes.OK - assert len(body["items"]) == 1 - assert body["items"][0]["name"] == "t1" + assert response.status_code == http_codes.OK + assert len(body) == 1 + assert body[0]["name"] == "t1" response, body = delete(client, f"/api/competitions/{competition_id}", headers=headers) - assert response.status_code == codes.OK + assert response.status_code == http_codes.NO_CONTENT # Get competition competition_id = 2 response, body = get(client, f"/api/competitions/{competition_id}", headers=headers) - assert response.status_code == codes.OK + assert response.status_code == http_codes.OK # Copies competition for _ in range(3): response, _ = post(client, f"/api/competitions/{competition_id}/copy", headers=headers) - assert response.status_code == codes.OK + assert response.status_code == http_codes.OK +@pytest.mark.skipif(DISABLE_TESTS, reason="Only run when DISABLE_TESTS is False") def test_auth_and_user_api(client): add_default_values() # Login in with default user response, body = post(client, "/api/auth/login", {"email": "test@test.se", "password": "password"}) - assert response.status_code == codes.OK + assert response.status_code == http_codes.OK headers = {"Authorization": "Bearer " + body["access_token"]} # Login in with default user but wrong password response, body = post(client, "/api/auth/login", {"email": "test@test.se", "password": "password1"}) - assert response.status_code == codes.UNAUTHORIZED + assert response.status_code == http_codes.UNAUTHORIZED # Create user register_data = {"email": "test1@test.se", "password": "abc123", "role_id": 2, "city_id": 1} - response, body = post(client, "/api/auth/signup", register_data, headers) - assert response.status_code == codes.OK + response, body = post(client, "/api/users", register_data, headers) + assert response.status_code == http_codes.OK assert body["id"] == 2 assert "password" not in body assert "_password" not in body # Try to create user with same email register_data = {"email": "test1@test.se", "password": "354213", "role_id": 1, "city_id": 1} - response, body = post(client, "/api/auth/signup", register_data, headers) - assert response.status_code == codes.BAD_REQUEST + response, body = post(client, "/api/users", register_data, headers) + assert response.status_code == http_codes.CONFLICT - # Try loggin with wrong PASSWORD + # Try login with wrong PASSWORD response, body = post(client, "/api/auth/login", {"email": "test1@test.se", "password": "abc1234"}) - assert response.status_code == codes.UNAUTHORIZED + assert response.status_code == http_codes.UNAUTHORIZED - # Try loggin with wrong Email + # Try login with wrong Email response, body = post(client, "/api/auth/login", {"email": "testx@test.se", "password": "abc1234"}) - assert response.status_code == codes.UNAUTHORIZED + assert response.status_code == http_codes.NOT_FOUND - # Try loggin with right PASSWORD + # Login with right PASSWORD response, body = post(client, "/api/auth/login", {"email": "test1@test.se", "password": "abc123"}) - assert response.status_code == codes.OK - # refresh_token = body["refresh_token"] + assert response.status_code == http_codes.OK + headers = {"Authorization": "Bearer " + body["access_token"]} # Get the current user response, body = get(client, "/api/users", headers=headers) - assert response.status_code == codes.OK + assert response.status_code == http_codes.OK assert body["email"] == "test1@test.se" # Edit current user name response, body = put(client, "/api/users", {"name": "carl carlsson", "city_id": 2, "role_id": 1}, headers=headers) - assert response.status_code == codes.OK + assert response.status_code == http_codes.OK assert body["name"] == "Carl Carlsson" - assert body["city_id"] == 2 and body["role_id"] == 1 + assert body["city_id"] == 2 + assert body["role_id"] == 1 # Find other user response, body = get( @@ -196,14 +200,14 @@ def test_auth_and_user_api(client): query_string={"name": "Carl Carlsson"}, headers=headers, ) - assert response.status_code == codes.OK - assert body["count"] == 1 + assert response.status_code == http_codes.OK + assert len(body) == 1 # Get user from ID - searched_user = body["items"][0] + searched_user = body[0] user_id = searched_user["id"] response, body = get(client, f"/api/users/{user_id}", headers=headers) - assert response.status_code == codes.OK + assert response.status_code == http_codes.OK assert searched_user["name"] == body["name"] assert searched_user["email"] == body["email"] assert searched_user["role_id"] == body["role_id"] @@ -212,46 +216,46 @@ def test_auth_and_user_api(client): # Login as admin response, body = post(client, "/api/auth/login", {"email": "test@test.se", "password": "password"}) - assert response.status_code == codes.OK + assert response.status_code == http_codes.OK headers = {"Authorization": "Bearer " + body["access_token"]} # Edit user from ID response, body = put(client, f"/api/users/{user_id}", {"email": "carl@carlsson.test"}, headers=headers) - assert response.status_code == codes.OK + assert response.status_code == http_codes.OK # assert body["email"] == "carl@carlsson.test" # Edit user from ID but add the same email as other user response, body = put(client, f"/api/users/{user_id}", {"email": "test@test.se"}, headers=headers) - assert response.status_code == codes.BAD_REQUEST + assert response.status_code == http_codes.CONFLICT # Delete other user - response, body = delete(client, f"/api/auth/delete/{user_id}", headers=headers) - assert response.status_code == codes.OK + response, body = delete(client, f"/api/users/{user_id}", headers=headers) + assert response.status_code == http_codes.NO_CONTENT # Try to delete other user again - response, body = delete(client, f"/api/auth/delete/{user_id}", headers=headers) - assert response.status_code == codes.NOT_FOUND + response, body = delete(client, f"/api/users/{user_id}", headers=headers) + assert response.status_code == http_codes.NOT_FOUND # Logout and try to access current user response, body = post(client, f"/api/auth/logout", headers=headers) - assert response.status_code == codes.OK + assert response.status_code == http_codes.NO_CONTENT # TODO: Check if current users jwt (jti) is in blacklist after logging out response, body = get(client, "/api/users", headers=headers) - assert response.status_code == codes.UNAUTHORIZED + assert response.status_code == http_codes.UNAUTHORIZED # Login in again with default user # response, body = post(client, "/api/auth/login", {"email": "test1@test.se", "password": "abc123"}) - # assert response.status_code == codes.OK + # assert response.status_code == http_codes.OK # headers = {"Authorization": "Bearer " + body["access_token"]} # # TODO: Add test for refresh api for current user # # response, body = post(client, "/api/auth/refresh", headers={**headers, "refresh_token": refresh_token}) - # # assert response.status_code == codes.OK + # # assert response.status_code == http_codes.OK # # Find current user # response, body = get(client, "/api/users", headers=headers) - # assert response.status_code == codes.OK + # assert response.status_code == http_codes.OK # assert body["email"] == "test1@test.se" # assert body["city_id"] == 2 # assert body["role_id"] == 1 @@ -259,44 +263,45 @@ def test_auth_and_user_api(client): # # Delete current user # user_id = body["id"] # response, body = delete(client, f"/api/auth/delete/{user_id}", headers=headers) - # assert response.status_code == codes.OK + # assert response.status_code == http_codes.OK # TODO: Check that user was blacklisted # Look for current users jwt in blacklist # Blacklist.query.filter(Blacklist.jti == ) +@pytest.mark.skipif(DISABLE_TESTS, reason="Only run when DISABLE_TESTS is False") def test_slide_api(client): add_default_values() # Login in with default user response, body = post(client, "/api/auth/login", {"email": "test@test.se", "password": "password"}) - assert response.status_code == codes.OK + assert response.status_code == http_codes.OK headers = {"Authorization": "Bearer " + body["access_token"]} # Get slides from empty competition CID = 1 response, body = get(client, f"/api/competitions/{CID}/slides", headers=headers) - assert response.status_code == codes.OK - assert body["count"] == 1 + assert response.status_code == http_codes.OK + assert len(body) == 1 # Get slides CID = 2 response, body = get(client, f"/api/competitions/{CID}/slides", headers=headers) - assert response.status_code == codes.OK - assert body["count"] == 3 + assert response.status_code == http_codes.OK + assert len(body) == 3 # Add slide response, body = post(client, f"/api/competitions/{CID}/slides", headers=headers) - assert response.status_code == codes.OK + assert response.status_code == http_codes.OK response, body = get(client, f"/api/competitions/{CID}/slides", headers=headers) - assert body["count"] == 4 + assert len(body) == 4 # Get slide slide_id = 2 response, item_slide = get(client, f"/api/competitions/{CID}/slides/{slide_id}", headers=headers) - assert response.status_code == codes.OK + assert response.status_code == http_codes.OK # Edit slide title = "Ny titel" @@ -308,12 +313,10 @@ def test_slide_api(client): response, item_slide = put( client, f"/api/competitions/{CID}/slides/{slide_id}", - # TODO: Implement so these commented lines can be edited - # {"order": order, "title": title, "body": body, "timer": timer}, {"title": title, "timer": timer}, headers=headers, ) - assert response.status_code == codes.OK + assert response.status_code == http_codes.OK # assert item_slide["order"] == order assert item_slide["title"] == title # assert item_slide["body"] == body @@ -321,22 +324,22 @@ def test_slide_api(client): # Delete slide response, _ = delete(client, f"/api/competitions/{CID}/slides/{slide_id}", headers=headers) - assert response.status_code == codes.NO_CONTENT + assert response.status_code == http_codes.NO_CONTENT # Checks that there are fewer slides response, body = get(client, f"/api/competitions/{CID}/slides", headers=headers) - assert response.status_code == codes.OK - assert body["count"] == 3 + assert response.status_code == http_codes.OK + assert len(body) == 3 # Tries to delete slide again, which will fail response, _ = delete(client, f"/api/competitions/{CID}/slides/{slide_id}", headers=headers) - assert response.status_code != codes.OK + assert response.status_code != http_codes.OK # Get all slides response, body = get(client, f"/api/competitions/{CID}/slides", headers=headers) - assert response.status_code == codes.OK - assert body["count"] == 3 - assert body["items"][0]["id"] == 3 - assert body["items"][0]["order"] == 0 + assert response.status_code == http_codes.OK + assert len(body) == 3 + assert body[0]["id"] == 3 + assert body[0]["order"] == 0 slide_id = 3 """ @@ -344,7 +347,7 @@ def test_slide_api(client): response, _ = put( client, f"/api/competitions/{CID}/slides/{slide_id}/order", {"order": 0}, headers=headers ) - assert response.status_code == codes.OK + assert response.status_code == http_codes.OK # Changes the order change_order_test(client, CID, slide_id, slide_id + 1, headers) @@ -352,7 +355,7 @@ def test_slide_api(client): # Copies slide for _ in range(10): response, _ = post(client, f"/api/competitions/{CID}/slides/{slide_id}/copy", headers=headers) - assert response.status_code == codes.OK + assert response.status_code == http_codes.OK """ # Get a specific component @@ -360,7 +363,7 @@ def test_slide_api(client): SID = 3 COMID = 2 response, c1 = get(client, f"/api/competitions/{CID}/slides/{SID}/components/{COMID}", headers=headers) - assert response.status_code == codes.OK + assert response.status_code == http_codes.OK # Copy the component to another view view_type_id = 3 @@ -368,7 +371,7 @@ def test_slide_api(client): client, f"/api/competitions/{CID}/slides/{SID}/components/{COMID}/copy/{view_type_id}", headers=headers ) # Check that the components metch - assert response.status_code == codes.OK + assert response.status_code == http_codes.OK assert c1 != c2 assert c1["x"] == c2["x"] assert c1["y"] == c2["y"] @@ -385,27 +388,16 @@ def test_slide_api(client): assert c2["view_type_id"] == 3 +@pytest.mark.skipif(DISABLE_TESTS, reason="Only run when DISABLE_TESTS is False") def test_question_api(client): add_default_values() # Login in with default user response, body = post(client, "/api/auth/login", {"email": "test@test.se", "password": "password"}) - assert response.status_code == codes.OK + assert response.status_code == http_codes.OK headers = {"Authorization": "Bearer " + body["access_token"]} - # Get questions from empty competition - CID = 1 # TODO: Fix api-calls so that the ones not using CID don't require one - slide_order = 1 - response, body = get(client, f"/api/competitions/{CID}/questions", headers=headers) - assert response.status_code == codes.OK - assert body["count"] == 0 - - # Get questions from another competition that should have some questions - CID = 3 - response, body = get(client, f"/api/competitions/{CID}/questions", headers=headers) num_questions = 3 - assert response.status_code == codes.OK - assert body["count"] == num_questions # Add question name = "Nytt namn" @@ -413,36 +405,33 @@ def test_question_api(client): slide_order = 6 response, item_question = post( client, - f"/api/competitions/{CID}/slides/{slide_order}/questions", + f"/api/competitions/{3}/slides/{slide_order}/questions", {"name": name, "type_id": type_id}, headers=headers, ) - assert response.status_code == codes.OK + assert response.status_code == http_codes.OK assert item_question["name"] == name assert item_question["type_id"] == type_id num_questions += 1 - # Checks number of questions - response, body = get(client, f"/api/competitions/{CID}/questions", headers=headers) - assert response.status_code == codes.OK - assert body["count"] == num_questions """ # Delete question response, _ = delete(client, f"/api/competitions/{CID}/slides/{slide_order}/questions/{QID}", headers=headers) num_questions -= 1 - assert response.status_code == codes.NO_CONTENT + assert response.status_code == http_codes.NO_CONTENT # Checks that there are fewer questions response, body = get(client, f"/api/competitions/{CID}/questions", headers=headers) - assert response.status_code == codes.OK + assert response.status_code == http_codes.OK assert body["count"] == num_questions # Tries to delete question again response, _ = delete(client, f"/api/competitions/{CID}/slides/{NEW_slide_order}/questions/{QID}", headers=headers) - assert response.status_code == codes.NOT_FOUND + assert response.status_code == http_codes.NOT_FOUND """ +@pytest.mark.skipif(DISABLE_TESTS, reason="Only run when DISABLE_TESTS is False") def test_authorization(client): add_default_values() @@ -451,8 +440,8 @@ def test_authorization(client): #### TEAM #### # Login in with team code - response, body = post(client, "/api/auth/login/code", {"code": "111111"}) - assert response.status_code == codes.OK + response, body = post(client, "/api/auth/code", {"code": "111111"}) + assert response.status_code == http_codes.OK headers = {"Authorization": "Bearer " + body["access_token"]} competition_id = body["competition_id"] @@ -460,56 +449,48 @@ def test_authorization(client): # Get competition team is in response, body = get(client, f"/api/competitions/{competition_id}", headers=headers) - assert response.status_code == codes.OK + assert response.status_code == http_codes.OK # Try to delete competition team is in response, body = delete(client, f"/api/competitions/{competition_id}", headers=headers) - assert response.status_code == codes.UNAUTHORIZED + assert response.status_code == http_codes.UNAUTHORIZED # Try to get a different competition response, body = get(client, f"/api/competitions/{competition_id+1}", headers=headers) - assert response.status_code == codes.UNAUTHORIZED + assert response.status_code == http_codes.UNAUTHORIZED # Get own answers - response, body = get( - client, f"/api/competitions/{competition_id}/teams/{team_id}/answers/question_alternatives", headers=headers - ) - assert response.status_code == codes.OK + response, body = get(client, f"/api/competitions/{competition_id}/teams/{team_id}/answers", headers=headers) + assert response.status_code == http_codes.OK # Try to get another teams answers - response, body = get( - client, f"/api/competitions/{competition_id}/teams/{team_id+1}/answers/question_alternatives", headers=headers - ) - assert response.status_code == codes.UNAUTHORIZED + response, body = get(client, f"/api/competitions/{competition_id}/teams/{team_id+1}/answers", headers=headers) + assert response.status_code == http_codes.UNAUTHORIZED #### JUDGE #### # Login in with judge code - response, body = post(client, "/api/auth/login/code", {"code": "222222"}) - assert response.status_code == codes.OK + response, body = post(client, "/api/auth/code", {"code": "222222"}) + assert response.status_code == http_codes.OK headers = {"Authorization": "Bearer " + body["access_token"]} competition_id = body["competition_id"] # Get competition judge is in response, body = get(client, f"/api/competitions/{competition_id}", headers=headers) - assert response.status_code == codes.OK + assert response.status_code == http_codes.OK # Try to delete competition judge is in response, body = delete(client, f"/api/competitions/{competition_id}", headers=headers) - assert response.status_code == codes.UNAUTHORIZED + assert response.status_code == http_codes.UNAUTHORIZED # Try to get a different competition response, body = get(client, f"/api/competitions/{competition_id+1}", headers=headers) - assert response.status_code == codes.UNAUTHORIZED + assert response.status_code == http_codes.UNAUTHORIZED # Get team answers - response, body = get( - client, f"/api/competitions/{competition_id}/teams/{team_id}/answers/question_alternatives", headers=headers - ) - assert response.status_code == codes.OK + response, body = get(client, f"/api/competitions/{competition_id}/teams/{team_id}/answers", headers=headers) + assert response.status_code == http_codes.OK # Also get antoher teams answers - response, body = get( - client, f"/api/competitions/{competition_id}/teams/{team_id+1}/answers/question_alternatives", headers=headers - ) - assert response.status_code == codes.OK + response, body = get(client, f"/api/competitions/{competition_id}/teams/{team_id+1}/answers", headers=headers) + assert response.status_code == http_codes.OK diff --git a/server/tests/test_db.py b/server/tests/test_db.py index 82a75a3f6f8916efefed99de346e22b91e1b0f54..6d000820135b88f21c5ab85071ce9be1f3bd9e4a 100644 --- a/server/tests/test_db.py +++ b/server/tests/test_db.py @@ -3,12 +3,14 @@ This file tests the database controller functions. """ import app.database.controller as dbc +import pytest from app.database.models import City, Code, Competition, Media, MediaType, Role, Slide, User -from tests import app, client, db +from tests import DISABLE_TESTS, app, client, db from tests.test_helpers import add_default_values, assert_all_slide_orders, assert_should_fail, assert_slide_order +@pytest.mark.skipif(DISABLE_TESTS, reason="Only run when DISABLE_TESTS is False") def test_default_values(client): add_default_values() @@ -40,6 +42,7 @@ def test_default_values(client): assert_should_fail(assert_all_slide_orders) +@pytest.mark.skipif(DISABLE_TESTS, reason="Only run when DISABLE_TESTS is enabled") def test_user(client): add_default_values() item_user = User.query.filter_by(email="test@test.se").first() @@ -57,6 +60,7 @@ def test_user(client): assert len(item_city.users) == 1 and item_city.users[0].id == item_user.id +@pytest.mark.skipif(DISABLE_TESTS, reason="Only run when DISABLE_TESTS is enabled") def test_media(client): add_default_values() item_user = User.query.filter_by(email="test@test.se").first() @@ -75,17 +79,18 @@ def test_media(client): assert item_media.upload_by.email == "test@test.se" +@pytest.mark.skipif(DISABLE_TESTS, reason="Only run when DISABLE_TESTS is enabled") def test_copy(client): add_default_values() # Fetches a competition - list_item_competitions, _ = dbc.search.competition(name="Tävling 1") + list_item_competitions = dbc.search.competition(name="Tävling 1") item_competition_original = list_item_competitions[0] # Fetches the first slide in that competition num_slides = 3 - item_slides, total = dbc.search.slide(competition_id=item_competition_original.id) - assert total == num_slides + item_slides = dbc.search.slide(competition_id=item_competition_original.id) + assert len(item_slides) == num_slides item_slide_original = item_slides[1] dbc.delete.slide(item_slides[0]) @@ -192,14 +197,15 @@ def check_slides_copy(item_slide_original, item_slide_copy, num_slides, order): assert a2.question_id == q2.id # Checks that the copy put the slide in the database - item_slides, total = dbc.search.slide( + item_slides = dbc.search.slide( competition_id=item_slide_copy.competition_id, # page_size=num_slides + 1, # Use this total > 15 ) - assert total == num_slides + assert len(item_slides) == num_slides assert item_slide_copy == item_slides[order] +@pytest.mark.skipif(DISABLE_TESTS, reason="Only run when DISABLE_TESTS is enabled") def test_move_slides(client): add_default_values() diff --git a/server/tests/test_helpers.py b/server/tests/test_helpers.py index f5abbfcbe1991bf379e5c9020b99088122d6d6e9..fdcef127df249587a45ad8647c62731497cdb8ff 100644 --- a/server/tests/test_helpers.py +++ b/server/tests/test_helpers.py @@ -1,7 +1,7 @@ import json -import app.core.http_codes as codes import app.database.controller as dbc +from app.apis import http_codes from app.core import db from app.database.models import City, Code, Role, Slide @@ -144,15 +144,15 @@ def assert_object_values(obj, values): # Changes order of slides def change_order_test(client, cid, slide_id, new_slide_id, h): response, new_order_body = get(client, f"/api/competitions/{cid}/slides/{new_slide_id}", headers=h) - assert response.status_code == codes.OK + assert response.status_code == http_codes.OK response, order_body = get(client, f"/api/competitions/{cid}/slides/{slide_id}", headers=h) - assert response.status_code == codes.OK + assert response.status_code == http_codes.OK new_order = new_order_body["order"] # Changes order response, _ = put(client, f"/api/competitions/{cid}/slides/{slide_id}/order", {"order": new_order}, headers=h) - assert response.status_code == codes.OK + assert response.status_code == http_codes.OK def assert_slide_order(item_comp, correct_order):