API - add enpoints to request user data export and get export status
This commit is contained in:
parent
8d8bb2efb9
commit
e4fc8849be
@ -9,7 +9,12 @@ from flask import Flask
|
|||||||
from freezegun import freeze_time
|
from freezegun import freeze_time
|
||||||
|
|
||||||
from fittrackee import db
|
from fittrackee import db
|
||||||
from fittrackee.users.models import BlacklistedToken, User, UserSportPreference
|
from fittrackee.users.models import (
|
||||||
|
BlacklistedToken,
|
||||||
|
User,
|
||||||
|
UserDataExport,
|
||||||
|
UserSportPreference,
|
||||||
|
)
|
||||||
from fittrackee.users.utils.token import get_user_token
|
from fittrackee.users.utils.token import get_user_token
|
||||||
from fittrackee.workouts.models import Sport
|
from fittrackee.workouts.models import Sport
|
||||||
|
|
||||||
@ -2822,3 +2827,154 @@ class TestUserPrivacyPolicyUpdate(ApiTestCaseMixin):
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == 400
|
assert response.status_code == 400
|
||||||
|
|
||||||
|
|
||||||
|
class TestPostUserDataExportRequest(ApiTestCaseMixin):
|
||||||
|
def test_it_returns_data_export_info_when_no_ongoing_request_exists_for_user( # noqa
|
||||||
|
self,
|
||||||
|
app: Flask,
|
||||||
|
user_1: User,
|
||||||
|
user_2: User,
|
||||||
|
) -> None:
|
||||||
|
db.session.add(UserDataExport(user_id=user_2.id))
|
||||||
|
db.session.commit()
|
||||||
|
client, auth_token = self.get_test_client_and_auth_token(
|
||||||
|
app, user_1.email
|
||||||
|
)
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
'/api/auth/profile/export/request',
|
||||||
|
content_type='application/json',
|
||||||
|
headers=dict(Authorization=f'Bearer {auth_token}'),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = json.loads(response.data.decode())
|
||||||
|
data_export_request = UserDataExport.query.filter_by(
|
||||||
|
user_id=user_1.id
|
||||||
|
).first()
|
||||||
|
assert data["status"] == "success"
|
||||||
|
assert data["request"] == jsonify_dict(data_export_request.serialize())
|
||||||
|
|
||||||
|
def test_it_returns_error_if_ongoing_request_exist(
|
||||||
|
self,
|
||||||
|
app: Flask,
|
||||||
|
user_1: User,
|
||||||
|
) -> None:
|
||||||
|
db.session.add(UserDataExport(user_id=user_1.id))
|
||||||
|
db.session.commit()
|
||||||
|
client, auth_token = self.get_test_client_and_auth_token(
|
||||||
|
app, user_1.email
|
||||||
|
)
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
'/api/auth/profile/export/request',
|
||||||
|
content_type='application/json',
|
||||||
|
headers=dict(Authorization=f'Bearer {auth_token}'),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assert_400(response, "ongoing request exists")
|
||||||
|
|
||||||
|
def test_it_returns_error_if_existing_request_is_completed_for_less_than_24_hours( # noqa
|
||||||
|
self,
|
||||||
|
app: Flask,
|
||||||
|
user_1: User,
|
||||||
|
) -> None:
|
||||||
|
completed_export_request = UserDataExport(user_id=user_1.id)
|
||||||
|
db.session.add(completed_export_request)
|
||||||
|
completed_export_request.completed = True
|
||||||
|
db.session.commit()
|
||||||
|
client, auth_token = self.get_test_client_and_auth_token(
|
||||||
|
app, user_1.email
|
||||||
|
)
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
'/api/auth/profile/export/request',
|
||||||
|
content_type='application/json',
|
||||||
|
headers=dict(Authorization=f'Bearer {auth_token}'),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assert_400(response, "completed request already exists")
|
||||||
|
|
||||||
|
def test_it_returns_new_request_if_existing_request_is_completed_for_more_than_more_24_hours( # noqa
|
||||||
|
self,
|
||||||
|
app: Flask,
|
||||||
|
user_1: User,
|
||||||
|
) -> None:
|
||||||
|
completed_export_request = UserDataExport(
|
||||||
|
user_id=user_1.id,
|
||||||
|
created_at=datetime.utcnow() - timedelta(hours=24),
|
||||||
|
)
|
||||||
|
db.session.add(completed_export_request)
|
||||||
|
completed_export_request.completed = True
|
||||||
|
db.session.commit()
|
||||||
|
client, auth_token = self.get_test_client_and_auth_token(
|
||||||
|
app, user_1.email
|
||||||
|
)
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
'/api/auth/profile/export/request',
|
||||||
|
content_type='application/json',
|
||||||
|
headers=dict(Authorization=f'Bearer {auth_token}'),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = json.loads(response.data.decode())
|
||||||
|
data_export_request = UserDataExport.query.filter_by(
|
||||||
|
user_id=user_1.id
|
||||||
|
).first()
|
||||||
|
assert data_export_request.id != completed_export_request.id
|
||||||
|
assert data["status"] == "success"
|
||||||
|
assert data["request"] == jsonify_dict(data_export_request.serialize())
|
||||||
|
|
||||||
|
|
||||||
|
class TestGetUserDataExportRequest(ApiTestCaseMixin):
|
||||||
|
def test_it_returns_none_if_no_request(
|
||||||
|
self,
|
||||||
|
app: Flask,
|
||||||
|
user_1: User,
|
||||||
|
user_2: User,
|
||||||
|
) -> None:
|
||||||
|
client, auth_token = self.get_test_client_and_auth_token(
|
||||||
|
app, user_1.email
|
||||||
|
)
|
||||||
|
|
||||||
|
response = client.get(
|
||||||
|
'/api/auth/profile/export',
|
||||||
|
content_type='application/json',
|
||||||
|
headers=dict(Authorization=f'Bearer {auth_token}'),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = json.loads(response.data.decode())
|
||||||
|
assert data["status"] == "success"
|
||||||
|
assert data["request"] is None
|
||||||
|
|
||||||
|
def test_it_returns_existing_request(
|
||||||
|
self,
|
||||||
|
app: Flask,
|
||||||
|
user_1: User,
|
||||||
|
user_2: User,
|
||||||
|
) -> None:
|
||||||
|
completed_export_request = UserDataExport(
|
||||||
|
user_id=user_1.id,
|
||||||
|
created_at=datetime.utcnow() - timedelta(hours=24),
|
||||||
|
)
|
||||||
|
db.session.add(completed_export_request)
|
||||||
|
db.session.commit()
|
||||||
|
client, auth_token = self.get_test_client_and_auth_token(
|
||||||
|
app, user_1.email
|
||||||
|
)
|
||||||
|
|
||||||
|
response = client.get(
|
||||||
|
'/api/auth/profile/export',
|
||||||
|
content_type='application/json',
|
||||||
|
headers=dict(Authorization=f'Bearer {auth_token}'),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = json.loads(response.data.decode())
|
||||||
|
assert data["status"] == "success"
|
||||||
|
assert data["request"] == jsonify_dict(
|
||||||
|
completed_export_request.serialize()
|
||||||
|
)
|
||||||
|
@ -33,7 +33,7 @@ from fittrackee.responses import (
|
|||||||
from fittrackee.utils import get_readable_duration
|
from fittrackee.utils import get_readable_duration
|
||||||
from fittrackee.workouts.models import Sport
|
from fittrackee.workouts.models import Sport
|
||||||
|
|
||||||
from .models import BlacklistedToken, User, UserSportPreference
|
from .models import BlacklistedToken, User, UserDataExport, UserSportPreference
|
||||||
from .utils.controls import check_password, is_valid_email, register_controls
|
from .utils.controls import check_password, is_valid_email, register_controls
|
||||||
from .utils.token import decode_user_token
|
from .utils.token import decode_user_token
|
||||||
|
|
||||||
@ -1684,3 +1684,132 @@ def accept_privacy_policy(auth_user: User) -> Union[Dict, HttpResponse]:
|
|||||||
return InvalidPayloadErrorResponse()
|
return InvalidPayloadErrorResponse()
|
||||||
except (exc.IntegrityError, exc.OperationalError, ValueError) as e:
|
except (exc.IntegrityError, exc.OperationalError, ValueError) as e:
|
||||||
return handle_error_and_return_response(e, db=db)
|
return handle_error_and_return_response(e, db=db)
|
||||||
|
|
||||||
|
|
||||||
|
@auth_blueprint.route('/auth/profile/export/request', methods=['POST'])
|
||||||
|
@require_auth()
|
||||||
|
def request_user_data_export(auth_user: User) -> Union[Dict, HttpResponse]:
|
||||||
|
"""
|
||||||
|
Request a data export for authenticated user.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
POST /auth/profile/export/request HTTP/1.1
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"request": {
|
||||||
|
"created_at": "Wed, 01 Mar 2023 12:31:17 GMT",
|
||||||
|
"status": "in_progress",
|
||||||
|
"file_name": null,
|
||||||
|
"file_size": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:reqheader Authorization: OAuth 2.0 Bearer Token
|
||||||
|
|
||||||
|
:statuscode 200: success
|
||||||
|
:statuscode 400:
|
||||||
|
- ongoing request exists
|
||||||
|
- completed request already exists
|
||||||
|
:statuscode 401:
|
||||||
|
- provide a valid auth token
|
||||||
|
- signature expired, please log in again
|
||||||
|
- invalid token, please log in again
|
||||||
|
:statuscode 500: internal server error
|
||||||
|
"""
|
||||||
|
existing_export_request = UserDataExport.query.filter_by(
|
||||||
|
user_id=auth_user.id
|
||||||
|
).first()
|
||||||
|
if existing_export_request:
|
||||||
|
if not existing_export_request.completed:
|
||||||
|
return InvalidPayloadErrorResponse("ongoing request exists")
|
||||||
|
|
||||||
|
if (
|
||||||
|
existing_export_request.created_at
|
||||||
|
> datetime.datetime.utcnow() - datetime.timedelta(hours=24)
|
||||||
|
):
|
||||||
|
return InvalidPayloadErrorResponse(
|
||||||
|
"completed request already exists"
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if existing_export_request:
|
||||||
|
db.session.delete(existing_export_request)
|
||||||
|
db.session.flush()
|
||||||
|
export_request = UserDataExport(user_id=auth_user.id)
|
||||||
|
db.session.add(export_request)
|
||||||
|
db.session.commit()
|
||||||
|
return {"status": "success", "request": export_request.serialize()}
|
||||||
|
except (exc.IntegrityError, exc.OperationalError, ValueError) as e:
|
||||||
|
return handle_error_and_return_response(e, db=db)
|
||||||
|
|
||||||
|
|
||||||
|
@auth_blueprint.route('/auth/profile/export', methods=['GET'])
|
||||||
|
@require_auth()
|
||||||
|
def get_user_data_export(auth_user: User) -> Union[Dict, HttpResponse]:
|
||||||
|
"""
|
||||||
|
Get a data export info for authenticated user if a request exists.
|
||||||
|
It returns:
|
||||||
|
- export creation date
|
||||||
|
- export status ("in_progress", "successful" and "errored")
|
||||||
|
- file name and size (in bytes) when export is successful
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
GET /auth/profile/export HTTP/1.1
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
- if a request exists:
|
||||||
|
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"request": {
|
||||||
|
"created_at": "Wed, 01 Mar 2023 12:31:17 GMT",
|
||||||
|
"status": "successful",
|
||||||
|
"file_name": "archive_rgjsR3fHt295ywNQr5Yp.zip",
|
||||||
|
"file_size": 924
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- if no request:
|
||||||
|
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"request": null
|
||||||
|
}
|
||||||
|
|
||||||
|
:reqheader Authorization: OAuth 2.0 Bearer Token
|
||||||
|
|
||||||
|
:statuscode 200: success
|
||||||
|
:statuscode 401:
|
||||||
|
- provide a valid auth token
|
||||||
|
- signature expired, please log in again
|
||||||
|
- invalid token, please log in again
|
||||||
|
"""
|
||||||
|
export_request = UserDataExport.query.filter_by(
|
||||||
|
user_id=auth_user.id
|
||||||
|
).first()
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"request": export_request.serialize() if export_request else None,
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user