From 1c13aca2eb19262d37fe7ebea67d944fa7a6c0f7 Mon Sep 17 00:00:00 2001 From: Sam Date: Sat, 2 May 2020 18:00:17 +0200 Subject: [PATCH] API & Client - add pagination and filter on users lists --- .flake8 | 3 + .../fittrackee_api/activities/activities.py | 13 +- .../fittrackee_api/activities/stats.py | 5 +- .../fittrackee_api/application/app_config.py | 2 +- .../tests/test_app_config_api.py | 10 + .../fittrackee_api/tests/test_users_api.py | 679 +++++++++++++++++- fittrackee_api/fittrackee_api/users/models.py | 21 +- fittrackee_api/fittrackee_api/users/users.py | 84 ++- fittrackee_client/src/actions/index.js | 9 + .../src/components/Admin/AdminUsers.jsx | 284 +++++--- fittrackee_client/src/components/App.css | 13 +- .../src/components/Common/Pagination.jsx | 72 ++ fittrackee_client/src/fitTrackeeApi/index.js | 10 +- fittrackee_client/src/locales/en/common.json | 12 +- fittrackee_client/src/locales/fr/common.json | 12 +- fittrackee_client/src/reducers/index.js | 7 + fittrackee_client/src/utils/index.js | 44 ++ 17 files changed, 1154 insertions(+), 126 deletions(-) create mode 100644 .flake8 create mode 100644 fittrackee_client/src/components/Common/Pagination.jsx diff --git a/.flake8 b/.flake8 new file mode 100644 index 00000000..cf798af2 --- /dev/null +++ b/.flake8 @@ -0,0 +1,3 @@ +[flake8] +per-file-ignores = + fittrackee_api/fittrackee_api/activities/stats.py:E501 diff --git a/fittrackee_api/fittrackee_api/activities/activities.py b/fittrackee_api/fittrackee_api/activities/activities.py index 433c2939..bbb1a062 100644 --- a/fittrackee_api/fittrackee_api/activities/activities.py +++ b/fittrackee_api/fittrackee_api/activities/activities.py @@ -31,6 +31,8 @@ from .utils_gpx import ( activities_blueprint = Blueprint('activities', __name__) +ACTIVITIES_PER_PAGE = 5 + @activities_blueprint.route('/activities', methods=['GET']) @authenticate @@ -152,7 +154,8 @@ def get_activities(auth_user_id): :param integer auth_user_id: authenticate user id (from JSON Web Token) :query integer page: page if using pagination (default: 1) - :query integer per_page: number of activities per page (default: 5) + :query integer per_page: number of activities per page + (default: 5, max: 50) :query integer sport_id: sport id :query string from: start date (format: ``%Y-%m-%d``) :query string to: end date (format: ``%Y-%m-%d``) @@ -200,7 +203,13 @@ def get_activities(auth_user_id): max_speed_to = params.get('max_speed_to') order = params.get('order') sport_id = params.get('sport_id') - per_page = int(params.get('per_page')) if params.get('per_page') else 5 + per_page = ( + int(params.get('per_page')) + if params.get('per_page') + else ACTIVITIES_PER_PAGE + ) + if per_page > 50: + per_page = 50 activities = ( Activity.query.filter( Activity.user_id == auth_user_id, diff --git a/fittrackee_api/fittrackee_api/activities/stats.py b/fittrackee_api/fittrackee_api/activities/stats.py index f57b6825..a5d8f708 100644 --- a/fittrackee_api/fittrackee_api/activities/stats.py +++ b/fittrackee_api/fittrackee_api/activities/stats.py @@ -158,7 +158,7 @@ def get_activities_by_time(auth_user_id, user_name): .. sourcecode:: http - GET /api/stats/admin/by_time?from=2018-01-01&to=2018-06-30&time=week HTTP/1.1 # noqa + GET /api/stats/admin/by_time?from=2018-01-01&to=2018-06-30&time=week HTTP/1.1 **Example responses**: @@ -346,7 +346,8 @@ def get_application_stats(auth_user_id): "data": { "activities": 3, "sports": 3, - "users": 2 + "users": 2, + "uploads_dir_size": 1000 }, "status": "success" } diff --git a/fittrackee_api/fittrackee_api/application/app_config.py b/fittrackee_api/fittrackee_api/application/app_config.py index 00a70289..532bfe9d 100644 --- a/fittrackee_api/fittrackee_api/application/app_config.py +++ b/fittrackee_api/fittrackee_api/application/app_config.py @@ -143,7 +143,7 @@ def update_application_config(auth_user_id): @config_blueprint.route('/ping', methods=['GET']) -def ping_pong(): +def health_check(): """ health check endpoint **Example request**: diff --git a/fittrackee_api/fittrackee_api/tests/test_app_config_api.py b/fittrackee_api/fittrackee_api/tests/test_app_config_api.py index 24eff81e..56b9475b 100644 --- a/fittrackee_api/fittrackee_api/tests/test_app_config_api.py +++ b/fittrackee_api/fittrackee_api/tests/test_app_config_api.py @@ -200,3 +200,13 @@ def test_update_config_no_config(app_no_config, user_1_admin): assert response.status_code == 500 assert 'error' in data['status'] assert 'Error on updating configuration.' in data['message'] + + +def test_ping(app): + """ => Ensure the /ping route behaves correctly.""" + client = app.test_client() + response = client.get('/api/ping') + data = json.loads(response.data.decode()) + assert response.status_code == 200 + assert 'pong' in data['message'] + assert 'success' in data['status'] diff --git a/fittrackee_api/fittrackee_api/tests/test_users_api.py b/fittrackee_api/fittrackee_api/tests/test_users_api.py index 343d45a4..8976c399 100644 --- a/fittrackee_api/fittrackee_api/tests/test_users_api.py +++ b/fittrackee_api/fittrackee_api/tests/test_users_api.py @@ -1,19 +1,11 @@ import json +from datetime import datetime, timedelta from io import BytesIO +from unittest.mock import patch from fittrackee_api.users.models import User -def test_ping(app): - """ => Ensure the /ping route behaves correctly.""" - client = app.test_client() - response = client.get('/api/ping') - data = json.loads(response.data.decode()) - assert response.status_code == 200 - assert 'pong' in data['message'] - assert 'success' in data['status'] - - def test_single_user(app, user_1, user_2): """=> Get single user details""" client = app.test_client() @@ -183,6 +175,13 @@ def test_users_list(app, user_1, user_2, user_3): assert data['data']['users'][2]['sports_list'] == [] assert data['data']['users'][2]['total_distance'] == 0 assert data['data']['users'][2]['total_duration'] == '0:00:00' + assert data['pagination'] == { + 'has_next': False, + 'has_prev': False, + 'page': 1, + 'pages': 1, + 'total': 3, + } def test_users_list_with_activities( @@ -246,6 +245,666 @@ def test_users_list_with_activities( assert data['data']['users'][2]['sports_list'] == [] assert data['data']['users'][2]['total_distance'] == 0 assert data['data']['users'][2]['total_duration'] == '0:00:00' + assert data['pagination'] == { + 'has_next': False, + 'has_prev': False, + 'page': 1, + 'pages': 1, + 'total': 3, + } + + +@patch('fittrackee_api.users.users.USER_PER_PAGE', 2) +def test_it_gets_first_page_on_users_list( + app, user_1, user_2, user_3, +): + 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/users?page=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']['users']) == 2 + assert data['pagination'] == { + 'has_next': True, + 'has_prev': False, + 'page': 1, + 'pages': 2, + 'total': 3, + } + + +@patch('fittrackee_api.users.users.USER_PER_PAGE', 2) +def test_it_gets_next_page_on_users_list( + app, user_1, user_2, user_3, +): + 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/users?page=2', + 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']['users']) == 1 + assert data['pagination'] == { + 'has_next': False, + 'has_prev': True, + 'page': 2, + 'pages': 2, + 'total': 3, + } + + +def test_it_gets_empty_next_page_on_users_list( + app, user_1, user_2, user_3, +): + 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/users?page=2', + 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']['users']) == 0 + assert data['pagination'] == { + 'has_next': False, + 'has_prev': True, + 'page': 2, + 'pages': 1, + 'total': 3, + } + + +def test_it_gets_user_list_with_2_per_page( + app, user_1, user_2, user_3, +): + 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/users?per_page=2', + 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']['users']) == 2 + assert data['pagination'] == { + 'has_next': True, + 'has_prev': False, + 'page': 1, + 'pages': 2, + 'total': 3, + } + + +def test_it_gets_next_page_on_user_list_with_2_per_page( + app, user_1, user_2, user_3, +): + 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/users?page=2&per_page=2', + 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']['users']) == 1 + assert data['pagination'] == { + 'has_next': False, + 'has_prev': True, + 'page': 2, + 'pages': 2, + 'total': 3, + } + + +def test_it_gets_users_list_ordered_by_username(app, user_1, user_2, user_3): + 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/users?order_by=username', + 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']['users']) == 3 + assert 'sam' in data['data']['users'][0]['username'] + assert 'test' in data['data']['users'][1]['username'] + assert 'toto' in data['data']['users'][2]['username'] + assert data['pagination'] == { + 'has_next': False, + 'has_prev': False, + 'page': 1, + 'pages': 1, + 'total': 3, + } + + +def test_it_gets_users_list_ordered_by_username_ascending( + app, user_1, user_2, user_3 +): + 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/users?order_by=username&order=asc', + 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']['users']) == 3 + assert 'sam' in data['data']['users'][0]['username'] + assert 'test' in data['data']['users'][1]['username'] + assert 'toto' in data['data']['users'][2]['username'] + assert data['pagination'] == { + 'has_next': False, + 'has_prev': False, + 'page': 1, + 'pages': 1, + 'total': 3, + } + + +def test_it_gets_users_list_ordered_by_username_descending( + app, user_1, user_2, user_3 +): + 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/users?order_by=username&order=desc', + 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']['users']) == 3 + assert 'toto' in data['data']['users'][0]['username'] + assert 'test' in data['data']['users'][1]['username'] + assert 'sam' in data['data']['users'][2]['username'] + assert data['pagination'] == { + 'has_next': False, + 'has_prev': False, + 'page': 1, + 'pages': 1, + 'total': 3, + } + + +def test_it_gets_users_list_ordered_by_creation_date( + app, user_2, user_3, user_1_admin +): + user_2.created_at = datetime.utcnow() - timedelta(days=1) + user_3.created_at = datetime.utcnow() - timedelta(hours=1) + user_1_admin.created_at = datetime.utcnow() + 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.get( + '/api/users?order_by=created_at', + 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']['users']) == 3 + assert 'toto' in data['data']['users'][0]['username'] + assert 'sam' in data['data']['users'][1]['username'] + assert 'admin' in data['data']['users'][2]['username'] + assert data['pagination'] == { + 'has_next': False, + 'has_prev': False, + 'page': 1, + 'pages': 1, + 'total': 3, + } + + +def test_it_gets_users_list_ordered_by_creation_date_ascending( + app, user_2, user_3, user_1_admin +): + user_2.created_at = datetime.utcnow() - timedelta(days=1) + user_3.created_at = datetime.utcnow() - timedelta(hours=1) + user_1_admin.created_at = datetime.utcnow() + 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.get( + '/api/users?order_by=created_at&order=asc', + 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']['users']) == 3 + assert 'toto' in data['data']['users'][0]['username'] + assert 'sam' in data['data']['users'][1]['username'] + assert 'admin' in data['data']['users'][2]['username'] + assert data['pagination'] == { + 'has_next': False, + 'has_prev': False, + 'page': 1, + 'pages': 1, + 'total': 3, + } + + +def test_it_gets_users_list_ordered_by_creation_date_descending( + app, user_2, user_3, user_1_admin +): + user_2.created_at = datetime.utcnow() - timedelta(days=1) + user_3.created_at = datetime.utcnow() - timedelta(hours=1) + user_1_admin.created_at = datetime.utcnow() + + 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.get( + '/api/users?order_by=created_at&order=desc', + 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']['users']) == 3 + assert 'admin' in data['data']['users'][0]['username'] + assert 'sam' in data['data']['users'][1]['username'] + assert 'toto' in data['data']['users'][2]['username'] + assert data['pagination'] == { + 'has_next': False, + 'has_prev': False, + 'page': 1, + 'pages': 1, + 'total': 3, + } + + +def test_it_gets_users_list_ordered_by_admin_rights( + app, user_2, user_1_admin, user_3 +): + 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.get( + '/api/users?order_by=admin', + 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']['users']) == 3 + assert 'toto' in data['data']['users'][0]['username'] + assert 'sam' in data['data']['users'][1]['username'] + assert 'admin' in data['data']['users'][2]['username'] + assert data['pagination'] == { + 'has_next': False, + 'has_prev': False, + 'page': 1, + 'pages': 1, + 'total': 3, + } + + +def test_it_gets_users_list_ordered_by_admin_rights_ascending( + app, user_2, user_1_admin, user_3 +): + 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.get( + '/api/users?order_by=admin&order=asc', + 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']['users']) == 3 + assert 'toto' in data['data']['users'][0]['username'] + assert 'sam' in data['data']['users'][1]['username'] + assert 'admin' in data['data']['users'][2]['username'] + assert data['pagination'] == { + 'has_next': False, + 'has_prev': False, + 'page': 1, + 'pages': 1, + 'total': 3, + } + + +def test_it_gets_users_list_ordered_by_admin_rights_descending( + app, user_2, user_3, 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.get( + '/api/users?order_by=admin&order=desc', + 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']['users']) == 3 + assert 'admin' in data['data']['users'][0]['username'] + assert 'toto' in data['data']['users'][1]['username'] + assert 'sam' in data['data']['users'][2]['username'] + assert data['pagination'] == { + 'has_next': False, + 'has_prev': False, + 'page': 1, + 'pages': 1, + 'total': 3, + } + + +def test_it_gets_users_list_ordered_by_activities_count( + app, user_1, user_2, user_3, sport_1_cycling, activity_cycling_user_2, +): + 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/users?order_by=activities_count', + 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']['users']) == 3 + assert 'test' in data['data']['users'][0]['username'] + assert 0 == data['data']['users'][0]['nb_activities'] + assert 'sam' in data['data']['users'][1]['username'] + assert 0 == data['data']['users'][1]['nb_activities'] + assert 'toto' in data['data']['users'][2]['username'] + assert 1 == data['data']['users'][2]['nb_activities'] + assert data['pagination'] == { + 'has_next': False, + 'has_prev': False, + 'page': 1, + 'pages': 1, + 'total': 3, + } + + +def test_it_gets_users_list_ordered_by_activities_count_ascending( + app, user_1, user_2, user_3, sport_1_cycling, activity_cycling_user_2, +): + 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/users?order_by=activities_count&order=asc', + 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']['users']) == 3 + assert 'test' in data['data']['users'][0]['username'] + assert 0 == data['data']['users'][0]['nb_activities'] + assert 'sam' in data['data']['users'][1]['username'] + assert 0 == data['data']['users'][1]['nb_activities'] + assert 'toto' in data['data']['users'][2]['username'] + assert 1 == data['data']['users'][2]['nb_activities'] + assert data['pagination'] == { + 'has_next': False, + 'has_prev': False, + 'page': 1, + 'pages': 1, + 'total': 3, + } + + +def test_it_gets_users_list_ordered_by_activities_count_descending( + app, user_1, user_2, user_3, sport_1_cycling, activity_cycling_user_2, +): + 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/users?order_by=activities_count&order=desc', + 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']['users']) == 3 + assert 'toto' in data['data']['users'][0]['username'] + assert 1 == data['data']['users'][0]['nb_activities'] + assert 'test' in data['data']['users'][1]['username'] + assert 0 == data['data']['users'][1]['nb_activities'] + assert 'sam' in data['data']['users'][2]['username'] + assert 0 == data['data']['users'][2]['nb_activities'] + assert data['pagination'] == { + 'has_next': False, + 'has_prev': False, + 'page': 1, + 'pages': 1, + 'total': 3, + } + + +def test_it_gets_users_list_filtering_on_username(app, user_1, user_2, user_3): + 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/users?q=toto', + 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']['users']) == 1 + assert 'toto' in data['data']['users'][0]['username'] + assert data['pagination'] == { + 'has_next': False, + 'has_prev': False, + 'page': 1, + 'pages': 1, + 'total': 1, + } + + +def test_it_returns_empty_users_list_filtering_on_username( + app, user_1, user_2, user_3 +): + 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/users?q=not_existing', + 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']['users']) == 0 + assert data['pagination'] == { + 'has_next': False, + 'has_prev': False, + 'page': 1, + 'pages': 0, + 'total': 0, + } + + +def test_it_users_list_with_complex_query(app, user_1, user_2, user_3): + 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/users?order_by=username&order=desc&page=2&per_page=2', + 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']['users']) == 1 + assert 'sam' in data['data']['users'][0]['username'] + assert data['pagination'] == { + 'has_next': False, + 'has_prev': True, + 'page': 2, + 'pages': 2, + 'total': 3, + } def test_encode_auth_token(app, user_1): diff --git a/fittrackee_api/fittrackee_api/users/models.py b/fittrackee_api/fittrackee_api/users/models.py index 80a3a2dc..68a26cbd 100644 --- a/fittrackee_api/fittrackee_api/users/models.py +++ b/fittrackee_api/fittrackee_api/users/models.py @@ -4,6 +4,8 @@ import jwt from fittrackee_api import bcrypt, db from flask import current_app from sqlalchemy import func +from sqlalchemy.ext.hybrid import hybrid_property +from sqlalchemy.sql.expression import select from ..activities.models import Activity @@ -88,13 +90,22 @@ class User(db.Model): except jwt.InvalidTokenError: return 'Invalid token. Please log in again.' + @hybrid_property + def activities_count(self): + return Activity.query.filter(Activity.user_id == self.id).count() + + @activities_count.expression + def activities_count(self): + return ( + select([func.count(Activity.id)]) + .where(Activity.user_id == self.id) + .label("activities_count") + ) + def serialize(self): - nb_activity = Activity.query.filter( - Activity.user_id == self.id - ).count() sports = [] total = (None, None) - if nb_activity > 0: + if self.activities_count > 0: sports = ( db.session.query(Activity.sport_id) .filter(Activity.user_id == self.id) @@ -123,7 +134,7 @@ class User(db.Model): 'timezone': self.timezone, 'weekm': self.weekm, 'language': self.language, - 'nb_activities': nb_activity, + 'nb_activities': self.activities_count, 'nb_sports': len(sports), 'sports_list': [ sport for sportslist in sports for sport in sportslist diff --git a/fittrackee_api/fittrackee_api/users/users.py b/fittrackee_api/fittrackee_api/users/users.py index e6d39044..42efe880 100644 --- a/fittrackee_api/fittrackee_api/users/users.py +++ b/fittrackee_api/fittrackee_api/users/users.py @@ -11,6 +11,8 @@ from .utils import authenticate, authenticate_as_admin users_blueprint = Blueprint('users', __name__) +USER_PER_PAGE = 10 + @users_blueprint.route('/users', methods=['GET']) @authenticate @@ -20,9 +22,18 @@ def get_users(auth_user_id): **Example request**: + - without parameters + .. sourcecode:: http - GET /api/users HTTP/1.1 + GET /api/users/ HTTP/1.1 + Content-Type: application/json + + - with some query parameters + + .. sourcecode:: http + + GET /api/users?order_by=activities_count&par_page=5 HTTP/1.1 Content-Type: application/json **Example response**: @@ -84,6 +95,13 @@ def get_users(auth_user_id): :param integer auth_user_id: authenticate user id (from JSON Web Token) + :query integer page: page if using pagination (default: 1) + :query integer per_page: number of users per page (default: 10, max: 50) + :query string q: query on user name + :query string order_by: sorting criteria (``username``, ``created_at``, + ``activities_count``, ``admin``) + :query string order: sorting order (default: ``asc``) + :reqheader Authorization: OAuth 2.0 Bearer Token :statuscode 200: success @@ -93,10 +111,61 @@ def get_users(auth_user_id): - Invalid token. Please log in again. """ - users = User.query.all() + params = request.args.copy() + page = 1 if 'page' not in params.keys() else int(params.get('page')) + per_page = ( + int(params.get('per_page')) + if params.get('per_page') + else USER_PER_PAGE + ) + if per_page > 50: + per_page = 50 + order_by = params.get('order_by') + order = params.get('order', 'asc') + query = params.get('q') + users_pagination = ( + User.query.filter( + User.username.like('%' + query + '%') if query else True, + ) + .order_by( + User.activities_count.asc() + if order_by == 'activities_count' and order == 'asc' + else True, + User.activities_count.desc() + if order_by == 'activities_count' and order == 'desc' + else True, + User.username.asc() + if order_by == 'username' and order == 'asc' + else True, + User.username.desc() + if order_by == 'username' and order == 'desc' + else True, + User.created_at.asc() + if order_by == 'created_at' and order == 'asc' + else True, + User.created_at.desc() + if order_by == 'created_at' and order == 'desc' + else True, + User.admin.asc() + if order_by == 'admin' and order == 'asc' + else True, + User.admin.desc() + if order_by == 'admin' and order == 'desc' + else True, + ) + .paginate(page, per_page, False) + ) + users = users_pagination.items response_object = { 'status': 'success', 'data': {'users': [user.serialize() for user in users]}, + 'pagination': { + 'has_next': users_pagination.has_next, + 'has_prev': users_pagination.has_prev, + 'page': users_pagination.page, + 'pages': users_pagination.pages, + 'total': users_pagination.total, + }, } return jsonify(response_object), 200 @@ -227,6 +296,7 @@ def get_picture(user_name): def update_user(auth_user_id, user_name): """ Update user to add admin rights + Only user with admin rights can modify another user **Example request**: @@ -321,12 +391,14 @@ def update_user(auth_user_id, user_name): @users_blueprint.route('/users/', methods=['DELETE']) @authenticate -def delete_activity(auth_user_id, user_name): +def delete_user(auth_user_id, user_name): """ Delete a user account - - a user can only delete his own account - - an admin can delete all accounts except his account if he's the only - one admin + + A user can only delete his own account + + An admin can delete all accounts except his account if he's the only + one admin **Example request**: diff --git a/fittrackee_client/src/actions/index.js b/fittrackee_client/src/actions/index.js index d072c29c..941a58f0 100644 --- a/fittrackee_client/src/actions/index.js +++ b/fittrackee_client/src/actions/index.js @@ -8,6 +8,12 @@ export const setData = (target, data) => ({ data, target, }) +export const setPaginatedData = (target, data, pagination) => ({ + type: 'SET_PAGINATED_DATA', + data, + pagination, + target, +}) export const setError = message => ({ type: 'SET_ERROR', @@ -50,6 +56,9 @@ export const getOrUpdateData = ( .then(ret => { if (ret.status === 'success') { if (canDispatch) { + if (target === 'users' && action === 'getData') { + return dispatch(setPaginatedData(target, ret.data, ret.pagination)) + } dispatch(setData(target, ret.data)) } else if (action === 'updateData' && target === 'sports') { dispatch(updateSportsData(ret.data.sports[0])) diff --git a/fittrackee_client/src/components/Admin/AdminUsers.jsx b/fittrackee_client/src/components/Admin/AdminUsers.jsx index f949f8a9..590a73cd 100644 --- a/fittrackee_client/src/components/Admin/AdminUsers.jsx +++ b/fittrackee_client/src/components/Admin/AdminUsers.jsx @@ -5,17 +5,69 @@ import { Helmet } from 'react-helmet' import { Link } from 'react-router-dom' import Message from '../Common/Message' +import Pagination from '../Common/Pagination' import { history } from '../../index' import { getOrUpdateData } from '../../actions' -import { apiUrl } from '../../utils' +import { + apiUrl, + formatUrl, + sortOrders, + translateValues, + userFilters, +} from '../../utils' class AdminUsers extends React.Component { + constructor(props, context) { + super(props, context) + this.state = { + page: null, + per_page: null, + order_by: 'created_at', + order: 'asc', + } + } + componentDidMount() { - this.props.loadUsers() + this.initState() + this.props.loadUsers(this.props.location.query) + } + + componentDidUpdate(prevProps) { + if (prevProps.location.query !== this.props.location.query) { + this.props.loadUsers(this.props.location.query) + } + } + + initState() { + const { query } = this.props.location + this.setState({ + page: query.page, + per_page: query.per_page, + order_by: query.order_by ? query.order_by : 'created_at', + order: query.order ? query.order : 'asc', + }) + } + + updatePage(key, value) { + const query = Object.assign({}, this.state) + query[key] = value + this.setState(query) + const url = formatUrl(this.props.location.pathname, query) + history.push(url) } render() { - const { message, t, updateUser, authUser, users } = this.props + const { + authUser, + location, + message, + t, + pagination, + updateUser, + users, + } = this.props + const translatedFilters = translateValues(t, userFilters) + const translatedSortOrders = translateValues(t, sortOrders) return (
@@ -24,89 +76,143 @@ class AdminUsers extends React.Component { {message && }
-
-
- - - - - - - - - - - - - - {users.map(user => ( - - - - - - - - +
+
+
{t('administration:Users')}
+
+
+
+ +
+
+ +
+
+
#{t('user:Username')}{t('user:Email')}{t('user:Registration Date')}{t('activities:Activities')}{t('user:Admin')}{t('administration:Actions')}
- {user.picture === true && ( - Avatar - )} - - - {user.username} - - {user.email} - {format( - new Date(user.created_at), - 'dd/MM/yyyy HH:mm' - )} - {user.nb_activities} - {user.admin ? ( - - - updateUser(user.username, !user.admin) - } - /> -
+ + + + + + + + + - ))} - -
#{t('user:Username')}{t('user:Email')}{t('user:Registration Date')}{t('activities:Activities')}{t('user:Admin')}{t('administration:Actions')}
- history.push('/admin/')} - value={t('common:Back')} - /> + + + {users.map(user => ( + + + {user.picture === true ? ( + Avatar + ) : ( +