update enrollment
This commit is contained in:
55
konviva-events/app/app.py
Normal file
55
konviva-events/app/app.py
Normal file
@@ -0,0 +1,55 @@
|
||||
from decimal import Decimal
|
||||
from http import HTTPStatus
|
||||
from typing import Any
|
||||
|
||||
from aws_lambda_powertools import Logger, Tracer
|
||||
from aws_lambda_powertools.event_handler.api_gateway import (
|
||||
APIGatewayHttpResolver,
|
||||
Response,
|
||||
)
|
||||
from aws_lambda_powertools.logging import correlation_paths
|
||||
from aws_lambda_powertools.utilities.typing import LambdaContext
|
||||
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair, SortKey
|
||||
|
||||
from boto3clients import dynamodb_client
|
||||
from config import ENROLLMENT_TABLE
|
||||
from enrollment import EnrollmentNotFoundError, set_score, update_progress
|
||||
|
||||
logger = Logger(__name__)
|
||||
tracer = Tracer()
|
||||
app = APIGatewayHttpResolver(enable_validation=True)
|
||||
dyn = DynamoDBPersistenceLayer(ENROLLMENT_TABLE, dynamodb_client)
|
||||
|
||||
|
||||
@app.post('/')
|
||||
@tracer.capture_method
|
||||
def update_enrollment():
|
||||
json_body = app.current_event.json_body
|
||||
|
||||
status = json_body['status']
|
||||
score = round(Decimal(json_body['APROVEITAMENTO']), 2)
|
||||
progress = round(Decimal(json_body['ANDAMENTO']), 2)
|
||||
enrollment_id = dyn.collection.get_item(
|
||||
KeyPair(
|
||||
pk='konviva',
|
||||
sk=SortKey(json_body['ID_MATRICULA'], path_spec='enrollment_id'),
|
||||
),
|
||||
exc_cls=EnrollmentNotFoundError,
|
||||
)
|
||||
|
||||
if status == 'IN_PROGRESS':
|
||||
update_progress(enrollment_id, progress, dynamodb_persistence_layer=dyn)
|
||||
|
||||
if status == 'COMPLETED':
|
||||
set_score(enrollment_id, score, dynamodb_persistence_layer=dyn)
|
||||
|
||||
return Response(status_code=HTTPStatus.NO_CONTENT)
|
||||
|
||||
|
||||
@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_HTTP)
|
||||
@tracer.capture_lambda_handler
|
||||
def lambda_handler(
|
||||
event: dict[str, Any],
|
||||
context: LambdaContext,
|
||||
) -> dict[str, Any]:
|
||||
return app.resolve(event, context)
|
||||
423
konviva-events/app/enrollment.py
Normal file
423
konviva-events/app/enrollment.py
Normal file
@@ -0,0 +1,423 @@
|
||||
from datetime import datetime, timedelta
|
||||
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 fromisoformat, 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()
|
||||
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 = :updated_at',
|
||||
cond_expr='#status = :in_progress',
|
||||
expr_attr_names={
|
||||
'#status': 'status',
|
||||
},
|
||||
expr_attr_values={
|
||||
':in_progress': 'IN_PROGRESS',
|
||||
':progress': progress,
|
||||
':updated_at': 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': 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 date
|
||||
transact.update(
|
||||
key=KeyPair(id, '0'),
|
||||
update_expr='SET progress = :progress, #status = :in_progress, \
|
||||
updated_at = :updated_at',
|
||||
cond_expr='#status = :pending',
|
||||
expr_attr_names={
|
||||
'#status': 'status',
|
||||
},
|
||||
expr_attr_values={
|
||||
':in_progress': 'IN_PROGRESS',
|
||||
':pending': 'PENDING',
|
||||
':progress': progress,
|
||||
':updated_at': now_,
|
||||
},
|
||||
)
|
||||
# Record the start date if it does not already exist
|
||||
transact.put(
|
||||
item={
|
||||
'id': id,
|
||||
'sk': 'STARTED',
|
||||
'started_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': ttl_,
|
||||
'created_at': now_,
|
||||
}
|
||||
)
|
||||
# Remove reminders and policies that no longer apply
|
||||
transact.delete(
|
||||
key=KeyPair(
|
||||
pk=id,
|
||||
sk='SCHEDULE#REMINDER_NO_ACCESS_AFTER_3_DAYS',
|
||||
)
|
||||
)
|
||||
transact.delete(
|
||||
key=KeyPair(pk=id, sk='CANCEL_POLICY'),
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
def set_score(
|
||||
id: str,
|
||||
score: Decimal,
|
||||
*,
|
||||
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
|
||||
):
|
||||
enrollment = dynamodb_persistence_layer.collection.get_items(
|
||||
TransactKey(id)
|
||||
+ SortKey('0')
|
||||
+ SortKey(
|
||||
sk='METADATA#COURSE',
|
||||
rename_key='metadata__course',
|
||||
)
|
||||
+ SortKey(
|
||||
sk='METADATA#DEDUPLICATION_WINDOW',
|
||||
path_spec='offset_days',
|
||||
rename_key='dedup_window_offset_days',
|
||||
),
|
||||
)
|
||||
now_ = now()
|
||||
user_id = enrollment['user']['id']
|
||||
course_id = glom(enrollment, 'course.id')
|
||||
created_at: datetime = fromisoformat(enrollment['created_at']) # type: ignore
|
||||
access_period = created_at + timedelta(
|
||||
days=int(glom(enrollment, 'metadata__course.access_period'))
|
||||
)
|
||||
|
||||
try:
|
||||
match score >= 70, now_ > access_period:
|
||||
case True, True:
|
||||
# Got a score of 70 or higher, but the access period has expired
|
||||
return _set_status_as_archived(
|
||||
id,
|
||||
dynamodb_persistence_layer=dynamodb_persistence_layer,
|
||||
)
|
||||
case True, False:
|
||||
# Got a score of 70 or higher, and still within the access period
|
||||
return _set_status_as_completed(
|
||||
id,
|
||||
score,
|
||||
user_id=user_id,
|
||||
course_id=course_id,
|
||||
cert_exp_interval=int(
|
||||
glom(enrollment, 'metadata__course.cert.exp_interval')
|
||||
),
|
||||
dedup_window_offset_days=int(
|
||||
enrollment['dedup_window_offset_days']
|
||||
),
|
||||
dynamodb_persistence_layer=dynamodb_persistence_layer,
|
||||
)
|
||||
case False, True:
|
||||
# Got a score below 70, and the access period has expired
|
||||
return _set_status_as_expired(
|
||||
id,
|
||||
dynamodb_persistence_layer=dynamodb_persistence_layer,
|
||||
)
|
||||
case _:
|
||||
# Got a score below 70, and still within the access period
|
||||
return _set_status_as_failed(
|
||||
id,
|
||||
score,
|
||||
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,
|
||||
*,
|
||||
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}')
|
||||
archive_ttl = ttl(
|
||||
start_dt=now_,
|
||||
days=cert_exp_interval,
|
||||
)
|
||||
cert_expiration_reminder_ttl = ttl(
|
||||
start_dt=now_,
|
||||
days=cert_exp_interval - 30,
|
||||
)
|
||||
deduplication_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, score = :score, \
|
||||
updated_at = :updated_at',
|
||||
cond_expr='#status = :in_progress',
|
||||
expr_attr_names={'#status': 'status'},
|
||||
expr_attr_values={
|
||||
':completed': 'COMPLETED',
|
||||
':in_progress': 'IN_PROGRESS',
|
||||
':score': score,
|
||||
':updated_at': now_,
|
||||
},
|
||||
exc_cls=EnrollmentConflictError,
|
||||
)
|
||||
transact.put(
|
||||
item={
|
||||
'id': id,
|
||||
'sk': 'COMPLETED',
|
||||
'completed_at': now_,
|
||||
},
|
||||
cond_expr='attribute_not_exists(sk)',
|
||||
)
|
||||
transact.put(
|
||||
item={
|
||||
'id': id,
|
||||
'sk': 'SCHEDULE#REMINDER_CERT_EXPIRATION_BEFORE_30_DAYS',
|
||||
'ttl': cert_expiration_reminder_ttl,
|
||||
'created_at': now_,
|
||||
}
|
||||
)
|
||||
transact.put(
|
||||
item={
|
||||
'id': id,
|
||||
'sk': 'SCHEDULE#SET_AS_ARCHIVED',
|
||||
'ttl': archive_ttl,
|
||||
'created_at': now_,
|
||||
}
|
||||
)
|
||||
transact.put(
|
||||
item={
|
||||
'id': id,
|
||||
'sk': 'LOCK',
|
||||
'ttl': deduplication_lock_ttl,
|
||||
'created_at': now_,
|
||||
}
|
||||
)
|
||||
transact.put(
|
||||
item={
|
||||
'id': 'LOCK',
|
||||
'sk': lock_hash,
|
||||
'enrollment_id': id,
|
||||
'ttl': deduplication_lock_ttl,
|
||||
'created_at': now_,
|
||||
}
|
||||
)
|
||||
# Remove reminders and policies that no longer apply
|
||||
transact.delete(
|
||||
key=KeyPair(
|
||||
pk=id,
|
||||
sk='SCHEDULE#SET_AS_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',
|
||||
)
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _set_status_as_failed(
|
||||
id: str,
|
||||
/,
|
||||
score: 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, score = :score, \
|
||||
updated_at = :updated_at',
|
||||
cond_expr='#status = :in_progress',
|
||||
expr_attr_names={'#status': 'status'},
|
||||
expr_attr_values={
|
||||
':failed': 'FAILED',
|
||||
':in_progress': 'IN_PROGRESS',
|
||||
':score': score,
|
||||
':updated_at': now_,
|
||||
},
|
||||
exc_cls=EnrollmentConflictError,
|
||||
)
|
||||
transact.put(
|
||||
item={
|
||||
'id': id,
|
||||
'sk': 'FAILED',
|
||||
'failed_at': now_,
|
||||
},
|
||||
cond_expr='attribute_not_exists(sk)',
|
||||
)
|
||||
# Remove reminders and events that no longer apply
|
||||
transact.delete(
|
||||
key=KeyPair(
|
||||
pk=id,
|
||||
sk='SCHEDULE#SET_AS_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
|
||||
|
||||
|
||||
def _set_status_as_archived(
|
||||
id: str,
|
||||
*,
|
||||
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
|
||||
):
|
||||
now_ = now()
|
||||
with dynamodb_persistence_layer.transact_writer() as transact:
|
||||
transact.update(
|
||||
key=KeyPair(pk=id, sk='0'),
|
||||
update_expr='SET #status = :archived, updated_at = :updated_at',
|
||||
cond_expr='#status = :completed',
|
||||
expr_attr_names={'#status': 'status'},
|
||||
expr_attr_values={
|
||||
':archived': 'ARCHIVED',
|
||||
':completed': 'COMPLETED',
|
||||
':updated_at': now_,
|
||||
},
|
||||
exc_cls=EnrollmentConflictError,
|
||||
)
|
||||
transact.put(
|
||||
item={
|
||||
'id': id,
|
||||
'sk': 'ARCHIVED',
|
||||
'archived_at': now_,
|
||||
},
|
||||
cond_expr='attribute_not_exists(sk)',
|
||||
)
|
||||
# Remove events that no longer apply
|
||||
transact.delete(
|
||||
key=KeyPair(
|
||||
pk=id,
|
||||
sk='SCHEDULE#SET_AS_ARCHIVED',
|
||||
)
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _set_status_as_expired(
|
||||
id: str,
|
||||
*,
|
||||
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
|
||||
):
|
||||
now_ = now()
|
||||
|
||||
with dynamodb_persistence_layer.transact_writer() as transact:
|
||||
transact.update(
|
||||
key=KeyPair(pk=id, sk='0'),
|
||||
update_expr='SET #status = :expired, updated_at = :updated_at',
|
||||
cond_expr='#status = :in_progress OR #status = :pending',
|
||||
expr_attr_names={
|
||||
'#status': 'status',
|
||||
},
|
||||
expr_attr_values={
|
||||
':pending': 'PENDING',
|
||||
':in_progress': 'IN_PROGRESS',
|
||||
':expired': 'EXPIRED',
|
||||
':updated_at': now_,
|
||||
},
|
||||
exc_cls=EnrollmentConflictError,
|
||||
)
|
||||
transact.put(
|
||||
item={
|
||||
'id': id,
|
||||
'sk': 'EXPIRED',
|
||||
'expired_at': now_,
|
||||
},
|
||||
cond_expr='attribute_not_exists(sk)',
|
||||
)
|
||||
# Remove events and policies that no longer apply
|
||||
transact.delete(
|
||||
key=KeyPair(
|
||||
pk=id,
|
||||
sk='SCHEDULE#SET_AS_EXPIRED',
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class EnrollmentNotFoundError(NotFoundError):
|
||||
def __init__(self, *_):
|
||||
super().__init__('Enrollment not found')
|
||||
|
||||
|
||||
class EnrollmentConflictError(BadRequestError): ...
|
||||
Reference in New Issue
Block a user