Admin: sports edition

This commit is contained in:
Sam 2018-04-30 20:08:18 +02:00
parent b89b40a4de
commit b69a55362d
13 changed files with 445 additions and 85 deletions

View File

@ -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 ..users.utils import authenticate
from .models import Activity, Sport from .models import Activity, Sport
@ -10,7 +12,7 @@ activities_blueprint = Blueprint('activities', __name__)
@authenticate @authenticate
def get_sports(auth_user_id): def get_sports(auth_user_id):
"""Get all sports""" """Get all sports"""
sports = Sport.query.all() sports = Sport.query.order_by(Sport.id).all()
sports_list = [] sports_list = []
for sport in sports: for sport in sports:
sport_object = { sport_object = {
@ -27,6 +29,83 @@ def get_sports(auth_user_id):
return jsonify(response_object), 200 return jsonify(response_object), 200
@activities_blueprint.route('/sports/<int:sport_id>', 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/<int:sport_id>', 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']) @activities_blueprint.route('/activities', methods=['GET'])
@authenticate @authenticate
def get_activities(auth_user_id): def get_activities(auth_user_id):

View File

@ -1,7 +1,7 @@
import datetime import datetime
import json 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): 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 2 == data['data']['activities'][1]['sport_id']
assert 3600 == data['data']['activities'][0]['duration'] assert 3600 == data['data']['activities'][0]['duration']
assert 1024 == data['data']['activities'][1]['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']

View File

@ -5,6 +5,18 @@ from mpwo_api.activities.models import Activity, Sport
from mpwo_api.users.models import User 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): def add_user(username, email, password):
user = User(username=username, email=email, password=password) user = User(username=username, email=email, password=password)
db.session.add(user) db.session.add(user)

View File

