Files
saladeaula.digital/http-api/app/rules/enrollment.py
2025-05-25 12:02:28 -03:00

272 lines
7.6 KiB
Python

from datetime import timedelta
from enum import Enum
from typing import Literal, TypedDict
from uuid import uuid4
from layercake.dateutils import now, ttl
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair, TransactItems
from layercake.strutils import md5_hash
from conf import ORDER_TABLE
from models import Course, Enrollment
class Tenant(TypedDict):
id: str
name: str
class Author(TypedDict):
id: str
name: str
class Vacancy(TypedDict): ...
class Rel(TypedDict):
id: str
scope: Literal['ORG', 'USER', 'ENROLLMENT']
class LifecycleEvents(str, Enum):
"""Schedules lifecycle events."""
# Reminder if the user does not access within 3 days
REMINDER_NO_ACCESS_3_DAYS = 'schedules#reminder_no_access_3_days'
# When there is no activity 7 days after the first access
NO_ACTIVITY_7_DAYS = 'schedules#no_activity_7_days'
# When the access period expires
ACCESS_PERIOD_EXPIRED = 'schedules#access_period_expired'
# When the course certificate expires
CERTIFICATE_EXPIRATION = 'schedules#certificate_expiration'
# Archive the course after the certificate expires
COURSE_ARCHIVED = 'schedules#course_archived'
def enroll(
enrollment: Enrollment,
*,
tenant: Tenant,
rel: tuple[Rel, ...] | Rel = (),
author: Author | None = None,
vacancy: Vacancy | None = None,
ensure_vacancy: bool = True,
persistence_layer: DynamoDBPersistenceLayer,
) -> bool:
"""Enrolls a user into a course and schedules lifecycle events."""
now_ = now()
user = enrollment.user
course = enrollment.course
exp_interval = course.exp_interval
lock_hash = md5_hash('%s%s' % (user.id, course.id))
ttl_date = now_ + timedelta(days=exp_interval - 30)
transact = TransactItems(persistence_layer.table_name)
transact.put(
item={
'sk': '0',
'create_date': now_,
'metadata__tenant_id': tenant['id'],
**enrollment.model_dump(),
},
)
transact.put(
item={
'id': enrollment.id,
'sk': 'metadata#tenant',
'org_id': tenant['id'],
'name': tenant['name'],
'create_date': now_,
},
)
transact.put(
item={
'id': enrollment.id,
'sk': LifecycleEvents.COURSE_ARCHIVED,
'create_date': now_,
'ttl': ttl(days=exp_interval, start_dt=now_),
},
)
transact.put(
item={
'id': enrollment.id,
'sk': LifecycleEvents.REMINDER_NO_ACCESS_3_DAYS,
'name': user.name,
'email': user.email,
'course': course.name,
'create_date': now_,
'ttl': ttl(days=3, start_dt=now_),
},
)
transact.put(
item={
'id': enrollment.id,
'sk': LifecycleEvents.ACCESS_PERIOD_EXPIRED,
'name': user.name,
'email': user.email,
'course': course.name,
'create_date': now_,
'ttl': ttl(start_dt=now_ + timedelta(days=course.access_period - 30)),
},
)
for r in rel:
print(r['id'])
transact.put(
item={
'id': enrollment.id,
# 'sk': 'rel#{}' % r['id'],
'create_date': now_,
},
)
if author:
transact.put(
item={
'id': enrollment.id,
'sk': 'metadata#author',
'user_id': author['id'],
'name': author['name'],
'create_date': now_,
},
)
if vacancy:
transact.put(
item={
'id': enrollment.id,
'sk': 'parent_vacancy',
# 'vacancy': vacancy.model_dump(),
}
)
if ensure_vacancy:
# Ensures that there's a vacancy
transact.delete(
key=vacancy.model_dump(),
cond_expr='attribute_exists(sk)',
)
# Add cancel policy if there is a vacancy
if vacancy:
transact.put(
item={
'id': enrollment.id,
'sk': 'metadata#cancel_policy',
'create_date': now_,
}
)
# To ensure that the user does not enroll in the same course again until
# the certificate expires.
transact.put(
item={
'id': 'metadata#lock',
'sk': lock_hash,
'enrollment_id': enrollment.id,
'create_date': vacancy,
'ttl': ttl(start_dt=ttl_date),
},
cond_expr='attribute_not_exists(sk)',
)
transact.put(
item={
'id': enrollment.id,
'sk': 'lock',
'hash': lock_hash,
'create_date': vacancy,
'ttl': ttl(start_dt=ttl_date),
},
)
return persistence_layer.transact_write_items(transact)
def set_status_as_canceled(
id: str,
*,
lock_hash: str,
author: Author,
course: Course | None = None,
vacancy_key: KeyPair | None = None,
persistence_layer: DynamoDBPersistenceLayer,
):
"""Cancel the enrollment if there's a `cancel_policy`
and put its vacancy back if `vacancy_key` is provided."""
now_ = now()
transact = TransactItems(persistence_layer.table_name)
transact.update(
key=KeyPair(id, '0'),
update_expr='SET #status = :canceled, update_date = :update',
expr_attr_names={
'#status': 'status',
},
expr_attr_values={
':canceled': 'CANCELED',
':update': now_,
},
)
transact.put(
item={
'id': id,
'sk': 'canceled_date',
'author': author,
'create_date': now_,
},
)
transact.delete(
key=KeyPair(id, 'cancel_policy'),
cond_expr='attribute_exists(sk)',
)
# Remove schedules lifecycle events, referencies and locks
transact.delete(key=KeyPair(id, 'schedules#archive_it'))
transact.delete(key=KeyPair(id, 'schedules#no_activity'))
transact.delete(key=KeyPair(id, 'schedules#access_period_ends'))
transact.delete(key=KeyPair(id, 'schedules#does_not_access'))
transact.delete(key=KeyPair(id, 'parent_vacancy'))
transact.delete(key=KeyPair(id, 'lock'))
transact.delete(key=KeyPair('lock', lock_hash))
if vacancy_key and course:
vacancy_pk, vacancy_sk = vacancy_key.values()
org_id = vacancy_pk.removeprefix('vacancies#')
order_id, enrollment_id = vacancy_sk.split('#')
transact.condition(
key=KeyPair(order_id, '0'),
cond_expr='attribute_exists(id)',
table_name=ORDER_TABLE,
)
# Put the vacancy back and assign a new ID
transact.put(
item={
'id': f'vacancies#{org_id}',
'sk': f'{order_id}#{uuid4()}',
'course': course,
'create_date': now_,
},
cond_expr='attribute_not_exists(sk)',
)
# Set the status of `generated_items` to `ROLLBACK` to know
# which vacancy is available for reuse
transact.update(
key=KeyPair(order_id, f'generated_items#{enrollment_id}'),
update_expr='SET #status = :status, update_date = :update',
expr_attr_names={
'#status': 'status',
},
expr_attr_values={
':status': 'ROLLBACK',
':update': now_,
},
cond_expr='attribute_exists(sk)',
table_name=ORDER_TABLE,
)
return persistence_layer.transact_write_items(transact)