diff --git a/mpwo_api/mpwo_api/activities/activities.py b/mpwo_api/mpwo_api/activities/activities.py index c9b6bc3c..23750a06 100644 --- a/mpwo_api/mpwo_api/activities/activities.py +++ b/mpwo_api/mpwo_api/activities/activities.py @@ -1,4 +1,6 @@ -from flask import Blueprint, jsonify +from flask import Blueprint, jsonify, request +from mpwo_api import appLog, db +from sqlalchemy import exc from ..users.utils import authenticate from .models import Activity, Sport @@ -10,7 +12,7 @@ activities_blueprint = Blueprint('activities', __name__) @authenticate def get_sports(auth_user_id): """Get all sports""" - sports = Sport.query.all() + sports = Sport.query.order_by(Sport.id).all() sports_list = [] for sport in sports: sport_object = { @@ -27,6 +29,83 @@ def get_sports(auth_user_id): return jsonify(response_object), 200 +@activities_blueprint.route('/sports/', methods=['GET']) +@authenticate +def get_sport(auth_user_id, sport_id): + """Get a sport""" + sport = Sport.query.filter_by(id=sport_id).first() + sports_list = [] + if sport: + sports_list.append({ + 'id': sport.id, + 'label': sport.label + }) + response_object = { + 'status': 'success', + 'data': { + 'sports': sports_list + } + } + code = 200 + else: + response_object = { + 'status': 'not found', + 'data': { + 'sports': sports_list + } + } + code = 404 + return jsonify(response_object), code + + +@activities_blueprint.route('/sports/', methods=['PATCH']) +@authenticate +def update_sport(auth_user_id, sport_id): + """Update a sport""" + sport_data = request.get_json() + if not sport_data or sport_data.get('label') is None: + response_object = { + 'status': 'error', + 'message': 'Invalid payload.' + } + return jsonify(response_object), 400 + + sports_list = [] + try: + sport = Sport.query.filter_by(id=sport_id).first() + if sport: + sport.label = sport_data.get('label') + db.session.commit() + sports_list.append({ + 'id': sport.id, + 'label': sport.label + }) + response_object = { + 'status': 'success', + 'data': { + 'sports': sports_list + } + } + code = 200 + else: + response_object = { + 'status': 'not found', + 'data': { + 'sports': sports_list + } + } + code = 404 + except (exc.IntegrityError, exc.OperationalError, ValueError) as e: + db.session.rollback() + appLog.error(e) + response_object = { + 'status': 'error', + 'message': 'Error. Please try again or contact the administrator.' + } + code = 500 + return jsonify(response_object), code + + @activities_blueprint.route('/activities', methods=['GET']) @authenticate def get_activities(auth_user_id): diff --git a/mpwo_api/mpwo_api/tests/test_activities.py b/mpwo_api/mpwo_api/tests/test_activities.py index a99876eb..8f2225cd 100644 --- a/mpwo_api/mpwo_api/tests/test_activities.py +++ b/mpwo_api/mpwo_api/tests/test_activities.py @@ -1,7 +1,7 @@ import datetime import json -from mpwo_api.tests.utils import add_activity, add_sport, add_user +from mpwo_api.tests.utils import add_activity, add_admin, add_sport, add_user def test_get_all_sports(app): @@ -85,3 +85,67 @@ def test_get_all_activities(app): assert 2 == data['data']['activities'][1]['sport_id'] assert 3600 == data['data']['activities'][0]['duration'] assert 1024 == data['data']['activities'][1]['duration'] + + +def test_get_a_sport(app): + add_user('test', 'test@test.com', '12345678') + add_sport('cycling') + + client = app.test_client() + resp_login = client.post( + '/api/auth/login', + data=json.dumps(dict( + email='test@test.com', + password='12345678' + )), + content_type='application/json' + ) + response = client.get( + '/api/sports/1', + headers=dict( + Authorization='Bearer ' + json.loads( + resp_login.data.decode() + )['auth_token'] + ) + ) + data = json.loads(response.data.decode()) + + assert response.status_code == 200 + assert 'success' in data['status'] + + assert len(data['data']['sports']) == 1 + assert 'cycling' in data['data']['sports'][0]['label'] + + +def test_update_a_sport(app): + add_admin() + add_sport('cycling') + + client = app.test_client() + resp_login = client.post( + '/api/auth/login', + data=json.dumps(dict( + email='admin@example.com', + password='12345678' + )), + content_type='application/json' + ) + response = client.patch( + '/api/sports/1', + content_type='application/json', + data=json.dumps(dict( + label='cycling updated' + )), + headers=dict( + Authorization='Bearer ' + json.loads( + resp_login.data.decode() + )['auth_token'] + ) + ) + data = json.loads(response.data.decode()) + + assert response.status_code == 200 + assert 'success' in data['status'] + + assert len(data['data']['sports']) == 1 + assert 'cycling updated' in data['data']['sports'][0]['label'] diff --git a/mpwo_api/mpwo_api/tests/utils.py b/mpwo_api/mpwo_api/tests/utils.py index e3a61682..574c055b 100644 --- a/mpwo_api/mpwo_api/tests/utils.py +++ b/mpwo_api/mpwo_api/tests/utils.py @@ -5,6 +5,18 @@ from mpwo_api.activities.models import Activity, Sport from mpwo_api.users.models import User +def add_admin(): + admin = User( + username="admin", + email="admin@example.com", + password="12345678" + ) + admin.admin = True + db.session.add(admin) + db.session.commit() + return admin + + def add_user(username, email, password): user = User(username=username, email=email, password=password) db.session.add(user) diff --git a/mpwo_client/src/actions/index.js b/mpwo_client/src/actions/index.js index 632f8be2..595fe968 100644 --- a/mpwo_client/src/actions/index.js +++ b/mpwo_client/src/actions/index.js @@ -7,25 +7,43 @@ export const setData = (target, data) => ({ target, }) -export const setError = (target, error) => ({ +export const setError = message => ({ type: 'SET_ERROR', - error, - target, + message, }) -export function getData(target) { +export function getData(target, id = null) { return function(dispatch) { + if (id !== null && isNaN(id)) { + return dispatch(setError(target, `${target}: Incorrect id`)) + } return mpwoApi - .getData(target) + .getData(target, id) .then(ret => { if (ret.status === 'success') { dispatch(setData(target, ret.data)) } else { - dispatch(setError(target, ret.message)) + dispatch(setError(`${target}: ${ret.status}`)) } }) - .catch(error => { - throw error - }) + .catch(error => dispatch(setError(`${target}: ${error}`))) + } +} + +export function updateData(target, data) { + return function(dispatch) { + if (isNaN(data.id)) { + return dispatch(setError(target, `${target}: Incorrect id`)) + } + return mpwoApi + .updateData(target, data) + .then(ret => { + if (ret.status === 'success') { + dispatch(setData(target, ret.data)) + } else { + dispatch(setError(`${target}: ${ret.status}`)) + } + }) + .catch(error => dispatch(setError(`${target}: ${error}`))) } } diff --git a/mpwo_client/src/components/Admin/AdminPage.jsx b/mpwo_client/src/components/Admin/AdminPage.jsx deleted file mode 100644 index 2e2bb666..00000000 --- a/mpwo_client/src/components/Admin/AdminPage.jsx +++ /dev/null @@ -1,59 +0,0 @@ -import React from 'react' -import { Helmet } from 'react-helmet' - - -export default function AdminPage(props) { - - const { target, data } = props - const error = data.error - const results = data.data - const tbKeys = [] - if (results.length > 0) { - Object.keys(results[0]).map(key => tbKeys.push(key)) - } - const title = target.charAt(0).toUpperCase() + target.slice(1) - - return ( -
- - mpwo - Admin - - {error && ( - {error} - )} -

- Administration - {title} -

-
-
-
-
- - - - - {tbKeys.map( - tbKey => - )} - - - - { results.map((result, idx) => ( - - { Object.keys(result).map(key => { - if (key === 'id') { - return - } - return - }) } - - ))} - -
{tbKey}
{result[key]}{result[key]}
-
-
-
-
-
- ) -} diff --git a/mpwo_client/src/components/Admin/AdminSport.jsx b/mpwo_client/src/components/Admin/AdminSport.jsx new file mode 100644 index 00000000..c50b8b04 --- /dev/null +++ b/mpwo_client/src/components/Admin/AdminSport.jsx @@ -0,0 +1,37 @@ +import React from 'react' +import { connect } from 'react-redux' + +import { getData } from '../../actions/index' +import AdminDetail from './generic/AdminDetail' + +class AdminSports extends React.Component { + componentDidMount() { + this.props.loadSport( + this.props.location.pathname.replace('/admin/sport/', '') + ) + } + render() { + const { sports } = this.props + + return ( +
+ +
+ ) + } +} + +export default connect( + state => ({ + sports: state.sports.data, + user: state.user, + }), + dispatch => ({ + loadSport: sportId => { + dispatch(getData('sports', sportId)) + }, + }) +)(AdminSports) diff --git a/mpwo_client/src/components/Admin/AdminSports.jsx b/mpwo_client/src/components/Admin/AdminSports.jsx index 9309be7a..b0ff6bf9 100644 --- a/mpwo_client/src/components/Admin/AdminSports.jsx +++ b/mpwo_client/src/components/Admin/AdminSports.jsx @@ -2,18 +2,20 @@ import React from 'react' import { connect } from 'react-redux' import { getData } from '../../actions/index' -import AdminPage from './AdminPage' +import AdminPage from './generic/AdminPage' class AdminSports extends React.Component { componentDidMount() { - this.props.loadSport() + this.props.loadSports() } render() { const { sports } = this.props + return (
@@ -27,7 +29,7 @@ export default connect( user: state.user, }), dispatch => ({ - loadSport: () => { + loadSports: () => { dispatch(getData('sports')) }, }) diff --git a/mpwo_client/src/components/Admin/generic/AdminDetail.jsx b/mpwo_client/src/components/Admin/generic/AdminDetail.jsx new file mode 100644 index 00000000..92e03353 --- /dev/null +++ b/mpwo_client/src/components/Admin/generic/AdminDetail.jsx @@ -0,0 +1,130 @@ +import React from 'react' +import { Helmet } from 'react-helmet' +import { connect } from 'react-redux' +import { Link } from 'react-router-dom' + +import { updateData } from '../../../actions/index' + +class AdminDetail extends React.Component { + + constructor(props, context) { + super(props, context) + this.state = { + isInEdition: false, + } + } + + render() { + const { + message, + onDataUpdate, + results, + target, + } = this.props + const { isInEdition } = this.state + const title = target.charAt(0).toUpperCase() + target.slice(1) + + return ( +
+ + mpwo - Admin + +

+ Administration - {title} +

+ {message ? ( + {message} + ) : ( + results.length === 1 && ( +
+
+
+
+
+
+ event.preventDefault()} + > + { results.map(result => ( + Object.keys(result).map(key => ( +
+ +
+ )) + ))} + {isInEdition ? ( +
+ { + onDataUpdate(event, target) + this.setState({ isInEdition: false }) + } + } + value="Submit" + /> + { + event.target.form.reset() + this.setState({ isInEdition: false }) + }} + value="Cancel" + /> +
+ ) : ( +
+ this.setState({ isInEdition: true })} + value="Edit" + /> + +
+ )} +
+ Back to the list +
+
+
+
+
+ ) + )} +
+ ) + } +} + +export default connect( + state => ({ + message: state.message, + }), + dispatch => ({ + onDataUpdate: (e, target) => { + const data = [].slice + .call(e.target.form.elements) + .reduce(function(map, obj) { + if (obj.name) { + map[obj.name] = obj.value + } + return map + }, {}) + dispatch(updateData(target, data)) + }, + }) +)(AdminDetail) diff --git a/mpwo_client/src/components/Admin/generic/AdminPage.jsx b/mpwo_client/src/components/Admin/generic/AdminPage.jsx new file mode 100644 index 00000000..7fa0c359 --- /dev/null +++ b/mpwo_client/src/components/Admin/generic/AdminPage.jsx @@ -0,0 +1,67 @@ +import React from 'react' +import { Helmet } from 'react-helmet' +import { Link } from 'react-router-dom' + +export default function AdminPage(props) { + + const { data, detailLink, target } = props + const { error } = data + const results = data.data + const tbKeys = [] + if (results.length > 0) { + Object.keys(results[0]).map(key => tbKeys.push(key)) + } + const title = target.charAt(0).toUpperCase() + target.slice(1) + + return ( +
+ + mpwo - Admin + +

+ Administration - {title} +

+ {error ? ( + {error} + ) : ( +
+
+
+
+ + + + {tbKeys.map( + tbKey => + )} + + + + { results.map((result, idx) => ( + + { Object.keys(result).map(key => { + if (key === 'id') { + return ( + + ) + } + return + }) + } + + ))} + +
{tbKey}
+ + {result[key]} + + {result[key]}
+
+
+
+
+ )} + +
+ ) +} diff --git a/mpwo_client/src/components/Admin/index.jsx b/mpwo_client/src/components/Admin/index.jsx index 73285662..97316769 100644 --- a/mpwo_client/src/components/Admin/index.jsx +++ b/mpwo_client/src/components/Admin/index.jsx @@ -4,6 +4,7 @@ import { connect } from 'react-redux' import { Redirect, Route, Switch } from 'react-router-dom' import AdminMenu from './AdminMenu' +import AdminSport from './AdminSport' import AdminSports from './AdminSports' import AccessDenied from './../Others/AccessDenied' import NotFound from './../Others/NotFound' @@ -22,7 +23,8 @@ class Admin extends React.Component { user.isAdmin ? ( - + + ) : ( diff --git a/mpwo_client/src/mwpoApi/index.js b/mpwo_client/src/mwpoApi/index.js index 7a30a908..7c0748e4 100644 --- a/mpwo_client/src/mwpoApi/index.js +++ b/mpwo_client/src/mwpoApi/index.js @@ -2,8 +2,8 @@ import { apiUrl } from '../utils' export default class MpwoApi { - static getData(target) { - const request = new Request(`${apiUrl}${target}`, { + static getData(target, id = null) { + const request = new Request(`${apiUrl}${target}${id ? `/${id}` : ''}`, { method: 'GET', headers: new Headers({ 'Content-Type': 'application/json', @@ -14,4 +14,18 @@ export default class MpwoApi { .then(response => response.json()) .catch(error => error) } + + static updateData(target, data) { + const request = new Request(`${apiUrl}${target}/${data.id}`, { + method: 'PATCH', + headers: new Headers({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${window.localStorage.getItem('authToken')}`, + }), + body: JSON.stringify(data) + }) + return fetch(request) + .then(response => response.json()) + .catch(error => error) + } } diff --git a/mpwo_client/src/reducers/index.js b/mpwo_client/src/reducers/index.js index d290edbc..e9ca0776 100644 --- a/mpwo_client/src/reducers/index.js +++ b/mpwo_client/src/reducers/index.js @@ -13,13 +13,6 @@ const handleDataAndError = (state, type, action) => { return { ...state, data: action.data[action.target], - error: null, - } - case 'SET_ERROR': - return { - ...state, - data: { ...initial[type].data }, - error: action.error, } default: return state @@ -76,9 +69,11 @@ const message = (state = initial.message, action) => { case 'PROFILE_ERROR': case 'PROFILE_UPDATE_ERROR': case 'PICTURE_ERROR': + case 'SET_ERROR': return action.message case 'LOGOUT': case 'PROFILE_SUCCESS': + case 'SET_RESULTS': case '@@router/LOCATION_CHANGE': return '' default: diff --git a/mpwo_client/src/reducers/initial.js b/mpwo_client/src/reducers/initial.js index dbc8988b..8d577362 100644 --- a/mpwo_client/src/reducers/initial.js +++ b/mpwo_client/src/reducers/initial.js @@ -1,6 +1,5 @@ const emptyData = { data: [], - error: null, } export default {