Files
saladeaula.digital/konviva-events/app/enrollment.py

362 lines
11 KiB
Python

from decimal import Decimal
from aws_lambda_powertools.event_handler.exceptions import (
BadRequestError,
NotFoundError,
)
from botocore.args import logger
from glom import glom
from layercake.dateutils import now, ttl
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair, SortKey, TransactKey
from layercake.strutils import md5_hash
def update_progress(
id: str,
progress: Decimal,
*,
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
):
now_ = now()
reminder_ttl = ttl(start_dt=now_, days=7)
try:
with dynamodb_persistence_layer.transact_writer() as transact:
# Update progress only if the enrollment status is `IN_PROGRESS`
transact.update(
key=KeyPair(id, '0'),
update_expr='SET progress = :progress, \
updated_at = :now',
cond_expr='#status = :in_progress',
expr_attr_names={
'#status': 'status',
},
expr_attr_values={
':in_progress': 'IN_PROGRESS',
':progress': progress,
':now': now_,
},
exc_cls=EnrollmentConflictError,
)
# Schedule a reminder if there is no activity after 7 days
transact.put(
item={
'id': id,
'sk': 'SCHEDULE#REMINDER_NO_ACTIVITY_AFTER_7_DAYS',
'ttl': reminder_ttl,
'created_at': now_,
}
)
except EnrollmentConflictError:
with dynamodb_persistence_layer.transact_writer() as transact:
# If the enrollment status is `PENDING`, set it to `IN_PROGRESS`
# and update `progress` and `updated_at`
transact.update(
key=KeyPair(id, '0'),
update_expr='SET progress = :progress, \
#status = :in_progress, \
started_at = :now, \
updated_at = :now',
cond_expr='#status = :pending',
expr_attr_names={
'#status': 'status',
},
expr_attr_values={
':in_progress': 'IN_PROGRESS',
':pending': 'PENDING',
':progress': progress,
':now': now_,
},
exc_cls=EnrollmentConflictError,
)
# Record the start date if it does not already exist
transact.put(
item={
'id': id,
'sk': 'STARTED',
'created_at': now_,
},
cond_expr='attribute_not_exists(sk)',
)
# Schedule a reminder for inactivity
transact.put(
item={
'id': id,
'sk': 'SCHEDULE#REMINDER_NO_ACTIVITY_AFTER_7_DAYS',
'ttl': reminder_ttl,
'created_at': now_,
}
)
# Remove reminders and policies that no longer apply
transact.delete(
key=KeyPair(
pk=id,
sk='CANCEL_POLICY',
),
)
transact.delete(
key=KeyPair(
pk=id,
sk='SCHEDULE#REMINDER_NO_ACCESS_AFTER_3_DAYS',
)
)
return True
def set_score(
id: str,
/,
score: Decimal,
progress: Decimal,
*,
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
):
enrollment = dynamodb_persistence_layer.collection.get_items(
TransactKey(id)
+ SortKey('0')
+ SortKey(
sk='METADATA#COURSE',
# Prevent conflicts with `course`
rename_key='metadata__course',
)
+ SortKey(
sk='METADATA#DEDUPLICATION_WINDOW',
path_spec='offset_days',
rename_key='dedup_window_offset_days',
),
)
user_id = enrollment['user']['id']
course_id = glom(enrollment, 'course.id')
try:
if score >= 70:
# Got a score of 70 or higher
return _set_status_as_completed(
id,
score,
progress=progress,
user_id=user_id,
course_id=course_id,
cert_exp_interval=int(
glom(enrollment, 'metadata__course.cert.exp_interval', default=0)
),
dedup_window_offset_days=int(enrollment['dedup_window_offset_days']),
dynamodb_persistence_layer=dynamodb_persistence_layer,
)
# Got a score below 70
return _set_status_as_failed(
id,
score,
progress=progress,
user_id=user_id,
course_id=course_id,
dynamodb_persistence_layer=dynamodb_persistence_layer,
)
except EnrollmentConflictError as err:
logger.exception(err)
raise
def _set_status_as_completed(
id: str,
/,
score: Decimal,
progress: Decimal,
*,
user_id: str,
course_id: str,
cert_exp_interval: int,
dedup_window_offset_days: int,
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
) -> bool:
now_ = now()
lock_hash = md5_hash(f'{user_id}{course_id}')
cert_exp_ttl = ttl(
start_dt=now_,
days=cert_exp_interval,
)
cert_exp_reminder_ttl = ttl(
start_dt=now_,
days=cert_exp_interval - 30,
)
dedup_lock_ttl = ttl(
start_dt=now_,
days=cert_exp_interval - dedup_window_offset_days,
)
with dynamodb_persistence_layer.transact_writer() as transact:
transact.update(
key=KeyPair(pk=id, sk='0'),
update_expr='SET #status = :completed, \
progress = :progress, \
score = :score, \
completed_at = :now, \
updated_at = :now',
cond_expr='#status = :in_progress',
expr_attr_names={'#status': 'status'},
expr_attr_values={
':completed': 'COMPLETED',
':in_progress': 'IN_PROGRESS',
':score': score,
':progress': progress,
':now': now_,
},
exc_cls=EnrollmentConflictError,
)
transact.put(
item={
'id': id,
'sk': 'COMPLETED',
'created_at': now_,
},
cond_expr='attribute_not_exists(sk)',
)
if cert_exp_interval:
transact.put(
item={
'id': id,
'sk': 'SCHEDULE#SET_CERT_EXPIRED',
'ttl': cert_exp_ttl,
'created_at': now_,
}
)
transact.put(
item={
'id': id,
'sk': 'SCHEDULE#REMINDER_CERT_EXPIRATION_BEFORE_30_DAYS',
'ttl': cert_exp_reminder_ttl,
'created_at': now_,
}
)
transact.put(
item={
'id': id,
'sk': 'LOCK',
'ttl': dedup_lock_ttl,
'hash': lock_hash,
'created_at': now_,
}
)
transact.put(
item={
'id': 'LOCK',
'sk': lock_hash,
'enrollment_id': id,
'ttl': dedup_lock_ttl,
'created_at': now_,
}
)
# Remove reminders and policies that no longer apply
transact.delete(
key=KeyPair(
pk=id,
sk='CANCEL_POLICY',
)
)
transact.delete(
key=KeyPair(
pk=id,
sk='SCHEDULE#REMINDER_NO_ACTIVITY_AFTER_7_DAYS',
)
)
transact.delete(
key=KeyPair(
pk=id,
sk='SCHEDULE#REMINDER_ACCESS_PERIOD_BEFORE_30_DAYS',
)
)
return True
def _set_status_as_failed(
id: str,
/,
score: Decimal,
progress: Decimal,
user_id: str,
course_id: str,
*,
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
) -> bool:
now_ = now()
lock_hash = md5_hash(f'{user_id}{course_id}')
with dynamodb_persistence_layer.transact_writer() as transact:
transact.update(
key=KeyPair(pk=id, sk='0'),
update_expr='SET #status = :failed, \
progress = :progress, \
score = :score, \
access_expired = :true, \
failed_at = :now, \
updated_at = :now',
cond_expr='#status = :in_progress',
expr_attr_names={'#status': 'status'},
expr_attr_values={
':failed': 'FAILED',
':in_progress': 'IN_PROGRESS',
':score': score,
':progress': progress,
':true': True,
':now': now_,
},
exc_cls=EnrollmentConflictError,
)
transact.put(
item={
'id': id,
'sk': 'FAILED',
'created_at': now_,
},
cond_expr='attribute_not_exists(sk)',
)
# Remove reminders and events that no longer apply
transact.delete(
key=KeyPair(
pk=id,
sk='CANCEL_POLICY',
)
)
transact.delete(
key=KeyPair(
pk=id,
sk='SCHEDULE#SET_ACCESS_EXPIRED',
)
)
transact.delete(
key=KeyPair(
pk=id,
sk='SCHEDULE#REMINDER_NO_ACTIVITY_AFTER_7_DAYS',
)
)
transact.delete(
key=KeyPair(
pk=id,
sk='SCHEDULE#REMINDER_ACCESS_PERIOD_BEFORE_30_DAYS',
)
)
# Remove locks related to this enrollment
transact.delete(
key=KeyPair(pk=id, sk='LOCK'),
)
transact.delete(
key=KeyPair(pk='LOCK', sk=lock_hash),
)
return True
class EnrollmentNotFoundError(NotFoundError):
def __init__(self, *_):
super().__init__('Enrollment not found')
class EnrollmentConflictError(BadRequestError):
def __init__(self, *_):
super().__init__('Enrollment status conflict')