352 lines
10 KiB
Python
352 lines
10 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
|
|
|
|
from boto3clients import dynamodb_client
|
|
from config import COURSE_TABLE
|
|
|
|
# @TODO Find a better way
|
|
course_layer = DynamoDBPersistenceLayer(COURSE_TABLE, dynamodb_client)
|
|
|
|
|
|
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 = if_not_exists(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,
|
|
)
|
|
# 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#DEDUPLICATION_WINDOW',
|
|
path_spec='offset_days',
|
|
rename_key='dedup_window_offset_days',
|
|
),
|
|
)
|
|
user_id = enrollment['user']['id']
|
|
course_id = glom(enrollment, 'course.id')
|
|
exp_interval = course_layer.collection.get_item(
|
|
KeyPair(
|
|
pk=course_id,
|
|
sk=SortKey('0', path_spec='cert.exp_interval'),
|
|
),
|
|
raise_on_error=False,
|
|
default=0,
|
|
)
|
|
|
|
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(exp_interval),
|
|
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 = if_not_exists(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,
|
|
)
|
|
|
|
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 = if_not_exists(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')
|