API & Client - sport administration & refactor - #15

This commit is contained in:
Sam
2019-09-22 23:03:56 +02:00
parent a10128f13e
commit 1f8de2eccc
30 changed files with 518 additions and 673 deletions

View File

@ -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
),
}

View File

@ -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

View File

@ -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']

View File

@ -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

View File

@ -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')