"""
byceps.services.user.command_service
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
:Copyright: 2006-2020 Jochen Kupperschmidt
:License: Modified BSD, see LICENSE for details.
"""
from datetime import date
from typing import Any, Optional
from ...database import db
from ...events.user import (
UserAccountDeleted,
UserAccountSuspended,
UserAccountUnsuspended,
UserDetailsUpdated,
UserEmailAddressChanged,
UserScreenNameChanged,
)
from ...typing import UserID
from ..authorization.models import RoleID
from ..authorization import service as authorization_service
from . import event_service
from .models.detail import UserDetail as DbUserDetail
from .models.event import UserEventData
from .models.user import User as DbUser
from . import service as user_service
def initialize_account(
user_id: UserID,
*,
initiator_id: Optional[UserID] = None,
assign_roles: bool = True,
) -> None:
"""Initialize the user account.
This is meant to happen only once at most, and can not be undone.
"""
user = _get_user(user_id)
if user.initialized:
raise ValueError(f'Account is already initialized.')
user.initialized = True
event_data = {}
if initiator_id:
event_data['initiator_id'] = str(initiator_id)
event = event_service.build_event('user-initialized', user.id, event_data)
db.session.add(event)
db.session.commit()
if assign_roles:
_assign_roles(user.id, initiator_id=initiator_id)
def _assign_roles(
user_id: UserID, *, initiator_id: Optional[UserID] = None
) -> None:
board_user_role = authorization_service.find_role(RoleID('board_user'))
authorization_service.assign_role_to_user(
board_user_role.id, user_id, initiator_id=initiator_id
)
def suspend_account(
user_id: UserID, initiator_id: UserID, reason: str
) -> UserAccountSuspended:
"""Suspend the user account."""
user = _get_user(user_id)
user.suspended = True
event = event_service.build_event(
'user-suspended',
user.id,
{
'initiator_id': str(initiator_id),
'reason': reason,
},
)
db.session.add(event)
db.session.commit()
return UserAccountSuspended(
occurred_at=event.occurred_at,
user_id=user.id,
initiator_id=initiator_id,
)
def unsuspend_account(
user_id: UserID, initiator_id: UserID, reason: str
) -> UserAccountUnsuspended:
"""Unsuspend the user account."""
user = _get_user(user_id)
user.suspended = False
event = event_service.build_event(
'user-unsuspended',
user.id,
{
'initiator_id': str(initiator_id),
'reason': reason,
},
)
db.session.add(event)
db.session.commit()
return UserAccountUnsuspended(
occurred_at=event.occurred_at,
user_id=user.id,
initiator_id=initiator_id,
)
def delete_account(
user_id: UserID, initiator_id: UserID, reason: str
) -> UserAccountDeleted:
"""Delete the user account."""
user = _get_user(user_id)
user.deleted = True
_anonymize_account(user)
event = event_service.build_event(
'user-deleted',
user.id,
{
'initiator_id': str(initiator_id),
'reason': reason,
},
)
db.session.add(event)
# Deassign authorization roles.
authorization_service.deassign_all_roles_from_user(
user.id, initiator_id, commit=False
)
db.session.commit()
return UserAccountDeleted(
occurred_at=event.occurred_at,
user_id=user.id,
initiator_id=initiator_id,
)
def change_screen_name(
user_id: UserID,
new_screen_name: str,
initiator_id: UserID,
*,
reason: Optional[str] = None,
) -> UserScreenNameChanged:
"""Change the user's screen name."""
user = _get_user(user_id)
old_screen_name = user.screen_name
user.screen_name = new_screen_name
event_data = {
'old_screen_name': old_screen_name,
'new_screen_name': new_screen_name,
'initiator_id': str(initiator_id),
}
if reason:
event_data['reason'] = reason
event = event_service.build_event(
'user-screen-name-changed', user.id, event_data
)
db.session.add(event)
db.session.commit()
return UserScreenNameChanged(
occurred_at=event.occurred_at,
user_id=user.id,
initiator_id=initiator_id,
old_screen_name=old_screen_name,
new_screen_name=new_screen_name,
)
def change_email_address(
user_id: UserID,
new_email_address: Optional[str],
initiator_id: UserID,
*,
reason: Optional[str] = None,
) -> UserEmailAddressChanged:
"""Change the user's e-mail address."""
user = _get_user(user_id)
old_email_address = user.email_address
user.email_address = new_email_address
user.email_address_verified = False
event_data = {
'old_email_address': old_email_address,
'new_email_address': new_email_address,
'initiator_id': str(initiator_id),
}
if reason:
event_data['reason'] = reason
event = event_service.build_event(
'user-email-address-changed', user.id, event_data
)
db.session.add(event)
db.session.commit()
return UserEmailAddressChanged(
occurred_at=event.occurred_at,
user_id=user.id,
initiator_id=initiator_id,
)
def update_user_details(
user_id: UserID,
first_names: str,
last_name: str,
date_of_birth: date,
country: str,
zip_code,
city: str,
street: str,
phone_number: str,
initiator_id: UserID,
) -> UserDetailsUpdated:
"""Update the user's details."""
detail = _get_user_detail(user_id)
old_first_names = detail.first_names
old_last_name = detail.last_name
old_date_of_birth = detail.date_of_birth
old_country = detail.country
old_zip_code = detail.zip_code
old_city = detail.city
old_street = detail.street
old_phone_number = detail.phone_number
detail.first_names = first_names
detail.last_name = last_name
detail.date_of_birth = date_of_birth
detail.country = country
detail.zip_code = zip_code
detail.city = city
detail.street = street
detail.phone_number = phone_number
event_data = {
'initiator_id': str(initiator_id),
}
_add_if_different(event_data, 'first_names', old_first_names, first_names)
_add_if_different(event_data, 'last_name', old_last_name, last_name)
_add_if_different(
event_data, 'date_of_birth', old_date_of_birth, date_of_birth
)
_add_if_different(event_data, 'country', old_country, country)
_add_if_different(event_data, 'zip_code', old_zip_code, zip_code)
_add_if_different(event_data, 'city', old_city, city)
_add_if_different(event_data, 'street', old_street, street)
_add_if_different(
event_data, 'phone_number', old_phone_number, phone_number
)
event = event_service.build_event(
'user-details-updated', user_id, event_data
)
db.session.add(event)
db.session.commit()
return UserDetailsUpdated(
occurred_at=event.occurred_at,
user_id=event.user_id,
initiator_id=initiator_id,
)
def _add_if_different(
event_data: UserEventData, base_key_name: str, old_value: str, new_value
) -> None:
if old_value != new_value:
event_data[f'old_{base_key_name}'] = _to_str_if_not_none(old_value)
event_data[f'new_{base_key_name}'] = _to_str_if_not_none(new_value)
def _to_str_if_not_none(value: Any) -> Optional[str]:
return str(value) if (value is not None) else None
def set_user_detail_extra(user_id: UserID, key: str, value: str) -> None:
"""Set a value for a key in the user's detail extras map."""
detail = _get_user_detail(user_id)
if detail.extras is None:
detail.extras = {}
detail.extras[key] = value
db.session.commit()
def remove_user_detail_extra(user_id: UserID, key: str) -> None:
"""Remove the entry with that key from the user's detail extras map."""
detail = _get_user_detail(user_id)
if (detail.extras is None) or (key not in detail.extras):
return
del detail.extras[key]
db.session.commit()
def _anonymize_account(user: DbUser) -> None:
"""Remove user details from the account."""
user.screen_name = None
user.email_address = None
user.legacy_id = None
# Remove details.
user.detail.first_names = None
user.detail.last_name = None
user.detail.date_of_birth = None
user.detail.country = None
user.detail.zip_code = None
user.detail.city = None
user.detail.street = None
user.detail.phone_number = None
# Remove avatar association.
if user.avatar_selection is not None:
db.session.delete(user.avatar_selection)
def _get_user(user_id: UserID) -> DbUser:
"""Return the user with that ID, or raise an exception."""
return user_service.get_db_user(user_id)
def _get_user_detail(user_id: UserID) -> DbUserDetail:
"""Return the user's details, or raise an exception."""
detail = DbUserDetail.query \
.filter_by(user_id=user_id) \
.one_or_none()
if detail is None:
raise ValueError(f"Unknown user ID '{user_id}'")
return detail