API - add enpoints to request user data export and get export status
This commit is contained in:
		@@ -9,7 +9,12 @@ from flask import Flask
 | 
			
		||||
from freezegun import freeze_time
 | 
			
		||||
 | 
			
		||||
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.workouts.models import Sport
 | 
			
		||||
 | 
			
		||||
@@ -2822,3 +2827,154 @@ class TestUserPrivacyPolicyUpdate(ApiTestCaseMixin):
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        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.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.token import decode_user_token
 | 
			
		||||
 | 
			
		||||
@@ -1684,3 +1684,132 @@ def accept_privacy_policy(auth_user: User) -> Union[Dict, HttpResponse]:
 | 
			
		||||
            return InvalidPayloadErrorResponse()
 | 
			
		||||
    except (exc.IntegrityError, exc.OperationalError, ValueError) as e:
 | 
			
		||||
        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,
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user