@ -7,25 +7,43 @@ export const setData = (target, data) => ({
target, target,
}) })
export const setError = (target, error) => ({ export const setError = message => ({
type: 'SET_ERROR', type: 'SET_ERROR',
error, message,
target,
}) })
export function getData(target) { export function getData(target, id = null) {
return function(dispatch) { return function(dispatch) {
if (id !== null && isNaN(id)) {
return dispatch(setError(target, `${target}: Incorrect id`))
}
return mpwoApi return mpwoApi
.getData(target) .getData(target, id)
.then(ret => { .then(ret => {
if (ret.status === 'success') { if (ret.status === 'success') {
dispatch(setData(target, ret.data)) dispatch(setData(target, ret.data))
} else { } else {
dispatch(setError(target, ret.message)) dispatch(setError(`${target}: ${ret.status}`))
} }
}) })
.catch(error => { .catch(error => dispatch(setError(`${target}: ${error}`)))
throw 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}`)))
} }
} }

View File

@ -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 (
<div>
<Helmet>
<title>mpwo - Admin</title>
</Helmet>
{error && (
<code>{error}</code>
)}
<h1 className="page-title">
Administration - {title}
</h1>
<div className="container">
<div className="row">
<div className="col-md-2" />
<div className="col-md-8 card">
<table className="table">
<thead>
<tr>
{tbKeys.map(
tbKey => <th key={tbKey} scope="col">{tbKey}</th>
)}
</tr>
</thead>
<tbody>
{ results.map((result, idx) => (
<tr key={idx}>
{ Object.keys(result).map(key => {
if (key === 'id') {
return <th key={key} scope="row">{result[key]}</th>
}
return <td key={key}>{result[key]}</td>
}) }
</tr>
))}
</tbody>
</table>
</div>
<div className="col-md-2" />
</div>
</div>
</div>
)
}

View File

@ -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 (
<div>
<AdminDetail
results={sports}
target="sports"
/>
</div>
)
}
}
export default connect(
state => ({
sports: state.sports.data,
user: state.user,
}),
dispatch => ({
loadSport: sportId => {
dispatch(getData('sports', sportId))
},
})
)(AdminSports)

View File

@ -2,18 +2,20 @@ import React from 'react'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { getData } from '../../actions/index' import { getData } from '../../actions/index'
import AdminPage from './AdminPage' import AdminPage from './generic/AdminPage'
class AdminSports extends React.Component { class AdminSports extends React.Component {
componentDidMount() { componentDidMount() {
this.props.loadSport() this.props.loadSports()
} }
render() { render() {
const { sports } = this.props const { sports } = this.props
return ( return (
<div> <div>
<AdminPage <AdminPage
data={sports} data={sports}
detailLink="sport"
target="sports" target="sports"
/> />
</div> </div>
@ -27,7 +29,7 @@ export default connect(
user: state.user, user: state.user,
}), }),
dispatch => ({ dispatch => ({
loadSport: () => { loadSports: () => {
dispatch(getData('sports')) dispatch(getData('sports'))
}, },
}) })

View File

@ -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 (
<div>
<Helmet>
<title>mpwo - Admin</title>
</Helmet>
<h1 className="page-title">
Administration - {title}
</h1>
{message ? (
<code>{message}</code>
) : (
results.length === 1 && (
<div className="container">
<div className="row">
<div className="col-md-2" />
<div className="col-md-8 card">
<div className="card-body">
<form onSubmit={event =>
event.preventDefault()}
>
{ results.map(result => (
Object.keys(result).map(key => (
<div className="form-group" key={key}>
<label>
{key}:
<input
className="form-control input-lg"
name={key}
readOnly={key === 'id' || !isInEdition}
defaultValue={result[key]}
/>
</label>
</div>
))
))}
{isInEdition ? (
<div>
<input
type="submit"
className="btn btn-primary btn-lg btn-block"
onClick={event => {
onDataUpdate(event, target)
this.setState({ isInEdition: false })
}
}
value="Submit"
/>
<input
type="submit"
className="btn btn-secondary btn-lg btn-block"
onClick={event => {
event.target.form.reset()
this.setState({ isInEdition: false })
}}
value="Cancel"
/>
</div>
) : (
<div>
<input
type="submit"
className="btn btn-primary btn-lg btn-block"
onClick={() => this.setState({ isInEdition: true })}
value="Edit"
/>
<input
type="submit"
className="btn btn-danger btn-lg btn-block"
value="Delete"
/>
</div>
)}
</form>
<Link to={`/admin/${target}`}>Back to the list</Link>
</div>
</div>
<div className="col-md-2" />
</div>
</div>
)
)}
</div>
)
}
}
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)

View File

@ -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 (
<div>
<Helmet>
<title>mpwo - Admin</title>
</Helmet>
<h1 className="page-title">
Administration - {title}
</h1>
{error ? (
<code>{error}</code>
) : (
<div className="container">
<div className="row">
<div className="col-md-2" />
<div className="col-md-8 card">
<table className="table">
<thead>
<tr>
{tbKeys.map(
tbKey => <th key={tbKey} scope="col">{tbKey}</th>
)}
</tr>
</thead>
<tbody>
{ results.map((result, idx) => (
<tr key={idx}>
{ Object.keys(result).map(key => {
if (key === 'id') {
return (
<th key={key} scope="row">
<Link to={`/admin/${detailLink}/${result[key]}`}>
{result[key]}
</Link>
</th>
)
}
return <td key={key}>{result[key]}</td>
})
}
</tr>
))}
</tbody>
</table>
</div>
<div className="col-md-2" />
</div>
</div>
)}
</div>
)
}

View File

@ -4,6 +4,7 @@ import { connect } from 'react-redux'
import { Redirect, Route, Switch } from 'react-router-dom' import { Redirect, Route, Switch } from 'react-router-dom'
import AdminMenu from './AdminMenu' import AdminMenu from './AdminMenu'
import AdminSport from './AdminSport'
import AdminSports from './AdminSports' import AdminSports from './AdminSports'
import AccessDenied from './../Others/AccessDenied' import AccessDenied from './../Others/AccessDenied'
import NotFound from './../Others/NotFound' import NotFound from './../Others/NotFound'
@ -22,7 +23,8 @@ class Admin extends React.Component {
user.isAdmin ? ( user.isAdmin ? (
<Switch> <Switch>
<Route exact path="/admin" component={AdminMenu} /> <Route exact path="/admin" component={AdminMenu} />
<Route path="/admin/sports" component={AdminSports} /> <Route exact path="/admin/sports" component={AdminSports} />
<Route path="/admin/sport" component={AdminSport} />
<Route component={NotFound} /> <Route component={NotFound} />
</Switch> </Switch>
) : ( ) : (

View File

@ -2,8 +2,8 @@ import { apiUrl } from '../utils'
export default class MpwoApi { export default class MpwoApi {
static getData(target) { static getData(target, id = null) {
const request = new Request(`${apiUrl}${target}`, { const request = new Request(`${apiUrl}${target}${id ? `/${id}` : ''}`, {
method: 'GET', method: 'GET',
headers: new Headers({ headers: new Headers({
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@ -14,4 +14,18 @@ export default class MpwoApi {
.then(response => response.json()) .then(response => response.json())
.catch(error => error) .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)
}
} }

View File

@ -13,13 +13,6 @@ const handleDataAndError = (state, type, action) => {
return { return {
...state, ...state,
data: action.data[action.target], data: action.data[action.target],
error: null,
}
case 'SET_ERROR':
return {
...state,
data: { ...initial[type].data },
error: action.error,
} }
default: default:
return state return state
@ -76,9 +69,11 @@ const message = (state = initial.message, action) => {
case 'PROFILE_ERROR': case 'PROFILE_ERROR':
case 'PROFILE_UPDATE_ERROR': case 'PROFILE_UPDATE_ERROR':
case 'PICTURE_ERROR': case 'PICTURE_ERROR':
case 'SET_ERROR':
return action.message return action.message
case 'LOGOUT': case 'LOGOUT':
case 'PROFILE_SUCCESS': case 'PROFILE_SUCCESS':
case 'SET_RESULTS':
case '@@router/LOCATION_CHANGE': case '@@router/LOCATION_CHANGE':
return '' return ''
default: default:

View File

@ -1,6 +1,5 @@
const emptyData = { const emptyData = {
data: [], data: [],
error: null,
} }
export default { export default {