diff --git a/.gitignore b/.gitignore index d5cf16df23f3f817ba7274e27d16f4dafde28ac4..3c5d2e1e3fe18f48ed48dcce1bbc83845d06ce39 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,4 @@ htmlcov .pytest_cache /.idea .vs/ -/server/app/static/ \ No newline at end of file +/server/app/static/ diff --git a/client/package-lock.json b/client/package-lock.json index 7e73859f4e13b504ee814cde74ed293dd53e33ed..667b75859fe71467505839e687c7e478b8a52834 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -2370,6 +2370,11 @@ "@types/node": "*" } }, + "@types/component-emitter": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@types/component-emitter/-/component-emitter-1.2.10.tgz", + "integrity": "sha512-bsjleuRKWmGqajMerkzox19aGbscQX5rmmvvXl3wlIp5gMG1HgkiwPxsN5p070fBDKTNSPgojVbuY1+HWMbFhg==" + }, "@types/enzyme": { "version": "3.10.8", "resolved": "https://registry.npmjs.org/@types/enzyme/-/enzyme-3.10.8.tgz", @@ -2587,6 +2592,11 @@ "@types/node": "*" } }, + "@types/socket.io-client": { + "version": "1.4.36", + "resolved": "https://registry.npmjs.org/@types/socket.io-client/-/socket.io-client-1.4.36.tgz", + "integrity": "sha512-ZJWjtFBeBy1kRSYpVbeGYTElf6BqPQUkXDlHHD4k/42byCN5Rh027f4yARHCink9sKAkbtGZXEAmR0ZCnc2/Ag==" + }, "@types/source-list-map": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz", @@ -3970,6 +3980,11 @@ "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==" }, + "backo2": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", + "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=" + }, "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", @@ -4025,6 +4040,11 @@ } } }, + "base64-arraybuffer": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz", + "integrity": "sha1-mBjHngWbE1X5fgQooBfIOOkLqBI=" + }, "base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -6061,6 +6081,30 @@ "once": "^1.4.0" } }, + "engine.io-client": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-5.0.1.tgz", + "integrity": "sha512-CQtGN3YwfvbxVwpPugcsHe5rHT4KgT49CEcQppNtu9N7WxbPN0MAG27lGaem7bvtCFtGNLSL+GEqXsFSz36jTg==", + "requires": { + "base64-arraybuffer": "0.1.4", + "component-emitter": "~1.3.0", + "debug": "~4.3.1", + "engine.io-parser": "~4.0.1", + "has-cors": "1.1.0", + "parseqs": "0.0.6", + "parseuri": "0.0.6", + "ws": "~7.4.2", + "yeast": "0.1.2" + } + }, + "engine.io-parser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-4.0.2.tgz", + "integrity": "sha512-sHfEQv6nmtJrq6TKuIz5kyEKH/qSdK56H/A+7DnAuUPWosnIZAS2NHNcPLmyjtY3cGS/MqJdZbUjW97JU72iYg==", + "requires": { + "base64-arraybuffer": "0.1.4" + } + }, "enhanced-resolve": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.5.0.tgz", @@ -7983,6 +8027,11 @@ "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==", "dev": true }, + "has-cors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz", + "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=" + }, "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -11969,6 +12018,16 @@ } } }, + "parseqs": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.6.tgz", + "integrity": "sha512-jeAGzMDbfSHHA091hr0r31eYfTig+29g3GKKE/PPbEQ65X0lmMwlEoqmhzu0iztID5uJpZsFlUPDP8ThPL7M8w==" + }, + "parseuri": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.6.tgz", + "integrity": "sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow==" + }, "parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -15203,6 +15262,30 @@ } } }, + "socket.io-client": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.0.1.tgz", + "integrity": "sha512-6AkaEG5zrVuSVW294cH1chioag9i1OqnCYjKwTc3EBGXbnyb98Lw7yMa40ifLjFj3y6fsFKsd0llbUZUCRf3Qw==", + "requires": { + "@types/component-emitter": "^1.2.10", + "backo2": "~1.0.2", + "component-emitter": "~1.3.0", + "debug": "~4.3.1", + "engine.io-client": "~5.0.0", + "parseuri": "0.0.6", + "socket.io-parser": "~4.0.4" + } + }, + "socket.io-parser": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.0.4.tgz", + "integrity": "sha512-t+b0SS+IxG7Rxzda2EVvyBZbvFPBCjJoyHuE0P//7OAsN23GItzDRdWa6ALxZI/8R5ygK7jAR6t028/z+7295g==", + "requires": { + "@types/component-emitter": "^1.2.10", + "component-emitter": "~1.3.0", + "debug": "~4.3.1" + } + }, "sockjs": { "version": "0.3.20", "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.20.tgz", @@ -18151,6 +18234,11 @@ } } }, + "yeast": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz", + "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=" + }, "yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/client/package.json b/client/package.json index 2d393d3d80ce5126841f980c77e830355be56960..7c354ece5569314b108c67617fecf9e0f9d637e0 100644 --- a/client/package.json +++ b/client/package.json @@ -18,6 +18,7 @@ "@types/node": "^12.19.16", "@types/react": "^17.0.1", "@types/react-dom": "^17.0.0", + "@types/socket.io-client": "^1.4.36", "axios": "^0.21.1", "formik": "^2.2.6", "jwt-decode": "^3.1.2", @@ -32,6 +33,7 @@ "redux-devtools-extension": "^2.13.8", "redux-mock-store": "^1.5.4", "redux-thunk": "^2.3.0", + "socket.io-client": "^4.0.1", "styled-components": "^5.2.1", "typescript": "^4.1.3", "web-vitals": "^1.1.0", diff --git a/client/src/actions/cities.ts b/client/src/actions/cities.ts index a54475c7cbd69b06db3476cd7d6c08ebd7739070..9226aee57b2f477d85d9d5fa781174d097773afc 100644 --- a/client/src/actions/cities.ts +++ b/client/src/actions/cities.ts @@ -11,11 +11,11 @@ export const getCities = () => async (dispatch: AppDispatch) => { payload: res.data.items, }) dispatch({ - type: Types.SET_COMPETITIONS_TOTAL, + type: Types.SET_CITIES_COUNT, payload: res.data.total_count, }) dispatch({ - type: Types.SET_COMPETITIONS_COUNT, + type: Types.SET_CITIES_TOTAL, payload: res.data.count, }) }) diff --git a/client/src/actions/editor.ts b/client/src/actions/editor.ts index d5e6fede6e777927eca7702098788c6a14b95e4b..ac26dbdbba7e2a5c4564d7cf359ab189e6275783 100644 --- a/client/src/actions/editor.ts +++ b/client/src/actions/editor.ts @@ -8,162 +8,17 @@ export const getEditorCompetition = (id: string) => async (dispatch: AppDispatch .then((res) => { dispatch({ type: Types.SET_EDITOR_COMPETITION, - //res.data, - payload: { - name: 'Tävling 1 (Hårdkodad)', - id: 1, - year: 1337, - city_id: 1, - slides: [ - { - competition_id: 1, - id: 1, - order: 1, - timer: 10, - title: 'Sida 1', - questions: [ - { - id: 1, - slide_id: 1, - name: 'Fråga 1 namn', - title: 'Fråga 1 titel', - total_score: 5, - type_id: 3, - question_answers: [ - { - id: 1, - question_id: 1, - team_id: 1, - data: 'question answer data 1', - score: 1, - }, - { - id: 2, - question_id: 1, - team_id: 2, - data: 'question answer data 2', - score: 3, - }, - ], - alternatives: [ - { - id: 1, - text: '1', - value: true, - question_id: 1, - }, - { - id: 2, - text: '0', - value: false, - question_id: 1, - }, - ], - }, - ], - body: 'Slide body 1', - settings: 'Slide settings 1', - }, - - { - competition_id: 1, - id: 2, - order: 2, - timer: 15, - title: 'Sida 2', - questions: [ - { - id: 2, - slide_id: 2, - name: 'Fråga 2 namn', - title: 'Fråga 2 titel', - total_score: 6, - type_id: 3, - question_answers: [ - { - id: 3, - question_id: 2, - team_id: 1, - data: 'question answer data 1', - score: 1, - }, - { - id: 4, - question_id: 2, - team_id: 2, - data: 'question answer data 2', - score: 4, - }, - ], - alternatives: [ - { - id: 1, - text: '5', - value: true, - question_id: 2, - }, - { - id: 2, - text: 'abc', - value: false, - question_id: 2, - }, - ], - }, - ], - body: 'Slide body 2', - settings: 'Slide settings 2', - }, - ], - - teams: [ - { - id: 1, - name: 'Örkelljunga IK', - question_answers: [ - { - id: 1, - question_id: 1, - team_id: 1, - data: 'question answer data 1', - score: 1, - }, - { - id: 3, - question_id: 2, - team_id: 1, - data: 'question answer data 1', - score: 1, - }, - ], - competition_id: 1, - }, - { - id: 2, - name: 'Vadstena OK', - question_answers: [ - { - id: 2, - question_id: 1, - team_id: 2, - data: 'question answer data 2', - score: 3, - }, - { - id: 4, - question_id: 2, - team_id: 2, - data: 'question answer data 2', - score: 4, - }, - ], - competition_id: 1, - }, - ], - }, + payload: res.data, }) }) .catch((err) => { console.log(err) }) } + +export const setEditorSlideId = (id: number) => (dispatch: AppDispatch) => { + dispatch({ + type: Types.SET_EDITOR_SLIDE_ID, + payload: id, + }) +} diff --git a/client/src/actions/presentation.ts b/client/src/actions/presentation.ts index 6821aa2c035c099eb4b21b3799b47e50157dc9aa..d1fddd97abf9d6ee672f1f8c3002e1561a50ef2f 100644 --- a/client/src/actions/presentation.ts +++ b/client/src/actions/presentation.ts @@ -42,3 +42,25 @@ export const setCurrentSlidePrevious = () => (dispatch: AppDispatch) => { export const setCurrentSlideNext = () => (dispatch: AppDispatch) => { dispatch({ type: Types.SET_PRESENTATION_SLIDE_NEXT }) } + +export const setCurrentSlideByOrder = (order: number) => (dispatch: AppDispatch) => { + dispatch({ type: Types.SET_PRESENTATION_SLIDE_BY_ORDER, payload: order }) +} + +export const setPresentationCode = (code: string) => (dispatch: AppDispatch) => { + dispatch({ type: Types.SET_PRESENTATION_CODE, payload: code }) +} + +export const setPresentationTimer = (timer: Timer) => (dispatch: AppDispatch) => { + dispatch({ type: Types.SET_PRESENTATION_TIMER, payload: timer }) +} + +export const setPresentationTimerDecrement = () => (dispatch: AppDispatch) => { + dispatch({ + type: Types.SET_PRESENTATION_TIMER, + payload: { + enabled: store.getState().presentation.timer.enabled, + value: store.getState().presentation.timer.value - 1, + }, + }) +} diff --git a/client/src/actions/types.ts b/client/src/actions/types.ts index 7c9cce081ce01f9cd317ca921f0352b32c4b02ee..a417f6afc797d8a69bd81d2b259283e98ed364e3 100644 --- a/client/src/actions/types.ts +++ b/client/src/actions/types.ts @@ -16,11 +16,15 @@ export default { SET_COMPETITIONS_TOTAL: 'SET_COMPETITIONS_TOTAL', SET_COMPETITIONS_COUNT: 'SET_COMPETITIONS_COUNT', SET_EDITOR_COMPETITION: 'SET_EDITOR_COMPETITION', + SET_EDITOR_SLIDE_ID: 'SET_EDITOR_SLIDE_ID', SET_PRESENTATION_COMPETITION: 'SET_PRESENTATION_COMPETITION', SET_PRESENTATION_SLIDE: 'SET_PRESENTATION_SLIDE', SET_PRESENTATION_SLIDE_PREVIOUS: 'SET_PRESENTATION_SLIDE_PREVIOUS', SET_PRESENTATION_SLIDE_NEXT: 'SET_PRESENTATION_SLIDE_NEXT', + SET_PRESENTATION_SLIDE_BY_ORDER: 'SET_PRESENTATION_SLIDE_BY_ORDER', SET_PRESENTATION_TEAMS: 'SET_PRESENTATION_TEAMS', + SET_PRESENTATION_CODE: 'SET_PRESENTATION_CODE', + SET_PRESENTATION_TIMER: 'SET_PRESENTATION_TIMER', SET_CITIES: 'SET_CITIES', SET_CITIES_TOTAL: 'SET_CITIES_TOTAL', SET_CITIES_COUNT: 'SET_CITIES_COUNT', diff --git a/client/src/actions/user.test.ts b/client/src/actions/user.test.ts index 156b17183a76117f87ead4f049346d85e499fea0..6ca594fa6630b35018eab088f21576836ef478f4 100644 --- a/client/src/actions/user.test.ts +++ b/client/src/actions/user.test.ts @@ -39,6 +39,9 @@ it('dispatches correct actions when logging in user', async () => { }) it('dispatches correct action when logging out user', async () => { + ;(mockedAxios.post as jest.Mock).mockImplementation((path: string, params?: any) => { + return Promise.resolve({ data: {} }) + }) const store = mockStore({}) await logoutUser()(store.dispatch) expect(store.getActions()).toEqual([{ type: Types.SET_UNAUTHENTICATED }]) diff --git a/client/src/actions/user.ts b/client/src/actions/user.ts index 30374ccafb38fd3c1f6e9a72378104b2cf2611fd..e14ea4c046013922a5e573998170b8bc46c0bcdc 100644 --- a/client/src/actions/user.ts +++ b/client/src/actions/user.ts @@ -40,11 +40,13 @@ export const getUserData = () => async (dispatch: AppDispatch) => { }) } -export const logoutUser = () => (dispatch: AppDispatch) => { - localStorage.removeItem('token') - delete axios.defaults.headers.common['Authorization'] - dispatch({ - type: Types.SET_UNAUTHENTICATED, +export const logoutUser = () => async (dispatch: AppDispatch) => { + await axios.post('/auth/logout').then(() => { + localStorage.removeItem('token') + delete axios.defaults.headers.common['Authorization'] + dispatch({ + type: Types.SET_UNAUTHENTICATED, + }) + window.location.href = '/' //redirect to login page }) - window.location.href = '/' //redirect to login page } diff --git a/client/src/enum/ComponentTypes.ts b/client/src/enum/ComponentTypes.ts index c8d194c09ebf38dd98e6a49a7062345cd110620a..8acb2fd91da2eba39dd935ddce1f0a8af9a59198 100644 --- a/client/src/enum/ComponentTypes.ts +++ b/client/src/enum/ComponentTypes.ts @@ -1,5 +1,5 @@ export enum ComponentTypes { - Text, + Text = 1, Checkbox, Image, } diff --git a/client/src/interfaces/ApiModels.ts b/client/src/interfaces/ApiModels.ts index 0053fc1bf3070e346798079bc467eac167aba358..14d7bf5b79afc4bb4eaff8afcf3096827e35bca0 100644 --- a/client/src/interfaces/ApiModels.ts +++ b/client/src/interfaces/ApiModels.ts @@ -78,16 +78,20 @@ export interface Component { y: number w: number h: number - type: number + type_id: number } export interface ImageComponent extends Component { - media_id: number + data: { + media_id: number + } } export interface TextComponent extends Component { - text: string - font: string + data: { + text: string + font: string + } } export interface QuestionAlternativeComponent extends Component { diff --git a/client/src/interfaces/Timer.ts b/client/src/interfaces/Timer.ts new file mode 100644 index 0000000000000000000000000000000000000000..49d1909e15692e68bb8cdef32ddd9f59d6b69409 --- /dev/null +++ b/client/src/interfaces/Timer.ts @@ -0,0 +1,4 @@ +export interface Timer { + enabled: boolean + value: number +} diff --git a/client/src/pages/admin/dashboard/components/NumberOfCompetitions.tsx b/client/src/pages/admin/dashboard/components/NumberOfCompetitions.tsx index da5d015fd52d77de7e599e4f1bc2a62cee805483..87d8272ae035ce56d509dd99380bdb1ed17426a6 100644 --- a/client/src/pages/admin/dashboard/components/NumberOfCompetitions.tsx +++ b/client/src/pages/admin/dashboard/components/NumberOfCompetitions.tsx @@ -1,23 +1,23 @@ import { Box, Typography } from '@material-ui/core' import React, { useEffect } from 'react' -import { getSearchUsers } from '../../../../actions/searchUser' +import { getCompetitions } from '../../../../actions/competitions' import { useAppDispatch, useAppSelector } from '../../../../hooks' const NumberOfCompetitions: React.FC = () => { - const cities = useAppSelector((state) => state.cities.cities) + const competitions = useAppSelector((state) => state.competitions.competitions) const dispatch = useAppDispatch() const handleCount = () => { - if (cities.length >= 1000000) { - ;<div>{cities.length / 1000000 + 'M'}</div> - } else if (cities.length >= 1000) { - ;<div>{cities.length / 1000 + 'K'}</div> + if (competitions.length >= 1000000) { + ;<div>{competitions.length / 1000000 + 'M'}</div> + } else if (competitions.length >= 1000) { + ;<div>{competitions.length / 1000 + 'K'}</div> } - return <div>{cities.length}</div> + return <div>{competitions.length}</div> } useEffect(() => { - dispatch(getSearchUsers()) + dispatch(getCompetitions()) }, []) return ( <div> diff --git a/client/src/pages/admin/dashboard/components/NumberOfRegions.tsx b/client/src/pages/admin/dashboard/components/NumberOfRegions.tsx index a48b41a61f7a46089cedcd9e6a981b9ca625ec56..360b3663b0e45e9a24018f855ede3023c95bf39a 100644 --- a/client/src/pages/admin/dashboard/components/NumberOfRegions.tsx +++ b/client/src/pages/admin/dashboard/components/NumberOfRegions.tsx @@ -1,23 +1,23 @@ import { Box, Typography } from '@material-ui/core' import React, { useEffect } from 'react' -import { getSearchUsers } from '../../../../actions/searchUser' +import { getCities } from '../../../../actions/cities' import { useAppDispatch, useAppSelector } from '../../../../hooks' const NumberOfRegions: React.FC = () => { - const competitionTotal = useAppSelector((state) => state.competitions.total) + const regions = useAppSelector((state) => state.cities.total) const dispatch = useAppDispatch() const handleCount = () => { - if (competitionTotal >= 1000000) { - ;<div>{competitionTotal / 1000000 + 'M'}</div> - } else if (competitionTotal >= 1000) { - ;<div>{competitionTotal / 1000 + 'K'}</div> + if (regions >= 1000000) { + ;<div>{regions / 1000000 + 'M'}</div> + } else if (regions >= 1000) { + ;<div>{regions / 1000 + 'K'}</div> } - return <div>{competitionTotal}</div> + return <div>{regions}</div> } useEffect(() => { - dispatch(getSearchUsers()) + dispatch(getCities()) }, []) return ( <div> diff --git a/client/src/pages/admin/users/UserManager.tsx b/client/src/pages/admin/users/UserManager.tsx index df9cdec0b1618291b3efbe903aea69f94351aaad..20f5738604e48abe05d2ac280aff1ec56d896f6d 100644 --- a/client/src/pages/admin/users/UserManager.tsx +++ b/client/src/pages/admin/users/UserManager.tsx @@ -181,7 +181,7 @@ const UserManager: React.FC = (props: any) => { ))} </TableBody> </Table> - {(!users || users.length === 0) && <Typography>Inga tävlingar hittades med nuvarande filter</Typography>} + {(!users || users.length === 0) && <Typography>Inga användare hittades med nuvarande filter</Typography>} </TableContainer> <TablePagination component="div" diff --git a/client/src/pages/presentationEditor/PresentationEditorPage.test.tsx b/client/src/pages/presentationEditor/PresentationEditorPage.test.tsx index ebc96d73452aff066c29856c4756573d28435bb2..956d0b7212baf5a3eed212c86e9946550b66e72f 100644 --- a/client/src/pages/presentationEditor/PresentationEditorPage.test.tsx +++ b/client/src/pages/presentationEditor/PresentationEditorPage.test.tsx @@ -13,7 +13,7 @@ it('renders presentation editor', () => { id: 0, year: 0, city_id: 0, - slides: [], + slides: [{ id: 5 }], teams: [], }, } diff --git a/client/src/pages/presentationEditor/PresentationEditorPage.tsx b/client/src/pages/presentationEditor/PresentationEditorPage.tsx index ce09f41d7a1d3b58eb9e0df6051418b679721a23..d57b0b987f1f3547c89740b2a380c85733a1209e 100644 --- a/client/src/pages/presentationEditor/PresentationEditorPage.tsx +++ b/client/src/pages/presentationEditor/PresentationEditorPage.tsx @@ -1,4 +1,4 @@ -import { Divider, Typography } from '@material-ui/core' +import { CircularProgress, Divider, Typography } from '@material-ui/core' import AppBar from '@material-ui/core/AppBar' import CssBaseline from '@material-ui/core/CssBaseline' import Drawer from '@material-ui/core/Drawer' @@ -14,7 +14,14 @@ import { useAppDispatch, useAppSelector } from '../../hooks' import { Content } from '../views/styled' import SettingsPanel from './components/SettingsPanel' import SlideEditor from './components/SlideEditor' -import { PresentationEditorContainer, SlideListItem, ToolBarContainer, ViewButton, ViewButtonGroup } from './styled' +import { + CenteredSpinnerContainer, + PresentationEditorContainer, + SlideListItem, + ToolBarContainer, + ViewButton, + ViewButtonGroup, +} from './styled' function createSlide(name: string) { return { name } @@ -65,13 +72,20 @@ const PresentationEditorPage: React.FC = () => { const classes = useStyles() const { id }: CompetitionParams = useParams() const dispatch = useAppDispatch() + const activeSlideId = useAppSelector((state) => state.editor.activeSlideId) const competition = useAppSelector((state) => state.editor.competition) + const competitionLoading = useAppSelector((state) => state.editor.loading) // TODO: wait for dispatch to finish useEffect(() => { dispatch(getEditorCompetition(id)) dispatch(getCities()) dispatch(getTypes()) }, []) + + const setActiveSlideId = (id: number) => { + dispatch(setEditorSlideId(id)) + } + return ( <PresentationEditorContainer> <CssBaseline /> @@ -104,11 +118,18 @@ const PresentationEditorPage: React.FC = () => { <div className={classes.toolbar} /> <Divider /> <List> - {competition.slides.map((slide) => ( - <SlideListItem divider button key={slide.title}> - <ListItemText primary={slide.title} /> - </SlideListItem> - ))} + {competition.slides && + competition.slides.map((slide) => ( + <SlideListItem + divider + button + key={slide.id} + selected={slide.id === activeSlideId} + onClick={() => setActiveSlideId(slide.id)} + > + <ListItemText primary={slide.title} /> + </SlideListItem> + ))} </List> </Drawer> <div className={classes.toolbar} /> @@ -120,7 +141,13 @@ const PresentationEditorPage: React.FC = () => { }} anchor="right" > - <SettingsPanel></SettingsPanel> + {!competitionLoading ? ( + <SettingsPanel /> + ) : ( + <CenteredSpinnerContainer> + <CircularProgress /> + </CenteredSpinnerContainer> + )} </Drawer> <Content leftDrawerWidth={leftDrawerWidth} rightDrawerWidth={rightDrawerWidth}> diff --git a/client/src/pages/presentationEditor/components/CompetitionSettings.tsx b/client/src/pages/presentationEditor/components/CompetitionSettings.tsx index 2c61633f04a7aaba595e293a8f8ed9670d5c46c3..6791ce549c9bf24c1538b35a4ac7fbe8a73dfe56 100644 --- a/client/src/pages/presentationEditor/components/CompetitionSettings.tsx +++ b/client/src/pages/presentationEditor/components/CompetitionSettings.tsx @@ -64,7 +64,6 @@ const CompetitionSettings: React.FC = () => { const { id }: CompetitionParams = useParams() const dispatch = useAppDispatch() const competition = useAppSelector((state) => state.editor.competition) - const updateCompetitionName = async (event: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => { await axios .put(`/competitions/${id}`, { name: event.target.value }) @@ -134,14 +133,15 @@ const CompetitionSettings: React.FC = () => { <ListItem> <ListItemText className={classes.textCenter} primary="Lag" /> </ListItem> - {competition.teams.map((team) => ( - <div key={team.id}> - <ListItem divider button> - <ListItemText primary={team.name} /> - <CloseIcon onClick={() => handleClick(team.id)} /> - </ListItem> - </div> - ))} + {competition.teams && + competition.teams.map((team) => ( + <div key={team.id}> + <ListItem divider button> + <ListItemText primary={team.name} /> + <CloseIcon onClick={() => handleClick(team.id)} /> + </ListItem> + </div> + ))} <ListItem className={classes.center} button> <Button>Lägg till lag</Button> </ListItem> diff --git a/client/src/pages/presentationEditor/components/SettingsPanel.tsx b/client/src/pages/presentationEditor/components/SettingsPanel.tsx index ac3c45f106305d3f09e84c3c133433f582c8ed9f..2b2147282c4afafe69548e515813aaff216f2c26 100644 --- a/client/src/pages/presentationEditor/components/SettingsPanel.tsx +++ b/client/src/pages/presentationEditor/components/SettingsPanel.tsx @@ -3,7 +3,7 @@ import AppBar from '@material-ui/core/AppBar' import React from 'react' import CompetitionSettings from './CompetitionSettings' import SlideSettings from './SlideSettings' -import { SettingsTab } from './styled' +import { SettingsContainer, SettingsTab, ToolbarPadding } from './styled' interface TabPanelProps { activeTab: number @@ -20,15 +20,16 @@ function TabContent(props: TabPanelProps) { const SettingsPanel: React.FC = () => { const [activeTab, setActiveTab] = React.useState(0) return ( - <div> - <AppBar position="static"> + <SettingsContainer> + <AppBar position="static" style={{ position: 'absolute' }}> <Tabs value={activeTab} onChange={(event, val) => setActiveTab(val)} aria-label="simple tabs example"> <SettingsTab label="Tävling" /> <SettingsTab label="Sida" /> </Tabs> </AppBar> + <ToolbarPadding /> <TabContent activeTab={activeTab} /> - </div> + </SettingsContainer> ) } diff --git a/client/src/pages/presentationEditor/components/SlideEditor.tsx b/client/src/pages/presentationEditor/components/SlideEditor.tsx index 22a73ef3164f0f8609f847563ceb7b014a59c181..a71b9262727741ea7fd29e703374c913f0d8dbb3 100644 --- a/client/src/pages/presentationEditor/components/SlideEditor.tsx +++ b/client/src/pages/presentationEditor/components/SlideEditor.tsx @@ -1,37 +1,36 @@ import React from 'react' import { ComponentTypes } from '../../../enum/ComponentTypes' +import { useAppSelector } from '../../../hooks' +import { ImageComponent, TextComponent } from '../../../interfaces/ApiModels' import CheckboxComponent from './CheckboxComponent' import ImageComponentDisplay from './ImageComponentDisplay' -import { SlideEditorContainer } from './styled' +import { SlideEditorContainer, SlideEditorContainerRatio, SlideEditorPaper } from './styled' import TextComponentDisplay from './TextComponentDisplay' const SlideEditor: React.FC = () => { - // const components = useAppSelector(state => state.editor.slide.components) // get the current RichSlide - const components: any[] = [ - { id: 0, x: 15, y: 150, w: 200, h: 300, type: ComponentTypes.Checkbox }, - { id: 1, x: 15, y: 250, w: 200, h: 300, type: ComponentTypes.Checkbox }, - { id: 2, x: 15, y: 350, w: 200, h: 300, type: ComponentTypes.Checkbox }, - { id: 3, x: 300, y: 500, w: 100, h: 300, type: ComponentTypes.Text, text: 'text component', font: 'arial' }, - { id: 4, x: 250, y: 100, w: 200, h: 300, type: ComponentTypes.Image }, - { id: 5, x: 350, y: 100, w: 200, h: 300, type: ComponentTypes.Image }, - ] + const components = useAppSelector( + (state) => + state.editor.competition.slides.find((slide) => slide && slide.id === state.editor.activeSlideId)?.components + ) return ( <SlideEditorContainer> - {components.map((component) => { - switch (component.type) { - case ComponentTypes.Checkbox: - return <CheckboxComponent key={component.id} component={component} /> - break - case ComponentTypes.Text: - return <TextComponentDisplay key={component.id} component={component} /> - break - case ComponentTypes.Image: - return <ImageComponentDisplay key={component.id} component={component} /> - break - default: - break - } - })} + <SlideEditorContainerRatio> + <SlideEditorPaper> + {components && + components.map((component) => { + switch (component.type_id) { + case ComponentTypes.Checkbox: + return <CheckboxComponent key={component.id} component={component} /> + case ComponentTypes.Text: + return <TextComponentDisplay key={component.id} component={component as TextComponent} /> + case ComponentTypes.Image: + return <ImageComponentDisplay key={component.id} component={component as ImageComponent} /> + default: + break + } + })} + </SlideEditorPaper> + </SlideEditorContainerRatio> </SlideEditorContainer> ) } diff --git a/client/src/pages/presentationEditor/components/SlideSettings.tsx b/client/src/pages/presentationEditor/components/SlideSettings.tsx index cd611060bf16fd2e3e7d6b959cb1e0610a2fa931..c61d478ebeb37398a6b443226e29aacb032fbcdd 100644 --- a/client/src/pages/presentationEditor/components/SlideSettings.tsx +++ b/client/src/pages/presentationEditor/components/SlideSettings.tsx @@ -20,6 +20,7 @@ import React, { useState } from 'react' import { useParams } from 'react-router-dom' import { getEditorCompetition } from '../../../actions/editor' import { useAppDispatch, useAppSelector } from '../../../hooks' +import { TextComponent } from '../../../interfaces/ApiModels' import { HiddenInput } from './styled' const useStyles = makeStyles((theme: Theme) => @@ -93,14 +94,12 @@ const SlideSettings: React.FC = () => { .catch(console.log) } - const textList = [ - { id: 'text1', name: 'Text 1' }, - { id: 'text2', name: 'Text 2' }, - ] - const handleCloseTextClick = (id: string) => { - setTexts(texts.filter((item) => item.id !== id)) //Will not be done like this when api is used - } - const [texts, setTexts] = useState(textList) + const texts = useAppSelector( + (state) => + state.editor.competition.slides + .find((slide) => slide.id === state.editor.activeSlideId) + ?.components.filter((component) => component.type_id === 1) as TextComponent[] + ) const pictureList = [ { id: 'picture1', name: 'Picture1.jpeg' }, @@ -151,7 +150,7 @@ const SlideSettings: React.FC = () => { const handleAddText = async () => { console.log('Add text component') // TODO: post the new text] - setTexts([...texts, { id: 'newText', name: 'New Text' }]) + // setTexts([...texts, { id: 'newText', name: 'New Text' }]) } const GreenCheckbox = withStyles({ @@ -206,21 +205,24 @@ const SlideSettings: React.FC = () => { secondary="(Fyll i rutan höger om textfältet för att markera korrekt svar)" /> </ListItem> - {(currentSlide?.questions[0].question_alternatives || []).map((alt) => ( - <div key={alt.id}> - <ListItem divider> - <TextField - className={classes.textInput} - id="outlined-basic" - label={`Svar ${alt.id}`} - value={alt.text} - variant="outlined" - /> - <GreenCheckbox checked={alt.value} onChange={updateAlternativeValue} /> - <CloseIcon className={classes.clickableIcon} onClick={() => handleCloseAnswerClick(alt.id)} /> - </ListItem> - </div> - ))} + {currentSlide && + currentSlide.questions[0] && + currentSlide.questions[0].question_alternatives && + currentSlide.questions[0].question_alternatives.map((alt) => ( + <div key={alt.id}> + <ListItem divider> + <TextField + className={classes.textInput} + id="outlined-basic" + label={`Svar ${alt.id}`} + value={alt.text} + variant="outlined" + /> + <GreenCheckbox checked={alt.value} onChange={updateAlternativeValue} /> + <CloseIcon className={classes.clickableIcon} onClick={() => handleCloseAnswerClick(alt.id)} /> + </ListItem> + </div> + ))} <ListItem className={classes.center} button> <Button>Lägg till svarsalternativ</Button> </ListItem> @@ -230,15 +232,16 @@ const SlideSettings: React.FC = () => { <ListItem divider> <ListItemText className={classes.textCenter} primary="Text" /> </ListItem> - {texts.map((text) => ( - <div key={text.id}> - <ListItem divider> - <TextField className={classes.textInput} label={text.name} variant="outlined" /> - <MoreHorizOutlinedIcon className={classes.clickableIcon} /> - <CloseIcon className={classes.clickableIcon} onClick={() => handleCloseTextClick(text.id)} /> - </ListItem> - </div> - ))} + {texts && + texts.map((text) => ( + <div key={text.id}> + <ListItem divider> + <TextField className={classes.textInput} label={text.data.text} variant="outlined" /> + <MoreHorizOutlinedIcon className={classes.clickableIcon} /> + <CloseIcon className={classes.clickableIcon} /> + </ListItem> + </div> + ))} <ListItem className={classes.center} button onClick={handleAddText}> <Button>Lägg till text</Button> </ListItem> diff --git a/client/src/pages/presentationEditor/components/TextComponentDisplay.test.tsx b/client/src/pages/presentationEditor/components/TextComponentDisplay.test.tsx index 8f61ee36f8bb86e9e422b135c76448c31d60a6d4..c448987894fedee6731d3610fe850871e608388d 100644 --- a/client/src/pages/presentationEditor/components/TextComponentDisplay.test.tsx +++ b/client/src/pages/presentationEditor/components/TextComponentDisplay.test.tsx @@ -1,12 +1,18 @@ import { Editor } from '@tinymce/tinymce-react' import { mount } from 'enzyme' import React from 'react' +import { Provider } from 'react-redux' +import store from '../../../store' import TextComponentDisplay from './TextComponentDisplay' it('renders text component display', () => { const testText = 'TEST' const container = mount( - <TextComponentDisplay component={{ id: 0, x: 0, y: 0, w: 0, h: 0, text: testText, type: 2, font: '123123' }} /> + <Provider store={store}> + <TextComponentDisplay + component={{ id: 0, x: 0, y: 0, w: 0, h: 0, data: { text: testText, font: '123123' }, type_id: 2 }} + /> + </Provider> ) expect(container.find(Editor).prop('initialValue')).toBe(testText) }) diff --git a/client/src/pages/presentationEditor/components/TextComponentDisplay.tsx b/client/src/pages/presentationEditor/components/TextComponentDisplay.tsx index 30426b9da323f72f53fc2f99e1959d1914be3dc6..4fb34650c3e63159d04ae745ac92f780493cdb52 100644 --- a/client/src/pages/presentationEditor/components/TextComponentDisplay.tsx +++ b/client/src/pages/presentationEditor/components/TextComponentDisplay.tsx @@ -1,6 +1,8 @@ import { Editor } from '@tinymce/tinymce-react' +import axios from 'axios' import React, { useState } from 'react' import { Rnd } from 'react-rnd' +import { useAppSelector } from '../../../hooks' import { TextComponent } from '../../../interfaces/ApiModels' import { Position, Size } from '../../../interfaces/Components' @@ -11,9 +13,26 @@ type ImageComponentProps = { const TextComponentDisplay = ({ component }: ImageComponentProps) => { const [currentPos, setCurrentPos] = useState<Position>({ x: component.x, y: component.y }) const [currentSize, setCurrentSize] = useState<Size>({ w: component.w, h: component.h }) + const competitionId = useAppSelector((state) => state.editor.competition.id) + const slideId = useAppSelector((state) => state.editor.activeSlideId) + if (component.id === 1) console.log(component) const handleEditorChange = (e: any) => { console.log('Content was updated:', e.target.getContent()) - //TODO: axios.post + axios.put(`/competitions/${competitionId}/slides/${slideId}/components/${component.id}`, { + data: { ...component.data, text: e.target.getContent() }, + }) + } + const handleUpdatePos = (pos: Position) => { + axios.put(`/competitions/${competitionId}/slides/${slideId}/components/${component.id}`, { + x: pos.x, + y: pos.y, + }) + } + const handleUpdateSize = () => { + axios.put(`/competitions/${competitionId}/slides/${slideId}/components/${component.id}`, { + w: currentSize.w, + h: currentSize.h, + }) } return ( <Rnd @@ -22,6 +41,7 @@ const TextComponentDisplay = ({ component }: ImageComponentProps) => { bounds="parent" onDragStop={(e, d) => { setCurrentPos({ x: d.x, y: d.y }) + handleUpdatePos(d) }} size={{ width: currentSize.w, height: currentSize.h }} position={{ x: currentPos.x, y: currentPos.y }} @@ -32,15 +52,12 @@ const TextComponentDisplay = ({ component }: ImageComponentProps) => { }) setCurrentPos(position) }} - onResizeStop={() => { - console.log('skickar till server') - }} + onResizeStop={handleUpdateSize} > <div style={{ height: '100%', width: '100%' }}> <Editor - initialValue={component.text} + initialValue={component.data.text} init={{ - body_class: 'mceBlackBody', height: '100%', menubar: false, plugins: [ diff --git a/client/src/pages/presentationEditor/components/styled.tsx b/client/src/pages/presentationEditor/components/styled.tsx index c4584f87468ceea090ecabca872e8000d3b58988..32df0ac9a0b57c86ef382ddfd2d172315d6fd1b2 100644 --- a/client/src/pages/presentationEditor/components/styled.tsx +++ b/client/src/pages/presentationEditor/components/styled.tsx @@ -7,13 +7,40 @@ export const SettingsTab = styled(Tab)` ` export const SlideEditorContainer = styled.div` + height: 100%; display: flex; align-items: center; justify-content: center; + background-color: rgba(0, 0, 0, 0.08); +` + +export const SlideEditorContainerRatio = styled.div` + padding-top: 56.25%; + width: 100%; + height: 0; + overflow: hidden; + padding-top: 56.25%; + position: relative; +` + +export const SlideEditorPaper = styled.div` + position: absolute; + top: 0; + left: 0; width: 100%; height: 100%; + background: white; ` export const HiddenInput = styled.input` display: none; ` + +export const SettingsContainer = styled.div` + overflow-x: hidden; +` + +export const ToolbarPadding = styled.div` + height: 0; + padding-top: 55px; +` diff --git a/client/src/pages/presentationEditor/styled.tsx b/client/src/pages/presentationEditor/styled.tsx index 462acf2e64259c13ab74830c7d2484fe2881267e..a2c6e6f79cafb4ade856c8ae8cf1bca23e89eb1a 100644 --- a/client/src/pages/presentationEditor/styled.tsx +++ b/client/src/pages/presentationEditor/styled.tsx @@ -23,3 +23,10 @@ export const SlideListItem = styled(ListItem)` export const PresentationEditorContainer = styled.div` height: 100%; ` + +export const CenteredSpinnerContainer = styled.div` + display: flex; + align-items: center; + justify-content: center; + height: 100%; +` diff --git a/client/src/pages/views/JudgeViewPage.tsx b/client/src/pages/views/JudgeViewPage.tsx index 293a1f08ca5a7bff56b980844233bcb879edd215..12e866a085b506df3d22eb38151dcca4acd222e6 100644 --- a/client/src/pages/views/JudgeViewPage.tsx +++ b/client/src/pages/views/JudgeViewPage.tsx @@ -2,7 +2,12 @@ import { Divider, List, ListItemText } from '@material-ui/core' import { createStyles, makeStyles, Theme } from '@material-ui/core/styles' import React, { useEffect, useState } from 'react' import { useParams } from 'react-router-dom' -import { getPresentationCompetition, getPresentationTeams, setCurrentSlide } from '../../actions/presentation' +import { + getPresentationCompetition, + getPresentationTeams, + setCurrentSlide, + setPresentationCode, +} from '../../actions/presentation' import { useAppDispatch, useAppSelector } from '../../hooks' import { ViewParams } from '../../interfaces/ViewParams' import { SlideListItem } from '../presentationEditor/styled' @@ -41,6 +46,7 @@ const JudgeViewPage: React.FC = () => { useEffect(() => { dispatch(getPresentationCompetition(id)) dispatch(getPresentationTeams(id)) + dispatch(setPresentationCode(code)) }, []) const teams = useAppSelector((state) => state.presentation.teams) const slides = useAppSelector((state) => state.presentation.competition.slides) diff --git a/client/src/pages/views/PresenterViewPage.tsx b/client/src/pages/views/PresenterViewPage.tsx index 131bde22f5b23e70e9ac071563395f353b7b9e1f..22a672bac56b5cae4825128cd21216835f425d57 100644 --- a/client/src/pages/views/PresenterViewPage.tsx +++ b/client/src/pages/views/PresenterViewPage.tsx @@ -2,15 +2,12 @@ import { List, ListItem, Popover } from '@material-ui/core' import ChevronRightIcon from '@material-ui/icons/ChevronRight' import React, { useEffect } from 'react' import { useHistory, useParams } from 'react-router-dom' -import { - getPresentationCompetition, - getPresentationTeams, - setCurrentSlideNext, - setCurrentSlidePrevious, -} from '../../actions/presentation' +import { getPresentationCompetition, getPresentationTeams, setPresentationCode } from '../../actions/presentation' import { useAppDispatch, useAppSelector } from '../../hooks' import { ViewParams } from '../../interfaces/ViewParams' import SlideDisplay from './components/SlideDisplay' +import SocketTest from './components/SocketTest' +import Timer from './components/Timer' import { PresenterButton, PresenterContainer, PresenterFooter, PresenterHeader } from './styled' const PresenterViewPage: React.FC = () => { @@ -22,6 +19,7 @@ const PresenterViewPage: React.FC = () => { useEffect(() => { dispatch(getPresentationCompetition(id)) dispatch(getPresentationTeams(id)) + dispatch(setPresentationCode(code)) }, []) const handleOpenPopover = (event: React.MouseEvent<HTMLButtonElement>) => { setAnchorEl(event.currentTarget) @@ -29,6 +27,15 @@ const PresenterViewPage: React.FC = () => { const handleClose = () => { setAnchorEl(null) } + const handleNextSlidePressed = () => { + // dispatch(setCurrentSlideNext()) + // syncSlide() + } + const handlePreviousSlidePressed = () => { + // dispatch(setCurrentSlidePrevious()) + // syncSlide() + } + return ( <PresenterContainer> <PresenterHeader> @@ -41,10 +48,12 @@ const PresenterViewPage: React.FC = () => { </PresenterHeader> <SlideDisplay /> <PresenterFooter> - <PresenterButton onClick={() => dispatch(setCurrentSlidePrevious())} variant="contained"> + <PresenterButton onClick={handlePreviousSlidePressed} variant="contained"> <ChevronRightIcon fontSize="large" /> </PresenterButton> - <PresenterButton onClick={() => dispatch(setCurrentSlideNext())} variant="contained"> + <SocketTest></SocketTest> + <Timer></Timer> + <PresenterButton onClick={handleNextSlidePressed} variant="contained"> <ChevronRightIcon fontSize="large" /> </PresenterButton> </PresenterFooter> diff --git a/client/src/pages/views/components/SocketTest.tsx b/client/src/pages/views/components/SocketTest.tsx new file mode 100644 index 0000000000000000000000000000000000000000..d99f5b2aa740d9a2690b3aadeae6e1050b92d089 --- /dev/null +++ b/client/src/pages/views/components/SocketTest.tsx @@ -0,0 +1,61 @@ +import React, { useEffect } from 'react' +import { connect } from 'react-redux' +import { useAppDispatch } from '../../../hooks' +import { + socketEndPresentation, + socketJoinPresentation, + socketSetSlideNext, + socketSetSlidePrev, + socketStartPresentation, + socketStartTimer, + socket_connect, +} from '../../../sockets' + +const mapStateToProps = (state: any) => { + return { + slide_order: state.presentation.slide.order, + } +} + +const mapDispatchToProps = (dispatch: any) => { + return { + // tickTimer: () => dispatch(tickTimer(1)), + } +} + +const SocketTest: React.FC = (props: any) => { + const dispatch = useAppDispatch() + + useEffect(() => { + socket_connect() + // dispatch(getPresentationCompetition('1')) // TODO: Use ID of item_code gotten from auth/login/<code> api call + // dispatch(getPresentationTeams('1')) // TODO: Use ID of item_code gotten from auth/login/<code> api call + }, []) + + return ( + <> + <button onClick={socketStartPresentation}>Start presentation</button> + <button onClick={socketJoinPresentation}>Join presentation</button> + <button onClick={socketEndPresentation}>End presentation</button> + <button onClick={socketSetSlidePrev}>Prev slide</button> + <button onClick={socketSetSlideNext}>Next slide</button> + <button onClick={socketStartTimer}>Start timer</button> + <div>Current slide: {props.slide_order}</div> + {/* <div>Timer: {props.timer.value}</div> + <div>Enabled: {props.timer.enabled.toString()}</div> + <button onClick={syncTimer}>Sync</button> + <button onClick={() => dispatch(setTimer(5))}>5 Sec</button> + <button + onClick={() => { + dispatch(setTimer(5)) + dispatch(setTimerEnabled(true)) + syncTimer() + }} + > + Sync and 5 sec + </button> */} + </> + ) +} + +export default connect(mapStateToProps, mapDispatchToProps)(SocketTest) diff --git a/client/src/pages/views/components/Timer.tsx b/client/src/pages/views/components/Timer.tsx new file mode 100644 index 0000000000000000000000000000000000000000..b4401a696c42d25cc8296f22059a780feb00e8e9 --- /dev/null +++ b/client/src/pages/views/components/Timer.tsx @@ -0,0 +1,47 @@ +import React, { useEffect } from 'react' +import { connect } from 'react-redux' +import { setPresentationTimer, setPresentationTimerDecrement } from '../../../actions/presentation' +import { useAppDispatch } from '../../../hooks' +import store from '../../../store' + +const mapStateToProps = (state: any) => { + return { + timer: state.presentation.timer, + timer_start_value: state.presentation.slide.timer, + } +} + +const mapDispatchToProps = (dispatch: any) => { + return { + // tickTimer: () => dispatch(tickTimer(1)), + } +} + +let timerIntervalId: NodeJS.Timeout + +const Timer: React.FC = (props: any) => { + const dispatch = useAppDispatch() + + useEffect(() => { + dispatch(setPresentationTimer({ enabled: false, value: store.getState().presentation.slide.timer })) + }, [props.timer_start_value]) + + useEffect(() => { + if (props.timer.enabled) { + timerIntervalId = setInterval(() => { + dispatch(setPresentationTimerDecrement()) + }, 1000) + } else { + clearInterval(timerIntervalId) + } + }, [props.timer.enabled]) + + return ( + <> + <div>Timer: {props.timer.value}</div> + <div>Enabled: {props.timer.enabled.toString()}</div> + </> + ) +} + +export default connect(mapStateToProps, mapDispatchToProps)(Timer) diff --git a/client/src/reducers/editorReducer.ts b/client/src/reducers/editorReducer.ts index 606f5d69ba7cb53e6ce9d9468b632045d8965b9d..81b10c32d206d8870a7cbee38c632991330b7149 100644 --- a/client/src/reducers/editorReducer.ts +++ b/client/src/reducers/editorReducer.ts @@ -4,6 +4,8 @@ import { RichCompetition } from '../interfaces/ApiRichModels' interface EditorState { competition: RichCompetition + activeSlideId: number + loading: boolean } const initialState: EditorState = { @@ -15,14 +17,22 @@ const initialState: EditorState = { slides: [], teams: [], }, + activeSlideId: 0, + loading: true, } export default function (state = initialState, action: AnyAction) { switch (action.type) { case Types.SET_EDITOR_COMPETITION: return { - ...state, competition: action.payload as RichCompetition, + activeSlideId: action.payload.slides[0].id as number, + loading: false, + } + case Types.SET_EDITOR_SLIDE_ID: + return { + ...state, + activeSlideId: action.payload as number, } default: return state diff --git a/client/src/reducers/presentationReducer.test.ts b/client/src/reducers/presentationReducer.test.ts index 15bcd3376110f565db2ed73bc4e2d848c8063f61..a155eab5c915e82c64f16c0093dd2fc296b10d7a 100644 --- a/client/src/reducers/presentationReducer.test.ts +++ b/client/src/reducers/presentationReducer.test.ts @@ -20,6 +20,11 @@ const initialState = { title: '', }, teams: [], + code: '', + timer: { + enabled: false, + value: 0, + }, } it('should return the initial state', () => { @@ -46,7 +51,9 @@ it('should handle SET_PRESENTATION_COMPETITION', () => { ).toEqual({ competition: testCompetition, slide: testCompetition.slides[0], - teams: [], + teams: initialState.teams, + code: initialState.code, + timer: initialState.timer, }) }) @@ -70,6 +77,8 @@ it('should handle SET_PRESENTATION_TEAMS', () => { competition: initialState.competition, slide: initialState.slide, teams: testTeams, + code: initialState.code, + timer: initialState.timer, }) }) @@ -92,6 +101,8 @@ it('should handle SET_PRESENTATION_SLIDE', () => { competition: initialState.competition, slide: testSlide, teams: initialState.teams, + code: initialState.code, + timer: initialState.timer, }) }) @@ -107,6 +118,8 @@ describe('should handle SET_PRESENTATION_SLIDE_PREVIOUS', () => { }, teams: initialState.teams, slide: { competition_id: 0, order: 1 } as Slide, + code: initialState.code, + timer: initialState.timer, } expect( presentationReducer(testPresentationState, { @@ -116,6 +129,8 @@ describe('should handle SET_PRESENTATION_SLIDE_PREVIOUS', () => { competition: testPresentationState.competition, slide: testPresentationState.competition.slides[0], teams: testPresentationState.teams, + code: initialState.code, + timer: initialState.timer, }) }) it('by not changing slide if there is no previous one', () => { @@ -129,6 +144,8 @@ describe('should handle SET_PRESENTATION_SLIDE_PREVIOUS', () => { }, teams: initialState.teams, slide: { competition_id: 0, order: 0 } as Slide, + code: initialState.code, + timer: initialState.timer, } expect( presentationReducer(testPresentationState, { @@ -138,6 +155,8 @@ describe('should handle SET_PRESENTATION_SLIDE_PREVIOUS', () => { competition: testPresentationState.competition, slide: testPresentationState.competition.slides[0], teams: testPresentationState.teams, + code: initialState.code, + timer: initialState.timer, }) }) }) diff --git a/client/src/reducers/presentationReducer.ts b/client/src/reducers/presentationReducer.ts index 936f874026aa48bed6c926f660d33a416ef760da..f706ba54f1e8bc7794fd1945d6551ba7c8547cfa 100644 --- a/client/src/reducers/presentationReducer.ts +++ b/client/src/reducers/presentationReducer.ts @@ -7,6 +7,8 @@ interface PresentationState { competition: RichCompetition slide: Slide teams: Team[] + code: string + timer: Timer } const initialState: PresentationState = { @@ -26,6 +28,11 @@ const initialState: PresentationState = { title: '', }, teams: [], + code: '', + timer: { + enabled: false, + value: 0, + }, } export default function (state = initialState, action: AnyAction) { @@ -41,6 +48,11 @@ export default function (state = initialState, action: AnyAction) { ...state, teams: action.payload as Team[], } + case Types.SET_PRESENTATION_CODE: + return { + ...state, + code: action.payload, + } case Types.SET_PRESENTATION_SLIDE: return { ...state, @@ -62,6 +74,21 @@ export default function (state = initialState, action: AnyAction) { } } return state + case Types.SET_PRESENTATION_SLIDE_BY_ORDER: + if (0 <= action.payload && action.payload < state.competition.slides.length) + return { + ...state, + slide: state.competition.slides[action.payload], + } + return state + case Types.SET_PRESENTATION_TIMER: + if (action.payload.value == 0) { + action.payload.enabled = false + } + return { + ...state, + timer: action.payload, + } default: return state } diff --git a/client/src/sockets.ts b/client/src/sockets.ts new file mode 100644 index 0000000000000000000000000000000000000000..874bfc46ad3a003013d32c31585704aceb527f84 --- /dev/null +++ b/client/src/sockets.ts @@ -0,0 +1,80 @@ +import io from 'socket.io-client' +import { setCurrentSlideByOrder, setPresentationTimer } from './actions/presentation' +import { Timer } from './interfaces/Timer' +import store from './store' + +interface SetSlideInterface { + slide_order: number +} + +interface TimerInterface { + value: number + enabled: boolean +} + +interface SetTimerInterface { + timer: TimerInterface +} + +let socket: SocketIOClient.Socket + +export const socket_connect = () => { + if (!socket) { + socket = io('localhost:5000') + + socket.on('set_slide', (data: SetSlideInterface) => { + setCurrentSlideByOrder(data.slide_order)(store.dispatch) + }) + + socket.on('set_timer', (data: SetTimerInterface) => { + setPresentationTimer(data.timer)(store.dispatch) + }) + + socket.on('end_presentation', () => { + socket.disconnect() + }) + } +} + +export const socketStartPresentation = () => { + socket.emit('start_presentation', { competition_id: store.getState().presentation.competition.id }) +} + +export const socketJoinPresentation = () => { + socket.emit('join_presentation', { code: 'OEM1V4' }) // TODO: Send code gotten from auth/login/<code> api call +} + +export const socketEndPresentation = () => { + socket.emit('end_presentation', { competition_id: store.getState().presentation.competition.id }) +} + +export const socketSetSlideNext = () => { + socketSetSlide(store.getState().presentation.slide.order + 1) // TODO: Check that this slide exists +} + +export const socketSetSlidePrev = () => { + socketSetSlide(store.getState().presentation.slide.order - 1) // TODO: Check that this slide exists +} + +export const socketSetSlide = (slide_order: number) => { + if (slide_order < 0 || store.getState().presentation.competition.slides.length <= slide_order) { + console.log('CANT CHANGE TO NON EXISTENT SLIDE') + return + } + + socket.emit('set_slide', { + competition_id: store.getState().presentation.competition.id, + slide_order: slide_order, + }) +} + +export const socketSetTimer = (timer: Timer) => { + socket.emit('set_timer', { + competition_id: store.getState().presentation.competition.id, + timer: timer, + }) +} + +export const socketStartTimer = () => { + socketSetTimer({ enabled: true, value: store.getState().presentation.timer.value }) +} diff --git a/client/src/utils/checkAuthentication.test.ts b/client/src/utils/checkAuthentication.test.ts index 901f331d6cc32c68e2a87c32338e879719c736af..6d12e1fc77cab5e2575af85e0b91c22ae29ad731 100644 --- a/client/src/utils/checkAuthentication.test.ts +++ b/client/src/utils/checkAuthentication.test.ts @@ -13,7 +13,9 @@ it('dispatches correct actions when auth token is ok', async () => { ;(mockedAxios.get as jest.Mock).mockImplementation((path: string, params?: any) => { return Promise.resolve(userRes) }) - + ;(mockedAxios.post as jest.Mock).mockImplementation((path: string, params?: any) => { + return Promise.resolve({ data: {} }) + }) const spy = jest.spyOn(store, 'dispatch') const testToken = 'Bearer eyJ0eXAiOiJeyJ0eXAiOiJKV1QeyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE2MTgzMDQ5MzksImV4cCI6MzI1MTI1MjU3NTcsImF1ZCI6Ind3dy5leGFtcGxlLmNvbSIsInN1YiI6Impyb2NrZXRAZXhhbXBsZS5jb20iLCJHaXZlbk5hbWUiOiJKb2hubnkiLCJTdXJuYW1lIjoiUm9ja2V0IiwiRW1haWwiOiJqcm9ja2V0QGV4YW1wbGUuY29tIiwiUm9sZSI6WyJNYW5hZ2VyIiwiUHJvamVjdCBBZG1pbmlzdHJhdG9yIl19.DrOOcCo5jMR-SNERE0uRp_kolQ2HjX8-hHXYEMnIxSceyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE2MTgzMDQ5MzksImV4cCI6MzI1MTI1MjU3NTcsImF1ZCI6Ind3dy5leGFtcGxlLmNvbSIsInN1YiI6Impyb2NrZXRAZXhhbXBsZS5jb20iLCJHaXZlbk5hbWUiOiJKb2hubnkiLCJTdXJuYW1lIjoiUm9ja2V0IiwiRW1haWwiOiJqcm9ja2V0QGV4YW1wbGUuY29tIiwiUm9sZSI6WyJNYW5hZ2VyIiwiUHJvamVjdCBBZG1pbmlzdHJhdG9yIl19.DrOOcCo5jMR-SNERE0uRp_kolQ2HjX8-hHXYEMnIxSceyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE2MTgzMDQ5MzksImV4cCI6MzI1MTI1MjU3NTcsImF1ZCI6Ind3dy5leGFtcGxlLmNvbSIsInN1YiI6Impyb2NrZXRAZXhhbXBsZS5jb20iLCJHaXZlbk5hbWUiOiJKb2hubnkiLCJTdXJuYW1lIjoiUm9ja2V0IiwiRW1haWwiOiJqcm9ja2V0QGV4YW1wbGUuY29tIiwiUm9sZSI6WyJNYW5hZ2VyIiwiUHJvamVjdCBBZG1pbmlzdHJhdG9yIl19.DrOOcCo5jMR-SNERE0uRp_kolQ2HjX8-hHXYEMnIxSceyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE2MTgzMDQ5MzksImV4cCI6MzI1MTI1MjU3NTcsImF1ZCI6Ind3dy5leGFtcGxlLmNvbSIsInN1YiI6Impyb2NrZXRAZXhhbXBsZS5jb20iLCJHaXZlbk5hbWUiOiJKb2hubnkiLCJTdXJuYW1lIjoiUm9ja2V0IiwiRW1haWwiOiJqcm9ja2V0QGV4YW1wbGUuY29tIiwiUm9sZSI6WyJNYW5hZ2VyIiwiUHJvamVjdCBBZG1pbmlzdHJhdG9yIl19.DrOOcCo5jMR-SNERE0uRp_kolQ2HjX8-hHXYEMnIxSciLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE2MTgzMDQ5MzksImV4cCI6MzI1MTI1MjU3NTcsImF1ZCI6Ind3dy5leGFtcGxlLmNvbSIsInN1YiI6Impyb2NrZXRAZXhhbXBsZS5jb20iLCJHaXZlbk5hbWUiOiJKb2hubnkiLCJTdXJuYW1lIjoiUm9ja2V0IiwiRW1haWwiOiJqcm9ja2V0QGV4YW1wbGUuY29tIiwiUm9sZSI6WyJNYW5hZ2VyIiwiUHJvamVjdCBBZG1pbmlzdHJhdG9yIl19.DrOOcCo5jMR-SNERE0uRp_kolQ2HjX8-hHXYEMnIxScKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE2MTgzMDQ5MzksImV4cCI6MzI1MTI1MjU3NTcsImF1ZCI6Ind3dy5leGFtcGxlLmNvbSIsInN1YiI6Impyb2NrZXRAZXhhbXBsZS5jb20iLCJHaXZlbk5hbWUiOiJKb2hubnkiLCJTdXJuYW1lIjoiUm9ja2V0IiwiRW1haWwiOiJqcm9ja2V0QGV4YW1wbGUuY29tIiwiUm9sZSI6WyJNYW5hZ2VyIiwiUHJvamVjdCBBZG1pbmlzdHJhdG9yIl19.DrOOcCo5jMR-SNERE0uRp_kolQ2HjX8-hHXYEMnIxSc' @@ -30,7 +32,9 @@ it('dispatches correct actions when getting user data fails', async () => { ;(mockedAxios.get as jest.Mock).mockImplementation((path: string, params?: any) => { return Promise.reject(new Error('failed getting user data')) }) - + ;(mockedAxios.post as jest.Mock).mockImplementation((path: string, params?: any) => { + return Promise.resolve({ data: {} }) + }) const spy = jest.spyOn(store, 'dispatch') const testToken = 'Bearer eyJ0eXAiOiJeyJ0eXAiOiJKV1QeyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE2MTgzMDQ5MzksImV4cCI6MzI1MTI1MjU3NTcsImF1ZCI6Ind3dy5leGFtcGxlLmNvbSIsInN1YiI6Impyb2NrZXRAZXhhbXBsZS5jb20iLCJHaXZlbk5hbWUiOiJKb2hubnkiLCJTdXJuYW1lIjoiUm9ja2V0IiwiRW1haWwiOiJqcm9ja2V0QGV4YW1wbGUuY29tIiwiUm9sZSI6WyJNYW5hZ2VyIiwiUHJvamVjdCBBZG1pbmlzdHJhdG9yIl19.DrOOcCo5jMR-SNERE0uRp_kolQ2HjX8-hHXYEMnIxSceyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE2MTgzMDQ5MzksImV4cCI6MzI1MTI1MjU3NTcsImF1ZCI6Ind3dy5leGFtcGxlLmNvbSIsInN1YiI6Impyb2NrZXRAZXhhbXBsZS5jb20iLCJHaXZlbk5hbWUiOiJKb2hubnkiLCJTdXJuYW1lIjoiUm9ja2V0IiwiRW1haWwiOiJqcm9ja2V0QGV4YW1wbGUuY29tIiwiUm9sZSI6WyJNYW5hZ2VyIiwiUHJvamVjdCBBZG1pbmlzdHJhdG9yIl19.DrOOcCo5jMR-SNERE0uRp_kolQ2HjX8-hHXYEMnIxSceyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE2MTgzMDQ5MzksImV4cCI6MzI1MTI1MjU3NTcsImF1ZCI6Ind3dy5leGFtcGxlLmNvbSIsInN1YiI6Impyb2NrZXRAZXhhbXBsZS5jb20iLCJHaXZlbk5hbWUiOiJKb2hubnkiLCJTdXJuYW1lIjoiUm9ja2V0IiwiRW1haWwiOiJqcm9ja2V0QGV4YW1wbGUuY29tIiwiUm9sZSI6WyJNYW5hZ2VyIiwiUHJvamVjdCBBZG1pbmlzdHJhdG9yIl19.DrOOcCo5jMR-SNERE0uRp_kolQ2HjX8-hHXYEMnIxSceyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE2MTgzMDQ5MzksImV4cCI6MzI1MTI1MjU3NTcsImF1ZCI6Ind3dy5leGFtcGxlLmNvbSIsInN1YiI6Impyb2NrZXRAZXhhbXBsZS5jb20iLCJHaXZlbk5hbWUiOiJKb2hubnkiLCJTdXJuYW1lIjoiUm9ja2V0IiwiRW1haWwiOiJqcm9ja2V0QGV4YW1wbGUuY29tIiwiUm9sZSI6WyJNYW5hZ2VyIiwiUHJvamVjdCBBZG1pbmlzdHJhdG9yIl19.DrOOcCo5jMR-SNERE0uRp_kolQ2HjX8-hHXYEMnIxSciLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE2MTgzMDQ5MzksImV4cCI6MzI1MTI1MjU3NTcsImF1ZCI6Ind3dy5leGFtcGxlLmNvbSIsInN1YiI6Impyb2NrZXRAZXhhbXBsZS5jb20iLCJHaXZlbk5hbWUiOiJKb2hubnkiLCJTdXJuYW1lIjoiUm9ja2V0IiwiRW1haWwiOiJqcm9ja2V0QGV4YW1wbGUuY29tIiwiUm9sZSI6WyJNYW5hZ2VyIiwiUHJvamVjdCBBZG1pbmlzdHJhdG9yIl19.DrOOcCo5jMR-SNERE0uRp_kolQ2HjX8-hHXYEMnIxScKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE2MTgzMDQ5MzksImV4cCI6MzI1MTI1MjU3NTcsImF1ZCI6Ind3dy5leGFtcGxlLmNvbSIsInN1YiI6Impyb2NrZXRAZXhhbXBsZS5jb20iLCJHaXZlbk5hbWUiOiJKb2hubnkiLCJTdXJuYW1lIjoiUm9ja2V0IiwiRW1haWwiOiJqcm9ja2V0QGV4YW1wbGUuY29tIiwiUm9sZSI6WyJNYW5hZ2VyIiwiUHJvamVjdCBBZG1pbmlzdHJhdG9yIl19.DrOOcCo5jMR-SNERE0uRp_kolQ2HjX8-hHXYEMnIxSc' @@ -43,12 +47,18 @@ it('dispatches correct actions when getting user data fails', async () => { }) it('dispatches no actions when no token exists', async () => { + ;(mockedAxios.post as jest.Mock).mockImplementation((path: string, params?: any) => { + return Promise.resolve({ data: {} }) + }) const spy = jest.spyOn(store, 'dispatch') await CheckAuthentication() expect(spy).not.toBeCalled() }) it('dispatches correct actions when token is expired', async () => { + ;(mockedAxios.post as jest.Mock).mockImplementation((path: string, params?: any) => { + return Promise.resolve({ data: {} }) + }) const testToken = 'Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE2MTgzMDY1MTUsImV4cCI6MTU4Njc3MDUxNSwiYXVkIjoid3d3LmV4YW1wbGUuY29tIiwic3ViIjoianJvY2tldEBleGFtcGxlLmNvbSIsIkdpdmVuTmFtZSI6IkpvaG5ueSIsIlN1cm5hbWUiOiJSb2NrZXQiLCJFbWFpbCI6Impyb2NrZXRAZXhhbXBsZS5jb20iLCJSb2xlIjpbIk1hbmFnZXIiLCJQcm9qZWN0IEFkbWluaXN0cmF0b3IiXX0.R5-oWGGumd-YWPoKyziJmVB8SdX6B9SsV6m7novIfgg' localStorage.setItem('token', testToken) diff --git a/client/src/utils/checkAuthentication.ts b/client/src/utils/checkAuthentication.ts index 231781543c4b8c3088585568fbeedbcab4e16f41..a782ea10f79a65998ed7d1a9e091361aa6a48982 100644 --- a/client/src/utils/checkAuthentication.ts +++ b/client/src/utils/checkAuthentication.ts @@ -4,8 +4,8 @@ import Types from '../actions/types' import { logoutUser } from '../actions/user' import store from '../store' -const UnAuthorized = () => { - logoutUser()(store.dispatch) +const UnAuthorized = async () => { + await logoutUser()(store.dispatch) } export const CheckAuthentication = async () => { @@ -29,7 +29,7 @@ export const CheckAuthentication = async () => { UnAuthorized() }) } else { - UnAuthorized() + await UnAuthorized() } } } diff --git a/server/app/__init__.py b/server/app/__init__.py index a25293952d75dd64ff86e4fe76b105447edf8cfe..9215563a9538bf1d33aecabc2b1abbc32779950a 100644 --- a/server/app/__init__.py +++ b/server/app/__init__.py @@ -7,7 +7,7 @@ from app.core.dto import MediaDTO def create_app(config_name="configmodule.DevelopmentConfig"): - app = Flask(__name__) + app = Flask(__name__, static_url_path="/static", static_folder="static") app.config.from_object(config_name) app.url_map.strict_slashes = False with app.app_context(): @@ -15,9 +15,14 @@ def create_app(config_name="configmodule.DevelopmentConfig"): bcrypt.init_app(app) jwt.init_app(app) db.init_app(app) + db.create_all() ma.init_app(app) configure_uploads(app, (MediaDTO.image_set,)) + from app.core.sockets import sio + + sio.init_app(app) + from app.apis import flask_api flask_api.init_app(app) @@ -34,7 +39,7 @@ def create_app(config_name="configmodule.DevelopmentConfig"): header["Access-Control-Allow-Origin"] = "*" return response - return app + return app, sio def identity(payload): diff --git a/server/app/apis/__init__.py b/server/app/apis/__init__.py index b48b8b33704902bca098e76af9080cf13cf6074c..683b61c02b3d48caf29e38470d878b3a38f62ae7 100644 --- a/server/app/apis/__init__.py +++ b/server/app/apis/__init__.py @@ -1,42 +1,55 @@ from functools import wraps -import app.core.http_codes as codes +import app.core.http_codes as http_codes from flask_jwt_extended import verify_jwt_in_request from flask_jwt_extended.utils import get_jwt_claims from flask_restx.errors import abort -def admin_required(): +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) + + +def check_jwt(editor=False, *views): def wrapper(fn): @wraps(fn) def decorator(*args, **kwargs): verify_jwt_in_request() claims = get_jwt_claims() - if claims["role"] == "Admin": + role = claims.get("role") + view = claims.get("view") + if role == "Admin": + return fn(*args, **kwargs) + elif editor and role == "Editor": + return fn(*args, **kwargs) + elif view in views: return fn(*args, **kwargs) else: - return {"message:": "Admins only"}, codes.FORBIDDEN + abort(http_codes.UNAUTHORIZED) return decorator return wrapper -def text_response(message, code=codes.OK): +def text_response(message, code=http_codes.OK): return {"message": message}, code -def list_response(items, total=None, code=codes.OK): +def list_response(items, total=None, code=http_codes.OK): if type(items) is not list: - abort(codes.INTERNAL_SERVER_ERROR) + abort(http_codes.INTERNAL_SERVER_ERROR) if not total: total = len(items) return {"items": items, "count": len(items), "total_count": total}, code -def item_response(item, code=codes.OK): +def item_response(item, code=http_codes.OK): if isinstance(item, list): - abort(codes.INTERNAL_SERVER_ERROR) + abort(http_codes.INTERNAL_SERVER_ERROR) return item, code diff --git a/server/app/apis/auth.py b/server/app/apis/auth.py index 10d820f88d1a5570635a05e54a2ef45492a1c645..86ac53d52d7712a927411f1b03e32ee0b99a385f 100644 --- a/server/app/apis/auth.py +++ b/server/app/apis/auth.py @@ -1,9 +1,9 @@ import app.core.http_codes as codes import app.database.controller as dbc -from app.apis import admin_required, item_response, text_response +from app.apis import check_jwt, item_response, text_response from app.core.codes import verify_code from app.core.dto import AuthDTO, CodeDTO -from app.core.parsers import create_user_parser, login_parser +from app.core.parsers import create_user_parser, login_code_parser, login_parser from app.database.models import User from flask_jwt_extended import ( create_access_token, @@ -21,12 +21,12 @@ list_schema = AuthDTO.list_schema def get_user_claims(item_user): - return {"role": item_user.role.name, "city": item_user.city.name} + return {"role": item_user.role.name, "city_id": item_user.city_id} @api.route("/signup") class AuthSignup(Resource): - @jwt_required + @check_jwt(editor=False) def post(self): args = create_user_parser.parse_args(strict=True) email = args.get("email") @@ -41,7 +41,7 @@ class AuthSignup(Resource): @api.route("/delete/<ID>") @api.param("ID") class AuthDelete(Resource): - @jwt_required + @check_jwt(editor=False) def delete(self, ID): item_user = dbc.get.user(ID) @@ -70,23 +70,22 @@ class AuthLogin(Resource): return response -@api.route("/login/<code>") -@api.param("code") -class AuthLogin(Resource): - def post(self, code): +@api.route("/login/code") +class AuthLoginCode(Resource): + def post(self): + args = login_code_parser.parse_args() + code = args["code"] + if not verify_code(code): api.abort(codes.BAD_REQUEST, "Invalid code") - item_code = dbc.get.code_by_code(code) - if not item_code: - api.abort(codes.UNAUTHORIZED, "A presentation with that code does not exist") - + item_code = dbc.get.code_by_code(code, True, "A presentation with that code does not exist") return item_response(CodeDTO.schema.dump(item_code)), codes.OK @api.route("/logout") class AuthLogout(Resource): - @jwt_required + @check_jwt(editor=True) def post(self): jti = get_raw_jwt()["jti"] dbc.add.blacklist(jti) @@ -95,7 +94,7 @@ class AuthLogout(Resource): @api.route("/refresh") class AuthRefresh(Resource): - @jwt_required + @check_jwt(editor=True) @jwt_refresh_token_required def post(self): old_jti = get_raw_jwt()["jti"] diff --git a/server/app/apis/codes.py b/server/app/apis/codes.py index af6aee8499dd595ecb189a72f9f575a96511bee1..332d5f3e612b8c0ecf886c95331114073d6c6030 100644 --- a/server/app/apis/codes.py +++ b/server/app/apis/codes.py @@ -1,11 +1,12 @@ import app.database.controller as dbc -from app.apis import admin_required, item_response, list_response +from app.apis import item_response, list_response from app.core import http_codes as codes from app.core.dto import CodeDTO from app.core.parsers import code_parser from app.database.models import Code, Competition from flask_jwt_extended import jwt_required from flask_restx import Resource +from app.apis import check_jwt api = CodeDTO.api schema = CodeDTO.schema @@ -15,7 +16,7 @@ list_schema = CodeDTO.list_schema @api.route("/") @api.param("CID") class CodesList(Resource): - @jwt_required + @check_jwt(editor=True) def get(self, CID): items = dbc.get.code_list(CID) return list_response(list_schema.dump(items), len(items)), codes.OK @@ -24,7 +25,7 @@ class CodesList(Resource): @api.route("/<code_id>") @api.param("CID, code_id") class CodesById(Resource): - @jwt_required + @check_jwt(editor=False) def put(self, CID, code_id): item = dbc.get.one(Code, code_id) item.code = dbc.utils.generate_unique_code() diff --git a/server/app/apis/competitions.py b/server/app/apis/competitions.py index 26b6f363831fb4508d75898c666e25897322eec0..db2ca68a44b9d8c88ee0abbf0d4be98c8dc1684d 100644 --- a/server/app/apis/competitions.py +++ b/server/app/apis/competitions.py @@ -1,5 +1,8 @@ +import time + import app.database.controller as dbc -from app.apis import admin_required, item_response, list_response +from app.apis import check_jwt, item_response, list_response +from app.core import rich_schemas from app.core.dto import CompetitionDTO from app.core.parsers import competition_parser, competition_search_parser from app.database.models import Competition @@ -8,12 +11,13 @@ from flask_restx import Resource api = CompetitionDTO.api schema = CompetitionDTO.schema +rich_schema = CompetitionDTO.rich_schema list_schema = CompetitionDTO.list_schema @api.route("/") class CompetitionsList(Resource): - @jwt_required + @check_jwt(editor=True) def post(self): args = competition_parser.parse_args(strict=True) @@ -28,12 +32,13 @@ class CompetitionsList(Resource): @api.route("/<CID>") @api.param("CID") class Competitions(Resource): - @jwt_required + @check_jwt(editor=True) def get(self, CID): - item = dbc.get.one(Competition, CID) - return item_response(schema.dump(item)) + item = dbc.get.competition(CID) + + return item_response(rich_schema.dump(item)) - @jwt_required + @check_jwt(editor=True) def put(self, CID): args = competition_parser.parse_args(strict=True) item = dbc.get.one(Competition, CID) @@ -41,7 +46,7 @@ class Competitions(Resource): return item_response(schema.dump(item)) - @jwt_required + @check_jwt(editor=True) def delete(self, CID): item = dbc.get.one(Competition, CID) dbc.delete.competition(item) @@ -51,7 +56,7 @@ class Competitions(Resource): @api.route("/search") class CompetitionSearch(Resource): - @jwt_required + @check_jwt(editor=True) def get(self): args = competition_search_parser.parse_args(strict=True) items, total = dbc.search.competition(**args) diff --git a/server/app/apis/components.py b/server/app/apis/components.py index f88e1e2de7eeca66859a46d5a790dbe6ef84662a..c68ca92864a75b21c5ac03d8a519029a3e44fd0a 100644 --- a/server/app/apis/components.py +++ b/server/app/apis/components.py @@ -1,6 +1,6 @@ import app.core.http_codes as codes import app.database.controller as dbc -from app.apis import admin_required, item_response, list_response +from app.apis import check_jwt, item_response, list_response from app.core.dto import ComponentDTO from app.core.parsers import component_create_parser, component_parser from app.database.models import Competition, Component @@ -16,18 +16,19 @@ list_schema = ComponentDTO.list_schema @api.route("/<component_id>") @api.param("CID, SOrder, component_id") class ComponentByID(Resource): - @jwt_required + @check_jwt(editor=True) def get(self, CID, SOrder, component_id): item = dbc.get.one(Component, component_id) return item_response(schema.dump(item)) - @jwt_required + @check_jwt(editor=True) def put(self, CID, SOrder, component_id): args = component_parser.parse_args() - item = dbc.edit.component(**args) + item = dbc.get.one(Component, component_id) + item = dbc.edit.component(item, **args) return item_response(schema.dump(item)) - @jwt_required + @check_jwt(editor=True) def delete(self, CID, SOrder, component_id): item = dbc.get.one(Component, component_id) dbc.delete.component(item) @@ -37,12 +38,12 @@ class ComponentByID(Resource): @api.route("/") @api.param("CID, SOrder") class ComponentList(Resource): - @jwt_required + @check_jwt(editor=True) def get(self, CID, SOrder): - items = dbc.get.component_list(SOrder) + items = dbc.get.component_list(CID, SOrder) return list_response(list_schema.dump(items)) - @jwt_required + @check_jwt(editor=True) def post(self, CID, SOrder): args = component_create_parser.parse_args() item_slide = dbc.get.slide(CID, SOrder) diff --git a/server/app/apis/media.py b/server/app/apis/media.py index a7c2d5d143963e87d434d4c6c7770427663622c0..830b16de0a6125dde7c95406c8b7bc928c2bc105 100644 --- a/server/app/apis/media.py +++ b/server/app/apis/media.py @@ -1,47 +1,84 @@ +import os + import app.core.http_codes as codes import app.database.controller as dbc -from app.apis import admin_required, item_response, list_response +from app.apis import check_jwt, item_response, list_response from app.core.dto import MediaDTO from app.core.parsers import media_parser_search from app.database.models import City, Media, MediaType, QuestionType, Role -from flask import request +from flask import current_app, request from flask_jwt_extended import get_jwt_identity, jwt_required from flask_restx import Resource, reqparse from flask_uploads import UploadNotAllowed from PIL import Image +from sqlalchemy import exc api = MediaDTO.api image_set = MediaDTO.image_set schema = MediaDTO.schema list_schema = MediaDTO.list_schema +PHOTO_PATH = current_app.config["UPLOADED_PHOTOS_DEST"] + def generate_thumbnail(filename): - with Image.open(f"./static/images/{filename}") as im: - im.thumbnail((120, 120)) - im.save(f"./static/images/thumbnail_{filename}") + thumbnail_size = current_app.config["THUMBNAIL_SIZE"] + path = os.path.join(PHOTO_PATH, filename) + thumb_path = os.path.join(PHOTO_PATH, f"thumbnail_{filename}") + with Image.open(path) as im: + im.thumbnail(thumbnail_size) + im.save(thumb_path) + + +def delete_image(filename): + path = os.path.join(PHOTO_PATH, filename) + thumb_path = os.path.join(PHOTO_PATH, f"thumbnail_{filename}") + os.remove(path) + os.remove(thumb_path) @api.route("/images") class ImageList(Resource): - @jwt_required + @check_jwt(editor=True) def get(self): args = media_parser_search.parse_args(strict=True) items, total = dbc.search.image(**args) return list_response(list_schema.dump(items), total) - @jwt_required + @check_jwt(editor=True) def post(self): if "image" not in request.files: api.abort(codes.BAD_REQUEST, "Missing image in request.files") - try: filename = image_set.save(request.files["image"]) generate_thumbnail(filename) print(filename) item = dbc.add.image(filename, get_jwt_identity()) - return item_response(schema.dump(item)) except UploadNotAllowed: api.abort(codes.BAD_REQUEST, "Could not save the image") except: api.abort(codes.INTERNAL_SERVER_ERROR, "Something went wrong when trying to save image") + finally: + return item_response(schema.dump(item)) + + +@api.route("/images/<ID>") +@api.param("ID") +class ImageList(Resource): + @check_jwt(editor=True) + def get(self, ID): + item = dbc.get.one(Media, ID) + return item_response(schema.dump(item)) + + @check_jwt(editor=True) + def delete(self, ID): + item = dbc.get.one(Media, ID) + try: + delete_image(item.filename) + dbc.delete.default(item) + except OSError: + api.abort(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") + finally: + return {}, codes.NO_CONTENT diff --git a/server/app/apis/misc.py b/server/app/apis/misc.py index a74ac6d1c790198e0e6cda87b4ddb638499802c2..5364a5e44d4e309c327bf91121bc1f8d95e40afa 100644 --- a/server/app/apis/misc.py +++ b/server/app/apis/misc.py @@ -1,8 +1,7 @@ import app.database.controller as dbc -from app.apis import admin_required, item_response, list_response +from app.apis import check_jwt, item_response, list_response from app.core.dto import MiscDTO -from app.database.models import (City, ComponentType, MediaType, QuestionType, - Role, ViewType) +from app.database.models import City, ComponentType, MediaType, QuestionType, Role, ViewType from flask_jwt_extended import jwt_required from flask_restx import Resource, reqparse @@ -34,7 +33,7 @@ class TypesList(Resource): @api.route("/roles") class RoleList(Resource): - @jwt_required + @check_jwt(editor=True) def get(self): items = dbc.get.all(Role) return list_response(role_schema.dump(items)) @@ -42,12 +41,12 @@ class RoleList(Resource): @api.route("/cities") class CitiesList(Resource): - @jwt_required + @check_jwt(editor=True) def get(self): items = dbc.get.all(City) return list_response(city_schema.dump(items)) - @jwt_required + @check_jwt(editor=False) def post(self): args = name_parser.parse_args(strict=True) dbc.add.city(args["name"]) @@ -58,7 +57,7 @@ class CitiesList(Resource): @api.route("/cities/<ID>") @api.param("ID") class Cities(Resource): - @jwt_required + @check_jwt(editor=False) def put(self, ID): item = dbc.get.one(City, ID) args = name_parser.parse_args(strict=True) @@ -67,7 +66,7 @@ class Cities(Resource): items = dbc.get.all(City) return list_response(city_schema.dump(items)) - @jwt_required + @check_jwt(editor=False) def delete(self, ID): item = dbc.get.one(City, ID) dbc.delete.default(item) diff --git a/server/app/apis/questions.py b/server/app/apis/questions.py index 55db2819949adae273c230c0f9548a6b2843db37..f486e3493b09070b5da145c48f497ea30792164e 100644 --- a/server/app/apis/questions.py +++ b/server/app/apis/questions.py @@ -1,6 +1,6 @@ import app.core.http_codes as codes import app.database.controller as dbc -from app.apis import admin_required, item_response, list_response +from app.apis import check_jwt, item_response, list_response from app.core.dto import QuestionDTO from app.core.parsers import question_parser from app.database.models import Question @@ -15,7 +15,7 @@ list_schema = QuestionDTO.list_schema @api.route("/questions") @api.param("CID") class QuestionsList(Resource): - @jwt_required + @check_jwt(editor=True) def get(self, CID): items = dbc.get.question_list(CID) return list_response(list_schema.dump(items)) @@ -24,7 +24,7 @@ class QuestionsList(Resource): @api.route("/slides/<SID>/questions") @api.param("CID, SID") class QuestionsList(Resource): - @jwt_required + @check_jwt(editor=True) def post(self, SID, CID): args = question_parser.parse_args(strict=True) del args["slide_id"] @@ -38,12 +38,12 @@ class QuestionsList(Resource): @api.route("/slides/<SID>/questions/<QID>") @api.param("CID, SID, QID") class Questions(Resource): - @jwt_required + @check_jwt(editor=True) def get(self, CID, SID, QID): item_question = dbc.get.question(CID, SID, QID) return item_response(schema.dump(item_question)) - @jwt_required + @check_jwt(editor=True) def put(self, CID, SID, QID): args = question_parser.parse_args(strict=True) @@ -52,7 +52,7 @@ class Questions(Resource): return item_response(schema.dump(item_question)) - @jwt_required + @check_jwt(editor=True) def delete(self, CID, SID, QID): item_question = dbc.get.question(CID, SID, QID) dbc.delete.question(item_question) diff --git a/server/app/apis/slides.py b/server/app/apis/slides.py index 9aeb793ad0e6e967eda3055d9490485f63a35547..02d0d3d699cf44a29acd5cee7e50fe3d4456c548 100644 --- a/server/app/apis/slides.py +++ b/server/app/apis/slides.py @@ -1,6 +1,6 @@ import app.core.http_codes as codes import app.database.controller as dbc -from app.apis import admin_required, item_response, list_response +from app.apis import check_jwt, item_response, list_response from app.core.dto import SlideDTO from app.core.parsers import slide_parser from app.database.models import Competition, Slide @@ -15,12 +15,12 @@ list_schema = SlideDTO.list_schema @api.route("/") @api.param("CID") class SlidesList(Resource): - @jwt_required + @check_jwt(editor=True) def get(self, CID): items = dbc.get.slide_list(CID) return list_response(list_schema.dump(items)) - @jwt_required + @check_jwt(editor=True) def post(self, CID): item_comp = dbc.get.one(Competition, CID) item_slide = dbc.add.slide(item_comp) @@ -32,12 +32,12 @@ class SlidesList(Resource): @api.route("/<SOrder>") @api.param("CID,SOrder") class Slides(Resource): - @jwt_required + @check_jwt(editor=True) def get(self, CID, SOrder): item_slide = dbc.get.slide(CID, SOrder) return item_response(schema.dump(item_slide)) - @jwt_required + @check_jwt(editor=True) def put(self, CID, SOrder): args = slide_parser.parse_args(strict=True) title = args.get("title") @@ -48,7 +48,7 @@ class Slides(Resource): return item_response(schema.dump(item_slide)) - @jwt_required + @check_jwt(editor=True) def delete(self, CID, SOrder): item_slide = dbc.get.slide(CID, SOrder) @@ -59,7 +59,7 @@ class Slides(Resource): @api.route("/<SOrder>/order") @api.param("CID,SOrder") class SlidesOrder(Resource): - @jwt_required + @check_jwt(editor=True) def put(self, CID, SOrder): args = slide_parser.parse_args(strict=True) order = args.get("order") diff --git a/server/app/apis/teams.py b/server/app/apis/teams.py index 6729ccf8984e6ba85de5b79e2cfa0c3edde0f4ee..2bb0a23570e5de4abb1668347a6fe287b56e7957 100644 --- a/server/app/apis/teams.py +++ b/server/app/apis/teams.py @@ -1,6 +1,6 @@ import app.core.http_codes as codes import app.database.controller as dbc -from app.apis import admin_required, item_response, list_response +from app.apis import check_jwt, item_response, list_response from app.core.dto import TeamDTO from app.core.parsers import team_parser from app.database.models import Competition, Team @@ -15,15 +15,15 @@ list_schema = TeamDTO.list_schema @api.route("/") @api.param("CID") class TeamsList(Resource): - @jwt_required + @check_jwt(editor=True) def get(self, CID): items = dbc.get.team_list(CID) return list_response(list_schema.dump(items)) - @jwt_required + @check_jwt(editor=True) def post(self, CID): args = team_parser.parse_args(strict=True) - item_comp = dbc.get.one(Competition,CID) + item_comp = dbc.get.one(Competition, CID) item_team = dbc.add.team(args["name"], item_comp) return item_response(schema.dump(item_team)) @@ -32,11 +32,13 @@ class TeamsList(Resource): @api.param("CID,TID") class Teams(Resource): @jwt_required + @check_jwt(editor=True) def get(self, CID, TID): item = dbc.get.team(CID, TID) return item_response(schema.dump(item)) @jwt_required + @check_jwt(editor=True) def delete(self, CID, TID): item_team = dbc.get.team(CID, TID) @@ -44,6 +46,7 @@ class Teams(Resource): return {}, codes.NO_CONTENT @jwt_required + @check_jwt(editor=True) def put(self, CID, TID): args = team_parser.parse_args(strict=True) name = args.get("name") diff --git a/server/app/apis/users.py b/server/app/apis/users.py index 28642b0dfa47ef9bcd3392c0016d49017f41f8a3..b9dba528a1a3529ec9e340d4418e44bc2e20fedb 100644 --- a/server/app/apis/users.py +++ b/server/app/apis/users.py @@ -1,6 +1,6 @@ import app.core.http_codes as codes import app.database.controller as dbc -from app.apis import admin_required, item_response, list_response +from app.apis import check_jwt, item_response, list_response from app.core.dto import UserDTO from app.core.parsers import user_parser, user_search_parser from app.database.models import User @@ -24,12 +24,12 @@ def edit_user(item_user, args): @api.route("/") class UsersList(Resource): - @jwt_required + @check_jwt(editor=True) def get(self): item = dbc.get.user(get_jwt_identity()) return item_response(schema.dump(item)) - @jwt_required + @check_jwt(editor=True) def put(self): args = user_parser.parse_args(strict=True) item = dbc.get.user(get_jwt_identity()) @@ -40,12 +40,12 @@ class UsersList(Resource): @api.route("/<ID>") @api.param("ID") class Users(Resource): - @jwt_required + @check_jwt(editor=True) def get(self, ID): item = dbc.get.user(ID) return item_response(schema.dump(item)) - @jwt_required + @check_jwt(editor=False) def put(self, ID): args = user_parser.parse_args(strict=True) item = dbc.get.user(ID) @@ -55,7 +55,7 @@ class Users(Resource): @api.route("/search") class UserSearch(Resource): - @jwt_required + @check_jwt(editor=True) def get(self): args = user_search_parser.parse_args(strict=True) items, total = dbc.search.user(**args) diff --git a/server/app/core/dto.py b/server/app/core/dto.py index 6541ef75903db43ecd9a239e3b970d1e4506e240..034826f5fb784bca39bc85c355d767ccc7ab1498 100644 --- a/server/app/core/dto.py +++ b/server/app/core/dto.py @@ -19,25 +19,26 @@ class MediaDTO: class AuthDTO: api = Namespace("auth") - schema = rich_schemas.UserSchemaRich(many=False) - list_schema = rich_schemas.UserSchemaRich(many=True) + schema = schemas.UserSchema(many=False) + list_schema = schemas.UserSchema(many=True) class UserDTO: api = Namespace("users") - schema = rich_schemas.UserSchemaRich(many=False) + schema = schemas.UserSchema(many=False) list_schema = schemas.UserSchema(many=True) class CompetitionDTO: api = Namespace("competitions") - schema = rich_schemas.CompetitionSchemaRich(many=False) + schema = schemas.CompetitionSchema(many=False) list_schema = schemas.CompetitionSchema(many=True) + rich_schema = rich_schemas.CompetitionSchemaRich(many=False) class CodeDTO: api = Namespace("codes") - schema = rich_schemas.CodeSchemaRich(many=False) + schema = schemas.CodeSchema(many=False) list_schema = schemas.CodeSchema(many=True) @@ -65,5 +66,5 @@ class MiscDTO: class QuestionDTO: api = Namespace("questions") - schema = rich_schemas.QuestionSchemaRich(many=False) + schema = schemas.QuestionSchema(many=False) list_schema = schemas.QuestionSchema(many=True) diff --git a/server/app/core/parsers.py b/server/app/core/parsers.py index f691ea8d3924a8679068713bf85ca47e2d065376..f5536cf2f1f0f5e0a751bb70257b9f6f35278d4e 100644 --- a/server/app/core/parsers.py +++ b/server/app/core/parsers.py @@ -86,3 +86,6 @@ component_parser.add_argument("data", type=dict, default=None, location="json") component_create_parser = component_parser.copy() component_create_parser.replace_argument("data", type=dict, required=True, location="json") component_create_parser.add_argument("type_id", type=int, required=True, location="json") + +login_code_parser = reqparse.RequestParser() +login_code_parser.add_argument("code", type=str, location="json") diff --git a/server/app/core/rich_schemas.py b/server/app/core/rich_schemas.py index fa6daac9ef59117b76ab9d2244f80af3224dbbe5..a890489e23a30aef19c41b1cfc105d9954c68d37 100644 --- a/server/app/core/rich_schemas.py +++ b/server/app/core/rich_schemas.py @@ -11,17 +11,6 @@ class RichSchema(ma.SQLAlchemySchema): include_relationships = True -class UserSchemaRich(RichSchema): - class Meta(RichSchema.Meta): - model = models.User - - id = ma.auto_field() - name = ma.auto_field() - email = ma.auto_field() - role = fields.Nested(schemas.RoleSchema, many=False) - city = fields.Nested(schemas.CitySchema, many=False) - - class QuestionSchemaRich(RichSchema): class Meta(RichSchema.Meta): model = models.Question @@ -30,7 +19,8 @@ class QuestionSchemaRich(RichSchema): name = ma.auto_field() total_score = ma.auto_field() slide_id = ma.auto_field() - type = fields.Nested(schemas.QuestionTypeSchema, many=False) + type_id = ma.auto_field() + alternatives = fields.Nested(schemas.QuestionAlternative, many=True) class TeamSchemaRich(RichSchema): @@ -43,16 +33,6 @@ class TeamSchemaRich(RichSchema): question_answers = fields.Nested(schemas.QuestionAnswerSchema, many=True) -class CodeSchemaRich(RichSchema): - class Meta(RichSchema.Meta): - model = models.Code - - id = ma.auto_field() - code = ma.auto_field() - pointer = ma.auto_field() - view_type = fields.Nested(schemas.ViewTypeSchema, many=False) - - class SlideSchemaRich(RichSchema): class Meta(RichSchema.Meta): model = models.Slide @@ -73,7 +53,7 @@ class CompetitionSchemaRich(RichSchema): id = ma.auto_field() name = ma.auto_field() year = ma.auto_field() - city = fields.Nested(schemas.CitySchema, many=False) + city_id = ma.auto_field() slides = fields.Nested( SlideSchemaRich, many=True, diff --git a/server/app/core/schemas.py b/server/app/core/schemas.py index 4f9646a55e2e64d9498f96589b59886824845a45..ff561491ac12644abda6e004efbec12a54becb77 100644 --- a/server/app/core/schemas.py +++ b/server/app/core/schemas.py @@ -68,6 +68,16 @@ class QuestionAnswerSchema(BaseSchema): team_id = ma.auto_field() +class QuestionAlternative(BaseSchema): + class Meta(BaseSchema.Meta): + model = models.QuestionAlternative + + id = ma.auto_field() + text = ma.auto_field() + value = ma.auto_field() + question_id = ma.auto_field() + + class RoleSchema(BaseSchema): class Meta(BaseSchema.Meta): model = models.Role diff --git a/server/app/core/sockets.py b/server/app/core/sockets.py index d9407a69b764e7a70ea81459a7a18f924daee21d..f4909531961078663f4e9d014311fec126afd560 100644 --- a/server/app/core/sockets.py +++ b/server/app/core/sockets.py @@ -1,8 +1,16 @@ +import app.database.controller as dbc +from app.core import db +from app.database.models import Competition, Slide, Team, ViewType from flask.globals import request from flask_socketio import SocketIO, emit, join_room +# Presentation is an active competition + + sio = SocketIO(cors_allowed_origins="http://localhost:3000") +presentations = {} + @sio.on("connect") def connect(): @@ -11,26 +19,138 @@ def connect(): @sio.on("disconnect") def disconnect(): + for competition_id, presentation in presentations.items(): + if request.sid in presentation["clients"]: + del presentation["clients"][request.sid] + break + + if presentations and not presentations[competition_id]["clients"]: + del presentations[competition_id] + + print(f"{presentations=}") + print(f"[Disconnected]: {request.sid}") -@sio.on("join_competition") -def join_competition(data): - competitionID = data["competitionID"] - join_room(data["competitionID"]) - print(f"[Join room]: {request.sid} -> {competitionID}") +@sio.on("start_presentation") +def start_presentation(data): + competition_id = data["competition_id"] + + # TODO: Do proper error handling + if competition_id in presentations: + print("THAT PRESENTATION IS ALREADY ACTIVE") + return + + presentations[competition_id] = { + "clients": {request.sid: {"view_type": "Operator"}}, + "slide": None, + "timer": {"enabled": False, "start_value": None, "value": None}, + } + + print(f"{presentations=}") + + join_room(competition_id) + print(f"[start_presentation]: {request.sid} -> {competition_id}.") + + +@sio.on("end_presentation") +def end_presentation(data): + competition_id = data["competition_id"] + + if competition_id not in presentations: + print("NO PRESENTATION WITH THAT NAME EXISTS") + return + + if presentations[competition_id]["clients"][request.sid]["view_type"] != "Operator": + print("YOU DONT HAVE ACCESS TO DO THAT") + return + + del presentations[competition_id] + + print(f"{presentations=}") + + emit("end_presentation", room=competition_id, include_self=True) + + +@sio.on("join_presentation") +def join_presentation(data): + team_view_id = 1 + code = data["code"] + item_code = dbc.get.code_by_code(code) + + # TODO: Do proper error handling + if not item_code: + print("CODE DOES NOT EXIST") + return + competition_id = ( + item_code.pointer + if item_code.view_type_id != team_view_id + else db.session.query(Team).filter(Team.id == item_code.pointer).one().competition_id + ) -@sio.on("sync_slide") -def sync_slide(data): - slide, competitionID = data["slide"], data["competitionID"] - emit("sync_slide", {"slide": slide}, room=competitionID, include_self=False) - print(f"[Sync slide]: {slide} -> {competitionID}") + if competition_id not in presentations: + print("THAT COMPETITION IS CURRENTLY NOT ACTIVE") + return + if request.sid in presentations[competition_id]["clients"]: + print("CLIENT ALREADY IN COMPETITION") + return -@sio.on("sync_timer") + # TODO: Write function in database controller to do this + view_type_name = db.session.query(ViewType).filter(ViewType.id == item_code.view_type_id).one().name + + presentations[competition_id]["clients"][request.sid] = {"view_type": view_type_name} + join_room(competition_id) + + print(f"{presentations=}") + + print(f"[Join presentation]: {request.sid} -> {competition_id}. {view_type_name=}") + + +@sio.on("set_slide") +def set_slide(data): + competition_id = data["competition_id"] + slide_order = data["slide_order"] + + if competition_id not in presentations: + print("CANT SET SLIDE IN NON ACTIVE COMPETITION") + return + + if presentations[competition_id]["clients"][request.sid]["view_type"] != "Operator": + print("YOU DONT HAVE ACCESS TO DO THAT") + return + + num_slides = db.session.query(Slide).filter(Slide.competition_id == competition_id).count() + + if not (0 <= slide_order < num_slides): + print("CANT CHANGE TO NON EXISTENT SLIDE") + return + + presentations[competition_id]["slide"] = slide_order + + print(f"{presentations=}") + + emit("set_slide", {"slide_order": slide_order}, room=competition_id, include_self=True) + print(f"[Set slide]: {slide_order} -> {competition_id}") + + +@sio.on("set_timer") def sync_timer(data): - competitionID = data["competitionID"] + competition_id = data["competition_id"] timer = data["timer"] - emit("sync_timer", {"timer": timer}, room=competitionID, include_self=False) - print(f"[Sync timer]: {competitionID=} {timer=}") + + if competition_id not in presentations: + print("CANT SET TIMER IN NON EXISTENT COMPETITION") + return + + if presentations[competition_id]["clients"][request.sid]["view_type"] != "Operator": + print("YOU DONT HAVE ACCESS TO DO THAT") + return + + # TODO: Save timer in presentation, maybe? + + print(f"{presentations=}") + + emit("set_timer", {"timer": timer}, room=competition_id, include_self=True) + print(f"[Set timer]: {timer=}, {competition_id=}") diff --git a/server/app/database/__init__.py b/server/app/database/__init__.py index ead77d9cd731952e4f478f64604bbf49a150ea9a..7deab3352fe533919186ecf8e217ce61d8eb02eb 100644 --- a/server/app/database/__init__.py +++ b/server/app/database/__init__.py @@ -41,7 +41,7 @@ class ExtendedQuery(BaseQuery): class Dictionary(TypeDecorator): - impl = Text(1024) + impl = Text def process_bind_param(self, value, dialect): if value is not None: diff --git a/server/app/database/controller/add.py b/server/app/database/controller/add.py index 755849bb8f81e398af27310c56f3156beddf0d63..de0135f2f514745017b85f654f4afc3203b69975 100644 --- a/server/app/database/controller/add.py +++ b/server/app/database/controller/add.py @@ -1,3 +1,7 @@ +""" +This file contains functionality to add data to the database. +""" + import app.core.http_codes as codes from app.core import db from app.database.controller import utils @@ -22,6 +26,11 @@ from flask_restx import abort def db_add(item): + """ + Internal function. Adds item to the database + and handles comitting and refreshing. + """ + db.session.add(item) db.session.commit() db.session.refresh(item) @@ -33,55 +42,88 @@ def db_add(item): def blacklist(jti): + """ Adds a blacklist to the database. """ + return db_add(Blacklist(jti)) def mediaType(name): + """ Adds a media type to the database. """ + return db_add(MediaType(name)) def questionType(name): + """ Adds a question type to the database. """ + return db_add(QuestionType(name)) def componentType(name): + """ Adds a component type to the database. """ + return db_add(ComponentType(name)) def viewType(name): + """ Adds a view type to the database. """ + return db_add(ViewType(name)) def role(name): + """ Adds a role to the database. """ + return db_add(Role(name)) def city(name): + """ Adds a city to the database. """ + return db_add(City(name)) def component(type_id, item_slide, data, x=0, y=0, w=0, h=0): + """ + Adds a component to the slide at the specified coordinates with the + provided size and data . + """ + return db_add(Component(item_slide.id, type_id, data, x, y, w, h)) def image(filename, user_id): + """ + Adds an image to the database and keeps track of who called the function. + """ + return db_add(Media(filename, 1, user_id)) def user(email, password, role_id, city_id, name=None): + """ Adds a user to the database using the provided arguments. """ + return db_add(User(email, password, role_id, city_id, name)) def question(name, total_score, type_id, item_slide): + """ + Adds a question to the specified slide using the provided arguments. + """ + return db_add(Question(name, total_score, type_id, item_slide.id)) def code(pointer, view_type_id): + """ Adds a code to the database using the provided arguments. """ + code_string = utils.generate_unique_code() return db_add(Code(code_string, pointer, view_type_id)) def team(name, item_competition): + """ Adds a team with the specified name to the provided competition. """ + item = db_add(Team(name, item_competition.id)) # Add code for the team @@ -91,11 +133,15 @@ def team(name, item_competition): def slide(item_competition): + """ Adds a slide to the provided competition. """ + order = Slide.query.filter(Slide.competition_id == item_competition.id).count() # first element has index 0 return db_add(Slide(order, item_competition.id)) def competition(name, year, city_id): + """ Adds a competition to the database using the provided arguments. """ + item_competition = db_add(Competition(name, year, city_id)) # Add one slide for the competition @@ -107,7 +153,7 @@ def competition(name, year, city_id): # Add code for Audience view code(item_competition.id, 3) - # Add two teams + # TODO: Add two teams utils.refresh(item_competition) return item_competition diff --git a/server/app/database/controller/delete.py b/server/app/database/controller/delete.py index 33bea2174058e21df2d25042effe8957bfa139ff..806f3672daef2e9831f6ca9300bf9f5b70ffe2bf 100644 --- a/server/app/database/controller/delete.py +++ b/server/app/database/controller/delete.py @@ -1,18 +1,28 @@ +""" +This file contains functionality to delete data to the database. +""" + import app.database.controller as dbc from app.core import db from app.database.models import Blacklist, City, Competition, Role, Slide, User def default(item): + """ Deletes item and commits. """ + db.session.delete(item) db.session.commit() def component(item_component): + """ Deletes component. """ + default(item_component) def _slide(item_slide): + """ Internal delete for slide. """ + for item_question in item_slide.questions: question(item_question) @@ -23,6 +33,8 @@ def _slide(item_slide): def slide(item_slide): + """ Deletes slide and updates order of other slides if neccesary. """ + competition_id = item_slide.competition_id slide_order = item_slide.order @@ -38,12 +50,16 @@ def slide(item_slide): def team(item_team): + """ Deletes team and its question answers. """ + for item_question_answer in item_team.question_answers: question_answers(item_question_answer) default(item_team) def question(item_question): + """ Deletes question and its alternatives and answers. """ + for item_question_answer in item_question.question_answers: question_answers(item_question_answer) for item_alternative in item_question.alternatives: @@ -52,14 +68,20 @@ def question(item_question): def alternatives(item_alternatives): + """ Deletes question alternative. """ + default(item_alternatives) def question_answers(item_question_answers): + """ Deletes question answer. """ + default(item_question_answers) def competition(item_competition): + """ Deletes competition and its slides and teams. """ + for item_slide in item_competition.slides: _slide(item_slide) for item_team in item_competition.teams: diff --git a/server/app/database/controller/edit.py b/server/app/database/controller/edit.py index 3afcb4d45c8ba0c0f9cfdaab83d88b286f566215..52471badd4edc43ff503436f4f9dbbb65a580c6c 100644 --- a/server/app/database/controller/edit.py +++ b/server/app/database/controller/edit.py @@ -1,7 +1,13 @@ +""" +This file contains functionality to get data from the database. +""" + from app.core import db def switch_order(item1, item2): + """ Switches order between two slides. """ + old_order = item1.order new_order = item2.order @@ -21,6 +27,8 @@ def switch_order(item1, item2): def component(item, x, y, w, h, data): + """ Edits position, size and content of the provided component. """ + if x: item.x = x if y: @@ -38,6 +46,8 @@ def component(item, x, y, w, h, data): def slide(item, title=None, timer=None): + """ Edits the title and timer of the slide. """ + if title: item.title = title if timer: @@ -49,6 +59,8 @@ def slide(item, title=None, timer=None): def team(item_team, name=None, competition_id=None): + """ Edits the name and competition of the team. """ + if name: item_team.name = name if competition_id: @@ -60,6 +72,8 @@ def team(item_team, name=None, competition_id=None): def competition(item, name=None, year=None, city_id=None): + """ Edits the name and year of the competition. """ + if name: item.name = name if year: @@ -73,6 +87,7 @@ def competition(item, name=None, year=None, city_id=None): def user(item, name=None, email=None, city_id=None, role_id=None): + """ Edits the name, email, city and role of the user. """ if name: item.name = name.title() @@ -92,6 +107,7 @@ def user(item, name=None, email=None, city_id=None, role_id=None): def question(item_question, name=None, total_score=None, type_id=None, slide_id=None): + """ Edits the name, score, type and slide of the question. """ if name: item_question.name = name diff --git a/server/app/database/controller/get.py b/server/app/database/controller/get.py index 269640b3c9af00d03aaa9d1a7d132154f4af66e2..f7d14914957066b6ac90bad09b16745bc9a59262 100644 --- a/server/app/database/controller/get.py +++ b/server/app/database/controller/get.py @@ -1,49 +1,96 @@ +""" +This file contains functionality to get data from the database. +""" + from app.core import db -from app.database.models import (City, Code, Competition, Component, - ComponentType, MediaType, Question, - QuestionType, Role, Slide, Team, User, - ViewType) +from app.core import http_codes as codes +from app.database.models import ( + City, + Code, + Competition, + Component, + ComponentType, + MediaType, + Question, + QuestionType, + Role, + Slide, + Team, + User, + ViewType, +) +from sqlalchemy.orm import contains_eager, joinedload, subqueryload def all(db_type): + """ Gets lazy db-item in the provided table. """ + return db_type.query.all() def one(db_type, id, required=True, error_msg=None): + """ Get lazy db-item in the table that has the same id. """ + return db_type.query.filter(db_type.id == id).first_extended(required, error_msg) def user_exists(email): + """ Checks if an user has that email. """ + return User.query.filter(User.email == email).count() > 0 -def code_by_code(code): - return Code.query.filter(Code.code == code.upper()).first() + +def code_by_code(code, required=True, error_msg=None): + """ Gets the code object associated with the provided code. """ + + return Code.query.filter(Code.code == code.upper()).first_extended(required, error_msg, codes.UNAUTHORIZED) def user(UID, required=True, error_msg=None): + """ Gets the user object associated with the provided id. """ + return User.query.filter(User.id == UID).first_extended(required, error_msg) def user_by_email(email, required=True, error_msg=None): + """ Gets the user object associated with the provided email. """ + return User.query.filter(User.email == email).first_extended(required, error_msg) def slide(CID, SOrder, required=True, error_msg=None): + """ Gets the slide object associated with the provided id and order. """ + filters = (Slide.competition_id == CID) & (Slide.order == SOrder) return Slide.query.filter(filters).first_extended(required, error_msg) def team(CID, TID, required=True, error_msg=None): + """ Gets the team object associated with the provided id and competition id. """ + return Team.query.filter((Team.competition_id == CID) & (Team.id == TID)).first_extended(required, error_msg) def question(CID, SOrder, QID, required=True, error_msg=None): + """ Gets the question object associated with the provided id, slide order and competition id. """ + join_filters = (Slide.competition_id == CID) & (Slide.order == SOrder) & (Slide.id == Question.slide_id) return Question.query.join(Slide, join_filters).filter(Question.id == QID).first_extended(required, error_msg) +def competition(CID): + """ Get Competition and all it's sub-entities """ + """ HOT PATH """ + + os1 = joinedload(Competition.slides).joinedload(Slide.components) + os2 = joinedload(Competition.slides).joinedload(Slide.questions).joinedload(Question.alternatives) + ot = joinedload(Competition.teams).joinedload(Team.question_answers) + return Competition.query.filter(Competition.id == CID).options(os1).options(os2).options(ot).first() + def code_list(competition_id): + """ Gets a list of all code objects associated with a the provided competition. """ + team_view_id = 1 join_filters = (Code.view_type_id == team_view_id) & (Team.id == Code.pointer) filters = ((Code.view_type_id != team_view_id) & (Code.pointer == competition_id))( @@ -53,22 +100,32 @@ def code_list(competition_id): def question_list(CID): + """ Gets a list of all question objects associated with a the provided competition. """ + join_filters = (Slide.competition_id == CID) & (Slide.id == Question.slide_id) return Question.query.join(Slide, join_filters).all() def component_list(CID, SOrder): + """ Gets a list of all component objects associated with a the provided competition id and slide order. """ + join_filters = (Slide.competition_id == CID) & (Slide.order == SOrder) & (Component.slide_id == Slide.id) return Component.query.join(Slide, join_filters).all() def team_list(CID): + """ Gets a list of all team objects associated with a the provided competition. """ + return Team.query.filter(Team.competition_id == CID).all() def slide_list(CID): + """ Gets a list of all slide objects associated with a the provided competition. """ + return Slide.query.filter(Slide.competition_id == CID).all() def slide_count(CID): + """ Gets the number of slides in the provided competition. """ + return Slide.query.filter(Slide.competition_id == CID).count() diff --git a/server/app/database/controller/search.py b/server/app/database/controller/search.py index 466efd01ceab68f316f63299fff73cddff2bfd9d..bfc40843b83675c8758a63f4d117d98ea6b2f609 100644 --- a/server/app/database/controller/search.py +++ b/server/app/database/controller/search.py @@ -1,7 +1,13 @@ +""" +This file contains functionality to find data to the database. +""" + from app.database.models import Competition, Media, Question, Slide, Team, User def image(filename, page=0, page_size=15, 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}%")) @@ -9,7 +15,18 @@ def image(filename, page=0, page_size=15, order=1, order_by=None): return query.pagination(page, page_size, None, None) -def user(email=None, name=None, city_id=None, role_id=None, page=0, page_size=15, order=1, order_by=None): +def user( + 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. """ + query = User.query if name: query = query.filter(User.name.like(f"%{name}%")) @@ -27,7 +44,17 @@ def user(email=None, name=None, city_id=None, role_id=None, page=0, page_size=15 return query.pagination(page, page_size, order_column, order) -def competition(name=None, year=None, city_id=None, page=0, page_size=15, order=1, order_by=None): +def competition( + 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. """ + query = Competition.query if name: query = query.filter(Competition.name.like(f"%{name}%")) @@ -43,7 +70,18 @@ def competition(name=None, year=None, city_id=None, page=0, page_size=15, order= return query.pagination(page, page_size, order_column, order) -def slide(slide_order=None, title=None, body=None, competition_id=None, page=0, page_size=15, order=1, order_by=None): +def slide( + 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. """ + query = Slide.query if slide_order: query = query.filter(Slide.order == slide_order) @@ -72,6 +110,8 @@ def questions( order=1, order_by=None, ): + """ Finds and returns a question from the provided parameters. """ + query = Question.query if name: query = query.filter(Question.name.like(f"%{name}%")) @@ -82,7 +122,10 @@ def questions( if slide_id: query = query.filter(Question.slide_id == slide_id) if competition_id: - query = query.join(Slide, (Slide.competition_id == competition_id) & (Slide.id == Question.slide_id)) + query = query.join( + Slide, + (Slide.competition_id == competition_id) & (Slide.id == Question.slide_id), + ) order_column = Question.id # Default order_by if order_by: diff --git a/server/app/database/controller/utils.py b/server/app/database/controller/utils.py index 61be34cae0520cb3db6ea58a0d23518e4f7af341..4b49e46e777d23228c72f15006f59c80a8029a60 100644 --- a/server/app/database/controller/utils.py +++ b/server/app/database/controller/utils.py @@ -1,9 +1,15 @@ +""" +This file contains some miscellaneous functionality. +""" + from app.core import db from app.core.codes import generate_code_string from app.database.models import Code def generate_unique_code(): + """ Generates a unique competition code. """ + code = generate_code_string() while db.session.query(Code).filter(Code.code == code).count(): code = generate_code_string() @@ -11,13 +17,18 @@ def generate_unique_code(): def commit_and_refresh(item): + """ Commits and refreshes the provided item. """ + db.session.commit() db.session.refresh(item) def refresh(item): + """ Refreshes the provided item. """ + db.session.refresh(item) -def commit(item): +def commit(): + """ Commits. """ db.session.commit() diff --git a/server/app/database/models.py b/server/app/database/models.py index cfedd923b0202ef75517f5d3d1ce7443e60a0471..da9e0620b55bf20c6b2ab11bbc4b3ea4a80fe661 100644 --- a/server/app/database/models.py +++ b/server/app/database/models.py @@ -1,6 +1,7 @@ from app.core import bcrypt, db -from sqlalchemy.ext.hybrid import hybrid_method, hybrid_property from app.database import Dictionary +from sqlalchemy.ext.hybrid import hybrid_method, hybrid_property + STRING_SIZE = 254 @@ -88,7 +89,7 @@ class Competition(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(STRING_SIZE), unique=True) year = db.Column(db.Integer, nullable=False, default=2020) - + font = db.Column(db.String(STRING_SIZE), nullable=False) city_id = db.Column(db.Integer, db.ForeignKey("city.id"), nullable=False) background_image_id = db.Column(db.Integer, db.ForeignKey("media.id"), nullable=True) @@ -101,6 +102,7 @@ class Competition(db.Model): self.name = name self.year = year self.city_id = city_id + self.font = "Calibri" class Team(db.Model): @@ -130,6 +132,7 @@ class Slide(db.Model): background_image = db.relationship("Media", uselist=False) components = db.relationship("Component", backref="slide") + questions = db.relationship("Question", backref="questions") def __init__(self, order, competition_id): self.order = order @@ -144,7 +147,6 @@ class Question(db.Model): type_id = db.Column(db.Integer, db.ForeignKey("question_type.id"), nullable=False) slide_id = db.Column(db.Integer, db.ForeignKey("slide.id"), nullable=False) - slide = db.relationship("Slide", backref="questions") question_answers = db.relationship("QuestionAnswer", backref="question") alternatives = db.relationship("QuestionAlternative", backref="question") @@ -182,9 +184,6 @@ class QuestionAnswer(db.Model): self.team_id = team_id - - - class Component(db.Model): id = db.Column(db.Integer, primary_key=True) x = db.Column(db.Integer, nullable=False, default=0) diff --git a/server/configmodule.py b/server/configmodule.py index 2d525424e891aed9d05ada1d2a75e57bc3171a2a..78537a0e97d293fe5a2688712ea74ab60be72238 100644 --- a/server/configmodule.py +++ b/server/configmodule.py @@ -13,14 +13,21 @@ class Config: JWT_BLACKLIST_TOKEN_CHECKS = ["access", "refresh"] JWT_ACCESS_TOKEN_EXPIRES = timedelta(days=2) JWT_REFRESH_TOKEN_EXPIRES = timedelta(days=30) - UPLOADED_PHOTOS_DEST = "static/images" # os.getcwd() + UPLOADED_PHOTOS_DEST = os.path.join(os.getcwd(), "app/static/images") + THUMBNAIL_SIZE = (120, 120) SECRET_KEY = os.urandom(24) SQLALCHEMY_ECHO = False class DevelopmentConfig(Config): DEBUG = True - SQLALCHEMY_DATABASE_URI = "sqlite:///database.db" + # HOST = "localhost" + # PORT = 5432 + # USER = "postgres" + # PASSWORD = "password" + # DATABASE = "teknik8" + # SQLALCHEMY_DATABASE_URI = "sqlite:///database.db" + # SQLALCHEMY_DATABASE_URI = "postgresql://" + USER + ":" + PASSWORD + "@" + HOST + ":" + str(PORT) + "/" + DATABASE SQLALCHEMY_ECHO = False @@ -33,7 +40,7 @@ class ProductionConfig(Config): SQLALCHEMY_DATABASE_URI = "sqlite:///database.db" # HOST = 'postgresql' # PORT = 5432 - # USER = 'asd' - # PASSWORD = 'asd' - # DATABASE = 'asd' - # DATABASE_URI = 'postgresql://'+USER+":"+PASSWORD+"@"+HOST+":"+str(PORT)+"/"+DATABASE + # USER = 'postgres' + # PASSWORD = 'password' + # DATABASE = 'teknik8' + # SQLALCHEMY_DATABASE_URI = 'postgresql://'+USER+":"+PASSWORD+"@"+HOST+":"+str(PORT)+"/"+DATABASE diff --git a/server/main.py b/server/main.py index cb6168551e98b6f67ca19bc25ecf64f7a6614659..bf4a239311a451132d5c1dfb2b262ce29f4ccb32 100644 --- a/server/main.py +++ b/server/main.py @@ -1,22 +1,5 @@ -from app import create_app, db - -# Development port -DEFAULT_DEV_PORT = 5000 - -# Production port -DEFAULT_PRO_PORT = 8080 +from app import create_app if __name__ == "__main__": - app = create_app("configmodule.DevelopmentConfig") - with app.app_context(): - db.create_all() - app.run(port=5000) - # CONFIG = "configmodule.DevelopmentConfig" - - # if "production-teknik8" in os.environ: - # CONFIG = "configmodule.ProductionConfig" - - # if "configmodule.DevelopmentConfig" == CONFIG: - # app.run(port=DEFAULT_DEV_PORT) - # else: - # app.run(host="0.0.0.0", port=DEFAULT_PRO_PORT) + app, sio = create_app("configmodule.DevelopmentConfig") + sio.run(app, port=5000) diff --git a/server/populate.py b/server/populate.py index bebcaa8b098cdffeaf070b71eb0570cd359e5bf9..7fb6a37011c452d804b325dae6182f206944a87d 100644 --- a/server/populate.py +++ b/server/populate.py @@ -7,7 +7,7 @@ def _add_items(): media_types = ["Image", "Video"] question_types = ["Boolean", "Multiple", "Text"] component_types = ["Text", "Image"] - view_types = ["Team", "Judge", "Audience"] + view_types = ["Team", "Judge", "Audience", "Operator"] roles = ["Admin", "Editor"] cities = ["Linköping", "Stockholm", "Norrköping", "Örkelljunga"] @@ -50,6 +50,8 @@ def _add_items(): for item_comp in item_comps: for item_slide in item_comp.slides: + dbc.edit.slide(item_slide, timer=5, title="test-slide-title") + for i in range(3): dbc.add.question(f"Q{i+1}", i + 1, text_id, item_slide) @@ -59,7 +61,7 @@ def _add_items(): if __name__ == "__main__": - app = create_app("configmodule.DevelopmentConfig") + app, _ = create_app("configmodule.DevelopmentConfig") with app.app_context(): db.drop_all() diff --git a/server/requirements.txt b/server/requirements.txt index cbda8a7040167ce59a2209747e8d7d4204f7fe2d..bda47a88036c81470bc7a6b2112b2ecedd904ed7 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 0b0deaf0c57b8faae6fa76a21b35cb394c049afa..c5b8f20d24cbfabe4d6c87f66a3e9b893a51d23d 100644 --- a/server/tests/__init__.py +++ b/server/tests/__init__.py @@ -4,7 +4,7 @@ from app import create_app, db @pytest.fixture def app(): - app = create_app("configmodule.TestingConfig") + app, _ = create_app("configmodule.TestingConfig") """ with app.app_context(): diff --git a/server/tests/test_app.py b/server/tests/test_app.py index 948b9ae42827cec8d14d4bb247fb04a4c4863f5f..467fd0596269bbb875262c9b1be74f45807ed494 100644 --- a/server/tests/test_app.py +++ b/server/tests/test_app.py @@ -143,13 +143,13 @@ def test_auth_and_user_api(client): response, body = put(client, "/api/users", {"name": "carl carlsson", "city_id": 2, "role_id": 1}, headers=headers) assert response.status_code == codes.OK assert body["name"] == "Carl Carlsson" - assert body["city"]["id"] == 2 and body["role"]["id"] == 1 + assert body["city_id"] == 2 and body["role_id"] == 1 # Find other user response, body = get( client, "/api/users/search", - query_string={"name": "Olle Olsson", "email": "test@test.se", "role_id": 1, "city_id": 1}, + query_string={"name": "Carl Carlsson"}, headers=headers, ) assert response.status_code == codes.OK @@ -162,17 +162,22 @@ def test_auth_and_user_api(client): assert response.status_code == codes.OK assert searched_user["name"] == body["name"] assert searched_user["email"] == body["email"] - assert searched_user["role_id"] == body["role"]["id"] - assert searched_user["city_id"] == body["city"]["id"] + assert searched_user["role_id"] == body["role_id"] + assert searched_user["city_id"] == body["city_id"] assert searched_user["id"] == body["id"] + # Login as admin + response, body = post(client, "/api/auth/login", {"email": "test@test.se", "password": "password"}) + assert response.status_code == 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 body["email"] == "carl@carlsson.test" + # 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": "test1@test.se"}, headers=headers) + response, body = put(client, f"/api/users/{user_id}", {"email": "test@test.se"}, headers=headers) assert response.status_code == codes.BAD_REQUEST # Delete other user @@ -193,25 +198,25 @@ def test_auth_and_user_api(client): assert response.status_code == 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 - 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}) + # response, body = post(client, "/api/auth/login", {"email": "test1@test.se", "password": "abc123"}) # assert response.status_code == codes.OK + # headers = {"Authorization": "Bearer " + body["access_token"]} - # Find current user - response, body = get(client, "/api/users", headers=headers) - assert response.status_code == codes.OK - assert body["email"] == "test1@test.se" - assert body["city"]["id"] == 2 - assert body["role"]["id"] == 1 + # # 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 - # 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 + # # Find current user + # response, body = get(client, "/api/users", headers=headers) + # assert response.status_code == codes.OK + # assert body["email"] == "test1@test.se" + # assert body["city_id"] == 2 + # assert body["role_id"] == 1 + + # # 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 # TODO: Check that user was blacklisted # Look for current users jwt in blacklist @@ -332,7 +337,7 @@ def test_question_api(client): num_questions = 4 assert response.status_code == codes.OK assert item_question["name"] == name - assert item_question["type"]["id"] == type_id + assert item_question["type_id"] == type_id # Checks number of questions response, body = get(client, f"/api/competitions/{CID}/questions", headers=headers)