API & Client - sport administration & refactor - #15
This commit is contained in:
@ -60,7 +60,7 @@ class Sport(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||
label = db.Column(db.String(50), unique=True, nullable=False)
|
||||
img = db.Column(db.String(255), unique=True, nullable=True)
|
||||
is_default = db.Column(db.Boolean, default=False, nullable=False)
|
||||
is_active = db.Column(db.Boolean, default=True, nullable=False)
|
||||
activities = db.relationship(
|
||||
'Activity', lazy=True, backref=db.backref('sports', lazy='joined')
|
||||
)
|
||||
@ -79,8 +79,10 @@ class Sport(db.Model):
|
||||
'id': self.id,
|
||||
'label': self.label,
|
||||
'img': self.img,
|
||||
'_can_be_deleted': len(self.activities) == 0
|
||||
and not self.is_default,
|
||||
'is_active': self.is_active,
|
||||
'_can_be_disabled': not (
|
||||
len(self.activities) > 0 and self.is_active
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
|
@ -32,39 +32,45 @@ def get_sports(auth_user_id):
|
||||
"data": {
|
||||
"sports": [
|
||||
{
|
||||
"_can_be_deleted": false,
|
||||
"_can_be_disabled": false,
|
||||
"id": 1,
|
||||
"img": "/img/sports/cycling-sport.png",
|
||||
"is_active": true,
|
||||
"label": "Cycling (Sport)"
|
||||
},
|
||||
{
|
||||
"_can_be_deleted": false,
|
||||
"_can_be_disabled": false,
|
||||
"id": 2,
|
||||
"img": "/img/sports/cycling-transport.png",
|
||||
"is_active": true,
|
||||
"label": "Cycling (Transport)"
|
||||
},
|
||||
{
|
||||
"_can_be_deleted": false,
|
||||
"_can_be_disabled": false,
|
||||
"id": 3,
|
||||
"img": "/img/sports/hiking.png",
|
||||
"is_active": true,
|
||||
"label": "Hiking"
|
||||
},
|
||||
{
|
||||
"_can_be_deleted": false,
|
||||
"_can_be_disabled": false,
|
||||
"id": 4,
|
||||
"img": "/img/sports/mountain-biking.png",
|
||||
"is_active": true,
|
||||
"label": "Mountain Biking"
|
||||
},
|
||||
{
|
||||
"_can_be_deleted": false,
|
||||
"_can_be_disabled": false,
|
||||
"id": 5,
|
||||
"img": "/img/sports/running.png",
|
||||
"is_active": true,
|
||||
"label": "Running"
|
||||
},
|
||||
{
|
||||
"_can_be_deleted": false,
|
||||
"_can_be_disabled": false,
|
||||
"id": 6,
|
||||
"img": "/img/sports/walking.png",
|
||||
"is_active": true,
|
||||
"label": "Walking"
|
||||
}
|
||||
]
|
||||
@ -117,9 +123,10 @@ def get_sport(auth_user_id, sport_id):
|
||||
"data": {
|
||||
"sports": [
|
||||
{
|
||||
"_can_be_deleted": false,
|
||||
"_can_be_disabled": false,
|
||||
"id": 1,
|
||||
"img": "/img/sports/cycling-sport.png",
|
||||
"is_active": true,
|
||||
"label": "Cycling (Sport)"
|
||||
}
|
||||
]
|
||||
@ -168,64 +175,101 @@ def get_sport(auth_user_id, sport_id):
|
||||
return jsonify(response_object), code
|
||||
|
||||
|
||||
# no administration - no documentation for now
|
||||
|
||||
|
||||
@sports_blueprint.route('/sports', methods=['POST'])
|
||||
@authenticate_as_admin
|
||||
def post_sport(auth_user_id):
|
||||
"""Post 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
|
||||
|
||||
try:
|
||||
new_sport = Sport(label=sport_data.get('label'))
|
||||
db.session.add(new_sport)
|
||||
db.session.commit()
|
||||
response_object = {
|
||||
'status': 'created',
|
||||
'data': {'sports': [new_sport.serialize()]},
|
||||
}
|
||||
code = 201
|
||||
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
|
||||
|
||||
|
||||
@sports_blueprint.route('/sports/<int:sport_id>', methods=['PATCH'])
|
||||
@authenticate_as_admin
|
||||
def update_sport(auth_user_id, sport_id):
|
||||
"""Update a sport"""
|
||||
"""Update a sport
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
PATCH /api/sports/1 HTTP/1.1
|
||||
Content-Type: application/json
|
||||
|
||||
**Example response**:
|
||||
|
||||
- success
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"data": {
|
||||
"sports": [
|
||||
{
|
||||
"_can_be_disabled": false,
|
||||
"id": 1,
|
||||
"img": "/img/sports/cycling-sport.png",
|
||||
"is_active": false,
|
||||
"label": "Cycling (Sport)"
|
||||
}
|
||||
]
|
||||
},
|
||||
"status": "success"
|
||||
}
|
||||
|
||||
- sport not found
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 404 NOT FOUND
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"data": {
|
||||
"sports": []
|
||||
},
|
||||
"status": "not found"
|
||||
}
|
||||
|
||||
:param integer auth_user_id: authenticate user id (from JSON Web Token)
|
||||
:param integer sport_id: sport id
|
||||
|
||||
:<json string is_active: sport active status
|
||||
|
||||
:reqheader Authorization: OAuth 2.0 Bearer Token
|
||||
|
||||
:statuscode 200: sport updated
|
||||
:statuscode 400: invalid payload
|
||||
:statuscode 401:
|
||||
- Provide a valid auth token.
|
||||
- Signature expired. Please log in again.
|
||||
- Invalid token. Please log in again.
|
||||
:statuscode 404: sport not found
|
||||
:statuscode 500:
|
||||
|
||||
"""
|
||||
sport_data = request.get_json()
|
||||
if not sport_data or sport_data.get('label') is None:
|
||||
if not sport_data or sport_data.get('is_active') 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
|
||||
if not (
|
||||
not sport_data.get('is_active')
|
||||
and sport.is_active
|
||||
and len(sport.activities) > 0
|
||||
):
|
||||
sport.is_active = sport_data.get('is_active')
|
||||
db.session.commit()
|
||||
response_object = {
|
||||
'status': 'success',
|
||||
'data': {'sports': [sport.serialize()]},
|
||||
}
|
||||
code = 200
|
||||
else:
|
||||
response_object = {
|
||||
'status': 'fail',
|
||||
'message': 'Sport can not be disabled, activities exist.',
|
||||
}
|
||||
code = 400
|
||||
else:
|
||||
response_object = {
|
||||
'status': 'not found',
|
||||
'data': {'sports': sports_list},
|
||||
}
|
||||
response_object = {'status': 'not found', 'data': {'sports': []}}
|
||||
code = 404
|
||||
except (exc.IntegrityError, exc.OperationalError, ValueError) as e:
|
||||
db.session.rollback()
|
||||
@ -236,36 +280,3 @@ def update_sport(auth_user_id, sport_id):
|
||||
}
|
||||
code = 500
|
||||
return jsonify(response_object), code
|
||||
|
||||
|
||||
@sports_blueprint.route('/sports/<int:sport_id>', methods=['DELETE'])
|
||||
@authenticate_as_admin
|
||||
def delete_sport(auth_user_id, sport_id):
|
||||
"""Delete a sport"""
|
||||
try:
|
||||
sport = Sport.query.filter_by(id=sport_id).first()
|
||||
if sport:
|
||||
db.session.delete(sport)
|
||||
db.session.commit()
|
||||
response_object = {'status': 'no content'}
|
||||
code = 204
|
||||
else:
|
||||
response_object = {'status': 'not found', 'data': {'sports': []}}
|
||||
code = 404
|
||||
except exc.IntegrityError as e:
|
||||
db.session.rollback()
|
||||
appLog.error(e)
|
||||
response_object = {
|
||||
'status': 'error',
|
||||
'message': 'Error. Associated activities exist.',
|
||||
}
|
||||
code = 500
|
||||
except (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
|
||||
|
@ -4,14 +4,16 @@ expected_sport_1_cycling_result = {
|
||||
'id': 1,
|
||||
'label': 'Cycling',
|
||||
'img': None,
|
||||
'_can_be_deleted': True,
|
||||
'is_active': True,
|
||||
'_can_be_disabled': True,
|
||||
}
|
||||
|
||||
expected_sport_2_running_result = {
|
||||
'id': 2,
|
||||
'label': 'Running',
|
||||
'img': None,
|
||||
'_can_be_deleted': True,
|
||||
'is_active': True,
|
||||
'_can_be_disabled': True,
|
||||
}
|
||||
|
||||
|
||||
@ -83,78 +85,6 @@ def test_get_a_sport_invalid(app, user_1):
|
||||
assert len(data['data']['sports']) == 0
|
||||
|
||||
|
||||
def test_add_a_sport(app, user_1_admin):
|
||||
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.post(
|
||||
'/api/sports',
|
||||
content_type='application/json',
|
||||
data=json.dumps(dict(label='Cycling')),
|
||||
headers=dict(
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token']
|
||||
),
|
||||
)
|
||||
data = json.loads(response.data.decode())
|
||||
|
||||
assert response.status_code == 201
|
||||
assert 'created' in data['status']
|
||||
|
||||
assert len(data['data']['sports']) == 1
|
||||
assert data['data']['sports'][0] == expected_sport_1_cycling_result
|
||||
|
||||
|
||||
def test_add_a_sport_not_admin(app, user_1):
|
||||
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.post(
|
||||
'/api/sports',
|
||||
content_type='application/json',
|
||||
data=json.dumps(dict(label='surfing')),
|
||||
headers=dict(
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token']
|
||||
),
|
||||
)
|
||||
data = json.loads(response.data.decode())
|
||||
|
||||
assert response.status_code == 403
|
||||
assert 'created' not in data['status']
|
||||
assert 'error' in data['status']
|
||||
assert 'You do not have permissions.' in data['message']
|
||||
|
||||
|
||||
def test_add_a_sport_invalid_payload(app, user_1_admin):
|
||||
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.post(
|
||||
'/api/sports',
|
||||
content_type='application/json',
|
||||
data=json.dumps(dict()),
|
||||
headers=dict(
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token']
|
||||
),
|
||||
)
|
||||
data = json.loads(response.data.decode())
|
||||
|
||||
assert response.status_code == 400
|
||||
assert 'error' in data['status']
|
||||
assert 'Invalid payload.' in data['message']
|
||||
|
||||
|
||||
def test_update_a_sport(app, user_1_admin, sport_1_cycling):
|
||||
client = app.test_client()
|
||||
resp_login = client.post(
|
||||
@ -165,7 +95,7 @@ def test_update_a_sport(app, user_1_admin, sport_1_cycling):
|
||||
response = client.patch(
|
||||
'/api/sports/1',
|
||||
content_type='application/json',
|
||||
data=json.dumps(dict(label='cycling updated')),
|
||||
data=json.dumps(dict(is_active=False)),
|
||||
headers=dict(
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token']
|
||||
@ -177,7 +107,49 @@ def test_update_a_sport(app, user_1_admin, sport_1_cycling):
|
||||
assert 'success' in data['status']
|
||||
|
||||
assert len(data['data']['sports']) == 1
|
||||
assert 'cycling updated' in data['data']['sports'][0]['label']
|
||||
assert data['data']['sports'][0]['is_active'] is False
|
||||
|
||||
response = client.patch(
|
||||
'/api/sports/1',
|
||||
content_type='application/json',
|
||||
data=json.dumps(dict(is_active=True)),
|
||||
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 data['data']['sports'][0]['is_active'] is True
|
||||
|
||||
|
||||
def test_disable_a_sport_with_activities(
|
||||
app, user_1_admin, sport_1_cycling, activity_cycling_user_1
|
||||
):
|
||||
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(is_active=False)),
|
||||
headers=dict(
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token']
|
||||
),
|
||||
)
|
||||
data = json.loads(response.data.decode())
|
||||
|
||||
assert response.status_code == 400
|
||||
assert 'fail' in data['status']
|
||||
assert 'Sport can not be disabled, activities exist.' in data['message']
|
||||
|
||||
|
||||
def test_update_a_sport_not_admin(app, user_1, sport_1_cycling):
|
||||
@ -190,7 +162,7 @@ def test_update_a_sport_not_admin(app, user_1, sport_1_cycling):
|
||||
response = client.patch(
|
||||
'/api/sports/1',
|
||||
content_type='application/json',
|
||||
data=json.dumps(dict(label='cycling updated')),
|
||||
data=json.dumps(dict(is_active=False)),
|
||||
headers=dict(
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token']
|
||||
@ -237,7 +209,7 @@ def test_update_a_sport_invalid_id(app, user_1_admin):
|
||||
response = client.patch(
|
||||
'/api/sports/1',
|
||||
content_type='application/json',
|
||||
data=json.dumps(dict(label='cycling updated')),
|
||||
data=json.dumps(dict(is_active=False)),
|
||||
headers=dict(
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token']
|
||||
@ -248,91 +220,3 @@ def test_update_a_sport_invalid_id(app, user_1_admin):
|
||||
assert response.status_code == 404
|
||||
assert 'not found' in data['status']
|
||||
assert len(data['data']['sports']) == 0
|
||||
|
||||
|
||||
def test_delete_a_sport(app, user_1_admin, sport_1_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.delete(
|
||||
'/api/sports/1',
|
||||
content_type='application/json',
|
||||
headers=dict(
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token']
|
||||
),
|
||||
)
|
||||
assert response.status_code == 204
|
||||
|
||||
|
||||
def test_delete_a_sport_not_admin(app, user_1, sport_1_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.delete(
|
||||
'/api/sports/1',
|
||||
content_type='application/json',
|
||||
headers=dict(
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token']
|
||||
),
|
||||
)
|
||||
|
||||
data = json.loads(response.data.decode())
|
||||
assert response.status_code == 403
|
||||
assert 'error' in data['status']
|
||||
assert 'You do not have permissions.' in data['message']
|
||||
|
||||
|
||||
def test_delete_a_sport_invalid_id(app, user_1_admin):
|
||||
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.delete(
|
||||
'/api/sports/1',
|
||||
content_type='application/json',
|
||||
data=json.dumps(dict()),
|
||||
headers=dict(
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token']
|
||||
),
|
||||
)
|
||||
data = json.loads(response.data.decode())
|
||||
|
||||
assert response.status_code == 404
|
||||
assert 'not found' in data['status']
|
||||
assert len(data['data']['sports']) == 0
|
||||
|
||||
|
||||
def test_delete_a_sport_with_an_activity(
|
||||
app, user_1_admin, sport_1_cycling, activity_cycling_user_1
|
||||
):
|
||||
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.delete(
|
||||
'/api/sports/1',
|
||||
content_type='application/json',
|
||||
headers=dict(
|
||||
Authorization='Bearer '
|
||||
+ json.loads(resp_login.data.decode())['auth_token']
|
||||
),
|
||||
)
|
||||
|
||||
data = json.loads(response.data.decode())
|
||||
|
||||
assert response.status_code == 500
|
||||
assert 'error' in data['status']
|
||||
assert 'Error. Associated activities exist.' in data['message']
|
||||
|
@ -6,7 +6,8 @@ def test_sport_model(app, sport_1_cycling):
|
||||
serialized_sport = sport_1_cycling.serialize()
|
||||
assert 1 == serialized_sport['id']
|
||||
assert 'Cycling' == serialized_sport['label']
|
||||
assert serialized_sport['_can_be_deleted'] is True
|
||||
assert serialized_sport['is_active'] is True
|
||||
assert serialized_sport['_can_be_disabled'] is True
|
||||
|
||||
|
||||
def test_sport_model_with_activity(
|
||||
@ -19,4 +20,5 @@ def test_sport_model_with_activity(
|
||||
serialized_sport = sport_1_cycling.serialize()
|
||||
assert 1 == serialized_sport['id']
|
||||
assert 'Cycling' == serialized_sport['label']
|
||||
assert serialized_sport['_can_be_deleted'] is False
|
||||
assert serialized_sport['is_active'] is True
|
||||
assert serialized_sport['_can_be_disabled'] is False
|
||||
|
@ -0,0 +1,36 @@
|
||||
"""replace 'is_default' with 'is_active' in 'Sports' table
|
||||
|
||||
Revision ID: 1345afe3b11d
|
||||
Revises: f69f1e413bde
|
||||
Create Date: 2019-09-22 17:57:00.595775
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '1345afe3b11d'
|
||||
down_revision = 'f69f1e413bde'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.add_column('sports',
|
||||
sa.Column('is_active',
|
||||
sa.Boolean(create_constraint=50),
|
||||
nullable=True))
|
||||
op.execute("UPDATE sports SET is_active = true")
|
||||
op.alter_column('sports', 'is_active', nullable=False)
|
||||
op.drop_column('sports', 'is_default')
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.add_column('sports',
|
||||
sa.Column('is_default',
|
||||
sa.Boolean(create_constraint=50),
|
||||
nullable=True))
|
||||
op.execute("UPDATE sports SET is_default = true")
|
||||
op.alter_column('sports', 'is_default', nullable=False)
|
||||
op.drop_column('sports', 'is_active')
|
Reference in New Issue
Block a user