API - init privacy policy

This commit is contained in:
Sam 2023-02-25 14:06:49 +01:00
parent 789aa4dddf
commit 4e3d2f98cf
11 changed files with 279 additions and 15 deletions

View File

@ -1,3 +1,4 @@
from datetime import datetime
from typing import Dict, Union from typing import Dict, Union
from flask import Blueprint, current_app, request from flask import Blueprint, current_app, request
@ -40,6 +41,7 @@ def get_application_config() -> Union[Dict, HttpResponse]:
{ {
"data": { "data": {
"about": null,
"admin_contact": "admin@example.com", "admin_contact": "admin@example.com",
"gpx_limit_import": 10, "gpx_limit_import": 10,
"is_email_sending_enabled": true, "is_email_sending_enabled": true,
@ -48,6 +50,8 @@ def get_application_config() -> Union[Dict, HttpResponse]:
"max_users": 0, "max_users": 0,
"max_zip_file_size": 10485760, "max_zip_file_size": 10485760,
"map_attribution": "&copy; <a href=http://www.openstreetmap.org/copyright>OpenStreetMap</a> contributors", "map_attribution": "&copy; <a href=http://www.openstreetmap.org/copyright>OpenStreetMap</a> contributors",
"privacy_policy": null,
"privacy_policy_date": null,
"version": "0.7.12", "version": "0.7.12",
"weather_provider": null "weather_provider": null
}, },
@ -93,6 +97,7 @@ def update_application_config(auth_user: User) -> Union[Dict, HttpResponse]:
{ {
"data": { "data": {
"about": null,
"admin_contact": "admin@example.com", "admin_contact": "admin@example.com",
"gpx_limit_import": 10, "gpx_limit_import": 10,
"is_email_sending_enabled": true, "is_email_sending_enabled": true,
@ -101,18 +106,22 @@ def update_application_config(auth_user: User) -> Union[Dict, HttpResponse]:
"max_users": 10, "max_users": 10,
"max_zip_file_size": 10485760, "max_zip_file_size": 10485760,
"map_attribution": "&copy; <a href=http://www.openstreetmap.org/copyright>OpenStreetMap</a> contributors", "map_attribution": "&copy; <a href=http://www.openstreetmap.org/copyright>OpenStreetMap</a> contributors",
"privacy_policy": null,
"privacy_policy_date": null,
"version": "0.7.12", "version": "0.7.12",
"weather_provider": null "weather_provider": null
}, },
"status": "success" "status": "success"
} }
:<json string about: instance information
:<json string admin_contact: email to contact the administrator :<json string admin_contact: email to contact the administrator
:<json integer gpx_limit_import: max number of files in zip archive :<json integer gpx_limit_import: max number of files in zip archive
:<json boolean is_registration_enabled: is registration enabled? :<json boolean is_registration_enabled: is registration enabled?
:<json integer max_single_file_size: max size of a single file :<json integer max_single_file_size: max size of a single file
:<json integer max_users: max users allowed to register on instance :<json integer max_users: max users allowed to register on instance
:<json integer max_zip_file_size: max size of a zip archive :<json integer max_zip_file_size: max size of a zip archive
:<json string privacy_policy: instance privacy policy
:reqheader Authorization: OAuth 2.0 Bearer Token :reqheader Authorization: OAuth 2.0 Bearer Token
@ -151,6 +160,11 @@ def update_application_config(auth_user: User) -> Union[Dict, HttpResponse]:
config.max_users = config_data.get('max_users') config.max_users = config_data.get('max_users')
if 'admin_contact' in config_data: if 'admin_contact' in config_data:
config.admin_contact = admin_contact if admin_contact else None config.admin_contact = admin_contact if admin_contact else None
if 'about' in config_data:
config.about = config_data.get('about')
if 'privacy_policy' in config_data:
config.privacy_policy = config_data.get('privacy_policy')
config.privacy_policy_date = datetime.utcnow()
if config.max_zip_file_size < config.max_single_file_size: if config.max_zip_file_size < config.max_single_file_size:
return InvalidPayloadErrorResponse( return InvalidPayloadErrorResponse(

View File

@ -25,6 +25,9 @@ class AppConfig(BaseModel):
) )
max_zip_file_size = db.Column(db.Integer, default=10485760, nullable=False) max_zip_file_size = db.Column(db.Integer, default=10485760, nullable=False)
admin_contact = db.Column(db.String(255), nullable=True) admin_contact = db.Column(db.String(255), nullable=True)
privacy_policy_date = db.Column(db.DateTime, nullable=True)
privacy_policy = db.Column(db.Text, nullable=True)
about = db.Column(db.Text, nullable=True)
@property @property
def is_registration_enabled(self) -> bool: def is_registration_enabled(self) -> bool:
@ -46,6 +49,7 @@ class AppConfig(BaseModel):
def serialize(self) -> Dict: def serialize(self) -> Dict:
weather_provider = os.getenv('WEATHER_API_PROVIDER', '').lower() weather_provider = os.getenv('WEATHER_API_PROVIDER', '').lower()
return { return {
'about': self.about,
'admin_contact': self.admin_contact, 'admin_contact': self.admin_contact,
'gpx_limit_import': self.gpx_limit_import, 'gpx_limit_import': self.gpx_limit_import,
'is_email_sending_enabled': current_app.config['CAN_SEND_EMAILS'], 'is_email_sending_enabled': current_app.config['CAN_SEND_EMAILS'],
@ -54,6 +58,8 @@ class AppConfig(BaseModel):
'max_zip_file_size': self.max_zip_file_size, 'max_zip_file_size': self.max_zip_file_size,
'max_users': self.max_users, 'max_users': self.max_users,
'map_attribution': self.map_attribution, 'map_attribution': self.map_attribution,
'privacy_policy': self.privacy_policy,
'privacy_policy_date': self.privacy_policy_date,
'version': VERSION, 'version': VERSION,
'weather_provider': ( 'weather_provider': (
weather_provider weather_provider

View File

@ -0,0 +1,48 @@
"""add privacy policy
Revision ID: 374a670efe23
Revises: 0f375c44e659
Create Date: 2023-02-25 11:08:08.977217
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '374a670efe23'
down_revision = '0f375c44e659'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('app_config', schema=None) as batch_op:
batch_op.add_column(sa.Column('privacy_policy_date', sa.DateTime(), nullable=True))
batch_op.add_column(sa.Column('privacy_policy', sa.Text(), nullable=True))
batch_op.add_column(sa.Column('about', sa.Text(), nullable=True))
with op.batch_alter_table('users', schema=None) as batch_op:
batch_op.add_column(sa.Column('accepted_policy_date', sa.DateTime(), nullable=True))
batch_op.alter_column('date_format',
existing_type=sa.VARCHAR(length=50),
nullable=True)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('users', schema=None) as batch_op:
batch_op.alter_column('date_format',
existing_type=sa.VARCHAR(length=50),
nullable=False)
batch_op.drop_column('accepted_policy_date')
with op.batch_alter_table('app_config', schema=None) as batch_op:
batch_op.drop_column('about')
batch_op.drop_column('privacy_policy')
batch_op.drop_column('privacy_policy_date')
# ### end Alembic commands ###

View File

@ -1,5 +1,7 @@
import json import json
from datetime import datetime
from typing import Optional from typing import Optional
from unittest.mock import Mock, patch
import pytest import pytest
from flask import Flask from flask import Flask
@ -296,7 +298,7 @@ class TestUpdateConfig(ApiTestCaseMixin):
@pytest.mark.parametrize( @pytest.mark.parametrize(
'input_description,input_email', [('input string', ''), ('None', None)] 'input_description,input_email', [('input string', ''), ('None', None)]
) )
def test_it_empties_error_if_admin_contact_is_an_empty( def test_it_empties_contact_if_provided_admin_contact_is_an_empty(
self, self,
app: Flask, app: Flask,
user_1_admin: User, user_1_admin: User,
@ -325,6 +327,66 @@ class TestUpdateConfig(ApiTestCaseMixin):
assert 'success' in data['status'] assert 'success' in data['status']
assert data['data']['admin_contact'] is None assert data['data']['admin_contact'] is None
def test_it_updates_about(
self,
app: Flask,
user_1_admin: User,
) -> None:
client, auth_token = self.get_test_client_and_auth_token(
app, user_1_admin.email
)
about = self.random_string()
response = client.patch(
'/api/config',
content_type='application/json',
data=json.dumps(
dict(
about=about,
)
),
headers=dict(Authorization=f'Bearer {auth_token}'),
)
assert response.status_code == 200
data = json.loads(response.data.decode())
assert 'success' in data['status']
assert data['data']['about'] == about
def test_it_updates_privacy_policy(
self,
app: Flask,
user_1_admin: User,
) -> None:
client, auth_token = self.get_test_client_and_auth_token(
app, user_1_admin.email
)
privacy_policy = self.random_string()
privacy_policy_date = datetime.utcnow()
with patch(
'fittrackee.application.app_config.datetime'
) as datetime_mock:
datetime_mock.utcnow = Mock(return_value=privacy_policy_date)
response = client.patch(
'/api/config',
content_type='application/json',
data=json.dumps(
dict(
privacy_policy=privacy_policy,
)
),
headers=dict(Authorization=f'Bearer {auth_token}'),
)
assert response.status_code == 200
data = json.loads(response.data.decode())
assert 'success' in data['status']
assert data['data']['privacy_policy'] == privacy_policy
assert data['data'][
'privacy_policy_date'
] == privacy_policy_date.strftime('%a, %d %b %Y %H:%M:%S GMT')
@pytest.mark.parametrize( @pytest.mark.parametrize(
'client_scope, can_access', 'client_scope, can_access',
[ [

View File

@ -1,3 +1,5 @@
from datetime import datetime
import pytest import pytest
from flask import Flask from flask import Flask
@ -5,6 +7,8 @@ from fittrackee import VERSION
from fittrackee.application.models import AppConfig from fittrackee.application.models import AppConfig
from fittrackee.users.models import User from fittrackee.users.models import User
from ..utils import random_string
class TestConfigModel: class TestConfigModel:
def test_application_config( def test_application_config(
@ -88,3 +92,26 @@ class TestConfigModel:
serialized_app_config['weather_provider'] serialized_app_config['weather_provider']
== expected_weather_provider == expected_weather_provider
) )
def test_it_returns_privacy_policy(self, app: Flask) -> None:
app_config = AppConfig.query.first()
privacy_policy = random_string()
privacy_policy_date = datetime.now()
app_config.privacy_policy = privacy_policy
app_config.privacy_policy_date = privacy_policy_date
serialized_app_config = app_config.serialize()
assert serialized_app_config["privacy_policy"] == privacy_policy
assert (
serialized_app_config["privacy_policy_date"] == privacy_policy_date
)
def test_it_returns_about(self, app: Flask) -> None:
app_config = AppConfig.query.first()
about = random_string()
app_config.about = about
serialized_app_config = app_config.serialize()
assert serialized_app_config["about"] == about

View File

@ -13,6 +13,7 @@ from ..utils import random_string
def user_1() -> User: def user_1() -> User:
user = User(username='test', email='test@test.com', password='12345678') user = User(username='test', email='test@test.com', password='12345678')
user.is_active = True user.is_active = True
user.accepted_policy = datetime.datetime.utcnow()
db.session.add(user) db.session.add(user)
db.session.commit() db.session.commit()
return user return user
@ -22,6 +23,7 @@ def user_1() -> User:
def user_1_upper() -> User: def user_1_upper() -> User:
user = User(username='TEST', email='TEST@TEST.COM', password='12345678') user = User(username='TEST', email='TEST@TEST.COM', password='12345678')
user.is_active = True user.is_active = True
user.accepted_policy = datetime.datetime.utcnow()
db.session.add(user) db.session.add(user)
db.session.commit() db.session.commit()
return user return user
@ -34,6 +36,7 @@ def user_1_admin() -> User:
) )
admin.admin = True admin.admin = True
admin.is_active = True admin.is_active = True
admin.accepted_policy = datetime.datetime.utcnow()
db.session.add(admin) db.session.add(admin)
db.session.commit() db.session.commit()
return admin return admin
@ -50,6 +53,7 @@ def user_1_full() -> User:
user.timezone = 'America/New_York' user.timezone = 'America/New_York'
user.birth_date = datetime.datetime.strptime('01/01/1980', '%d/%m/%Y') user.birth_date = datetime.datetime.strptime('01/01/1980', '%d/%m/%Y')
user.is_active = True user.is_active = True
user.accepted_policy = datetime.datetime.utcnow()
db.session.add(user) db.session.add(user)
db.session.commit() db.session.commit()
return user return user
@ -60,6 +64,7 @@ def user_1_paris() -> User:
user = User(username='test', email='test@test.com', password='12345678') user = User(username='test', email='test@test.com', password='12345678')
user.timezone = 'Europe/Paris' user.timezone = 'Europe/Paris'
user.is_active = True user.is_active = True
user.accepted_policy = datetime.datetime.utcnow()
db.session.add(user) db.session.add(user)
db.session.commit() db.session.commit()
return user return user
@ -69,6 +74,7 @@ def user_1_paris() -> User:
def user_2() -> User: def user_2() -> User:
user = User(username='toto', email='toto@toto.com', password='12345678') user = User(username='toto', email='toto@toto.com', password='12345678')
user.is_active = True user.is_active = True
user.accepted_policy = datetime.datetime.utcnow()
db.session.add(user) db.session.add(user)
db.session.commit() db.session.commit()
return user return user
@ -79,6 +85,7 @@ def user_2_admin() -> User:
user = User(username='toto', email='toto@toto.com', password='12345678') user = User(username='toto', email='toto@toto.com', password='12345678')
user.is_active = True user.is_active = True
user.admin = True user.admin = True
user.accepted_policy = datetime.datetime.utcnow()
db.session.add(user) db.session.add(user)
db.session.commit() db.session.commit()
return user return user
@ -89,6 +96,7 @@ def user_3() -> User:
user = User(username='sam', email='sam@test.com', password='12345678') user = User(username='sam', email='sam@test.com', password='12345678')
user.is_active = True user.is_active = True
user.weekm = True user.weekm = True
user.accepted_policy = datetime.datetime.utcnow()
db.session.add(user) db.session.add(user)
db.session.commit() db.session.commit()
return user return user
@ -100,6 +108,7 @@ def inactive_user() -> User:
username='inactive', email='inactive@example.com', password='12345678' username='inactive', email='inactive@example.com', password='12345678'
) )
user.confirmation_token = random_string() user.confirmation_token = random_string()
user.accepted_policy = datetime.datetime.utcnow()
db.session.add(user) db.session.add(user)
db.session.commit() db.session.commit()
return user return user

View File

@ -33,6 +33,48 @@ class TestUserRegistration(ApiTestCaseMixin):
self.assert_400(response) self.assert_400(response)
def test_it_returns_error_if_accepted_policy_is_missing(
self, app: Flask
) -> None:
client = app.test_client()
response = client.post(
'/api/auth/register',
data=json.dumps(
dict(
username=self.random_string(),
email=self.random_email(),
password=self.random_string(),
)
),
content_type='application/json',
)
self.assert_400(response)
def test_it_returns_error_if_accepted_policy_is_false(
self, app: Flask
) -> None:
client = app.test_client()
response = client.post(
'/api/auth/register',
data=json.dumps(
dict(
username=self.random_string(),
email=self.random_email(),
password=self.random_string(),
accepted_policy=False,
)
),
content_type='application/json',
)
self.assert_400(
response,
'sorry, you must agree privacy policy to register',
)
def test_it_returns_error_if_username_is_missing(self, app: Flask) -> None: def test_it_returns_error_if_username_is_missing(self, app: Flask) -> None:
client = app.test_client() client = app.test_client()
@ -42,6 +84,7 @@ class TestUserRegistration(ApiTestCaseMixin):
dict( dict(
email=self.random_email(), email=self.random_email(),
password=self.random_string(), password=self.random_string(),
accepted_policy=True,
) )
), ),
content_type='application/json', content_type='application/json',
@ -65,6 +108,7 @@ class TestUserRegistration(ApiTestCaseMixin):
username=self.random_string(length=input_username_length), username=self.random_string(length=input_username_length),
email=self.random_email(), email=self.random_email(),
password=self.random_string(), password=self.random_string(),
accepted_policy=True,
) )
), ),
content_type='application/json', content_type='application/json',
@ -91,6 +135,7 @@ class TestUserRegistration(ApiTestCaseMixin):
username=input_username, username=input_username,
email=self.random_email(), email=self.random_email(),
password=self.random_email(), password=self.random_email(),
accepted_policy=True,
) )
), ),
content_type='application/json', content_type='application/json',
@ -121,6 +166,7 @@ class TestUserRegistration(ApiTestCaseMixin):
), ),
email=self.random_email(), email=self.random_email(),
password=self.random_string(), password=self.random_string(),
accepted_policy=True,
) )
), ),
content_type='application/json', content_type='application/json',
@ -137,6 +183,7 @@ class TestUserRegistration(ApiTestCaseMixin):
dict( dict(
username=self.random_string(), username=self.random_string(),
email=self.random_email(), email=self.random_email(),
accepted_policy=True,
) )
), ),
content_type='application/json', content_type='application/json',
@ -156,6 +203,7 @@ class TestUserRegistration(ApiTestCaseMixin):
username=self.random_string(), username=self.random_string(),
email=self.random_email(), email=self.random_email(),
password=self.random_string(length=7), password=self.random_string(length=7),
accepted_policy=True,
) )
), ),
content_type='application/json', content_type='application/json',
@ -172,6 +220,7 @@ class TestUserRegistration(ApiTestCaseMixin):
dict( dict(
username=self.random_string(), username=self.random_string(),
password=self.random_string(), password=self.random_string(),
accepted_policy=True,
) )
), ),
content_type='application/json', content_type='application/json',
@ -189,6 +238,7 @@ class TestUserRegistration(ApiTestCaseMixin):
username=self.random_string(), username=self.random_string(),
email=self.random_string(), email=self.random_string(),
password=self.random_string(), password=self.random_string(),
accepted_policy=True,
) )
), ),
content_type='application/json', content_type='application/json',
@ -207,6 +257,7 @@ class TestUserRegistration(ApiTestCaseMixin):
dict( dict(
username=self.random_string(), username=self.random_string(),
email=self.random_string(), email=self.random_string(),
accepted_policy=True,
) )
), ),
content_type='application/json', content_type='application/json',
@ -224,6 +275,7 @@ class TestUserRegistration(ApiTestCaseMixin):
username=self.random_string(), username=self.random_string(),
email=self.random_email(), email=self.random_email(),
password=self.random_string(), password=self.random_string(),
accepted_policy=True,
) )
), ),
content_type='application/json', content_type='application/json',
@ -248,6 +300,7 @@ class TestUserRegistration(ApiTestCaseMixin):
username=username, username=username,
email=self.random_email(), email=self.random_email(),
password=self.random_string(), password=self.random_string(),
accepted_policy=True,
) )
), ),
content_type='application/json', content_type='application/json',
@ -269,7 +322,10 @@ class TestUserRegistration(ApiTestCaseMixin):
client = app.test_client() client = app.test_client()
username = self.random_string() username = self.random_string()
email = self.random_email() email = self.random_email()
accepted_policy_date = datetime.utcnow()
with patch('fittrackee.users.auth.datetime.datetime') as datetime_mock:
datetime_mock.utcnow = Mock(return_value=accepted_policy_date)
client.post( client.post(
'/api/auth/register', '/api/auth/register',
data=json.dumps( data=json.dumps(
@ -278,6 +334,7 @@ class TestUserRegistration(ApiTestCaseMixin):
email=email, email=email,
password=self.random_string(), password=self.random_string(),
language=input_language, language=input_language,
accepted_policy=True,
) )
), ),
content_type='application/json', content_type='application/json',
@ -288,6 +345,7 @@ class TestUserRegistration(ApiTestCaseMixin):
assert new_user.password is not None assert new_user.password is not None
assert new_user.is_active is False assert new_user.is_active is False
assert new_user.language == expected_language assert new_user.language == expected_language
assert new_user.accepted_policy_date == accepted_policy_date
@pytest.mark.parametrize( @pytest.mark.parametrize(
'input_language,expected_language', 'input_language,expected_language',
@ -314,6 +372,7 @@ class TestUserRegistration(ApiTestCaseMixin):
email=email, email=email,
password='12345678', password='12345678',
language=input_language, language=input_language,
accepted_policy=True,
) )
), ),
content_type='application/json', content_type='application/json',
@ -353,6 +412,7 @@ class TestUserRegistration(ApiTestCaseMixin):
username=username, username=username,
email=email, email=email,
password='12345678', password='12345678',
accepted_policy=True,
) )
), ),
content_type='application/json', content_type='application/json',
@ -381,6 +441,7 @@ class TestUserRegistration(ApiTestCaseMixin):
else user_1.email.lower() else user_1.email.lower()
), ),
password=self.random_string(), password=self.random_string(),
accepted_policy=True,
) )
), ),
content_type='application/json', content_type='application/json',
@ -404,6 +465,7 @@ class TestUserRegistration(ApiTestCaseMixin):
username=self.random_string(), username=self.random_string(),
email=user_1.email, email=user_1.email,
password=self.random_string(), password=self.random_string(),
accepted_policy=True,
) )
), ),
content_type='application/json', content_type='application/json',
@ -1983,6 +2045,7 @@ class TestRegistrationConfiguration(ApiTestCaseMixin):
username=self.random_string(), username=self.random_string(),
email=self.random_email(), email=self.random_email(),
password=self.random_string(), password=self.random_string(),
accepted_policy=True,
) )
), ),
content_type='application/json', content_type='application/json',
@ -1995,6 +2058,7 @@ class TestRegistrationConfiguration(ApiTestCaseMixin):
username=self.random_string(), username=self.random_string(),
email=self.random_email(), email=self.random_email(),
password=self.random_string(), password=self.random_string(),
accepted_policy=True,
) )
), ),
content_type='application/json', content_type='application/json',
@ -2015,6 +2079,7 @@ class TestRegistrationConfiguration(ApiTestCaseMixin):
username=self.random_string(), username=self.random_string(),
email=self.random_email(), email=self.random_email(),
password=self.random_string(), password=self.random_string(),
accepted_policy=True,
) )
), ),
content_type='application/json', content_type='application/json',
@ -2027,6 +2092,7 @@ class TestRegistrationConfiguration(ApiTestCaseMixin):
username=self.random_string(), username=self.random_string(),
email=self.random_email(), email=self.random_email(),
password=self.random_string(), password=self.random_string(),
accepted_policy=True,
) )
), ),
content_type='application/json', content_type='application/json',

View File

@ -1624,7 +1624,7 @@ class TestDeleteUser(ApiTestCaseMixin):
'you can not delete your account, no other user has admin rights', 'you can not delete your account, no other user has admin rights',
) )
def test_it_enables_registration_after_user_delete( def test_it_enables_registration_after_user_delete_when_users_count_is_below_limit( # noqa
self, self,
app_with_3_users_max: Flask, app_with_3_users_max: Flask,
user_1_admin: User, user_1_admin: User,
@ -1646,6 +1646,7 @@ class TestDeleteUser(ApiTestCaseMixin):
username=self.random_string(), username=self.random_string(),
email=self.random_email(), email=self.random_email(),
password=self.random_string(), password=self.random_string(),
accepted_policy=True,
) )
), ),
content_type='application/json', content_type='application/json',
@ -1653,7 +1654,7 @@ class TestDeleteUser(ApiTestCaseMixin):
assert response.status_code == 200 assert response.status_code == 200
def test_it_does_not_enable_registration_on_user_delete( def test_it_does_not_enable_registration_on_user_delete_when_users_count_is_not_below_limit( # noqa
self, self,
app_with_3_users_max: Flask, app_with_3_users_max: Flask,
user_1_admin: User, user_1_admin: User,
@ -1677,6 +1678,7 @@ class TestDeleteUser(ApiTestCaseMixin):
email='test@test.com', email='test@test.com',
password='12345678', password='12345678',
password_conf='12345678', password_conf='12345678',
accepted_policy=True,
) )
), ),
content_type='application/json', content_type='application/json',

View File

@ -78,6 +78,16 @@ class TestUserSerializeAsAuthUser(UserModelAssertMixin):
self.assert_workouts_keys_are_present(serialized_user) self.assert_workouts_keys_are_present(serialized_user)
def test_it_returns_accepted_privacy_policy_date(
self, app: Flask, user_1: User
) -> None:
serialized_user = user_1.serialize(user_1)
assert (
serialized_user['accepted_policy_date']
== user_1.accepted_policy_date
)
def test_it_does_not_return_confirmation_token( def test_it_does_not_return_confirmation_token(
self, app: Flask, user_1_admin: User, user_2: User self, app: Flask, user_1_admin: User, user_2: User
) -> None: ) -> None:
@ -118,6 +128,13 @@ class TestUserSerializeAsAdmin(UserModelAssertMixin):
self.assert_workouts_keys_are_present(serialized_user) self.assert_workouts_keys_are_present(serialized_user)
def test_it_does_not_return_accepted_privacy_policy_date(
self, app: Flask, user_1_admin: User, user_2: User
) -> None:
serialized_user = user_2.serialize(user_1_admin)
assert 'accepted_policy_date' not in serialized_user
def test_it_does_not_return_confirmation_token( def test_it_does_not_return_confirmation_token(
self, app: Flask, user_1_admin: User, user_2: User self, app: Flask, user_1_admin: User, user_2: User
) -> None: ) -> None:

View File

@ -115,6 +115,7 @@ def register_user() -> Union[Tuple[Dict, int], HttpResponse]:
:<json string password: password (8 characters required) :<json string password: password (8 characters required)
:<json string lang: user language preferences (if not provided or invalid, :<json string lang: user language preferences (if not provided or invalid,
fallback to 'en' (english)) fallback to 'en' (english))
:<json boolean accepted_policy: true if user accepted privacy policy
:statuscode 200: success :statuscode 200: success
:statuscode 400: :statuscode 400:
@ -141,8 +142,16 @@ def register_user() -> Union[Tuple[Dict, int], HttpResponse]:
or post_data.get('username') is None or post_data.get('username') is None
or post_data.get('email') is None or post_data.get('email') is None
or post_data.get('password') is None or post_data.get('password') is None
or post_data.get('accepted_policy') is None
): ):
return InvalidPayloadErrorResponse() return InvalidPayloadErrorResponse()
accepted_policy = post_data.get('accepted_policy') is True
if not accepted_policy:
return InvalidPayloadErrorResponse(
'sorry, you must agree privacy policy to register'
)
username = post_data.get('username') username = post_data.get('username')
email = post_data.get('email') email = post_data.get('email')
password = post_data.get('password') password = post_data.get('password')
@ -176,6 +185,7 @@ def register_user() -> Union[Tuple[Dict, int], HttpResponse]:
new_user.date_format = 'MM/dd/yyyy' new_user.date_format = 'MM/dd/yyyy'
new_user.confirmation_token = secrets.token_urlsafe(30) new_user.confirmation_token = secrets.token_urlsafe(30)
new_user.language = language new_user.language = language
new_user.accepted_policy_date = datetime.datetime.utcnow()
db.session.add(new_user) db.session.add(new_user)
db.session.commit() db.session.commit()
@ -288,6 +298,7 @@ def get_authenticated_user_profile(
{ {
"data": { "data": {
"accepted_privacy_policy": "Sat, 25 Fev 2023 13:52:58 GMT",
"admin": false, "admin": false,
"bio": null, "bio": null,
"birth_date": null, "birth_date": null,

View File

@ -52,6 +52,7 @@ class User(BaseModel):
email_to_confirm = db.Column(db.String(255), nullable=True) email_to_confirm = db.Column(db.String(255), nullable=True)
confirmation_token = db.Column(db.String(255), nullable=True) confirmation_token = db.Column(db.String(255), nullable=True)
display_ascent = db.Column(db.Boolean, default=True, nullable=False) display_ascent = db.Column(db.Boolean, default=True, nullable=False)
accepted_policy_date = db.Column(db.DateTime, nullable=True)
def __repr__(self) -> str: def __repr__(self) -> str:
return f'<User {self.username!r}>' return f'<User {self.username!r}>'
@ -191,6 +192,7 @@ class User(BaseModel):
serialized_user = { serialized_user = {
**serialized_user, **serialized_user,
**{ **{
'accepted_policy_date': self.accepted_policy_date,
'date_format': self.date_format, 'date_format': self.date_format,
'display_ascent': self.display_ascent, 'display_ascent': self.display_ascent,
'imperial_units': self.imperial_units, 'imperial_units': self.imperial_units,