diff --git a/api.saladeaula.digital/app/app.py b/api.saladeaula.digital/app/app.py index 428a0ef..2deed8c 100644 --- a/api.saladeaula.digital/app/app.py +++ b/api.saladeaula.digital/app/app.py @@ -38,6 +38,7 @@ app.include_router(enrollments.cancel, prefix='/enrollments') app.include_router(enrollments.dedup_window, prefix='/enrollments') app.include_router(enrollments.download_cert, prefix='/enrollments') app.include_router(enrollments.enroll, prefix='/enrollments') +app.include_router(enrollments.scorm, prefix='/enrollments') app.include_router(users.router, prefix='/users') app.include_router(users.emails, prefix='/users') app.include_router(users.orgs, prefix='/users') diff --git a/api.saladeaula.digital/app/routes/enrollments/__init__.py b/api.saladeaula.digital/app/routes/enrollments/__init__.py index 5899927..bf00236 100644 --- a/api.saladeaula.digital/app/routes/enrollments/__init__.py +++ b/api.saladeaula.digital/app/routes/enrollments/__init__.py @@ -1,16 +1,7 @@ -from http import HTTPStatus -from os import rename -from typing import Annotated - from aws_lambda_powertools import Logger from aws_lambda_powertools.event_handler.api_gateway import Router -from aws_lambda_powertools.event_handler.exceptions import NotFoundError -from aws_lambda_powertools.event_handler.openapi.params import Body -from glom import T, glom -from layercake.dateutils import now -from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair, SortKey, TransactKey +from layercake.dynamodb import DynamoDBPersistenceLayer, SortKey, TransactKey -from api_gateway import JSONResponse from boto3clients import dynamodb_client from config import ENROLLMENT_TABLE @@ -18,8 +9,9 @@ from .cancel import router as cancel from .dedup_window import router as dedup_window from .download_cert import router as download_cert from .enroll import router as enroll +from .scorm import router as scorm -__all__ = ['cancel', 'dedup_window', 'download_cert', 'enroll'] +__all__ = ['cancel', 'dedup_window', 'download_cert', 'enroll', 'scorm'] logger = Logger(__name__) router = Router() @@ -29,62 +21,5 @@ dyn = DynamoDBPersistenceLayer(ENROLLMENT_TABLE, dynamodb_client) @router.get('/') def get_enrollment(enrollment_id: str): return dyn.collection.get_items( - TransactKey(enrollment_id) - + SortKey('0') - + SortKey('ORG') - + SortKey('LOCK') - + SortKey('SCORM_COMMIT#LAST', rename_key='last_commit'), + TransactKey(enrollment_id) + SortKey('0') + SortKey('ORG') + SortKey('LOCK') ) - - -@router.get('//scorm') -def get_scorm(enrollment_id: str): - enrollment = dyn.collection.get_items( - TransactKey(enrollment_id) + SortKey('0') + SortKey('SCORM_COMMIT#LAST') - ) - - if not enrollment: - raise NotFoundError('Enrollment not found') - - spec = {'id': 'course.id', 'scormset': 'course.scormset'} - course_id, scormset_id = glom(enrollment, spec).values() - scormset = dyn.collection.get_item( - KeyPair(course_id, f'SCORMSET#{scormset_id}'), - raise_on_error=False, - default={}, - ) - - return enrollment | {'scormset': scormset} - - -@router.post('/') -def post_enrollment( - enrollment_id: str, - cmi: Annotated[dict, Body(embed=True)], -): - now_ = now() - - with dyn.transact_writer() as transact: - transact.condition( - key=KeyPair(enrollment_id, '0'), - cond_expr='attribute_exists(sk)', - exc_cls=NotFoundError, - ) - transact.put( - item={ - 'id': enrollment_id, - 'sk': 'SCORM_COMMIT#LAST', - 'cmi': cmi, - 'created_at': now_, - } - ) - transact.put( - item={ - 'id': f'SCORM_COMMIT#{enrollment_id}', - 'sk': now_.isoformat(), - 'cmi': cmi, - 'created_at': now_, - } - ) - - return JSONResponse(HTTPStatus.NO_CONTENT) diff --git a/api.saladeaula.digital/app/routes/enrollments/scorm.py b/api.saladeaula.digital/app/routes/enrollments/scorm.py new file mode 100644 index 0000000..6130c33 --- /dev/null +++ b/api.saladeaula.digital/app/routes/enrollments/scorm.py @@ -0,0 +1,73 @@ +from http import HTTPStatus +from typing import Annotated + +from aws_lambda_powertools import Logger +from aws_lambda_powertools.event_handler.api_gateway import Router +from aws_lambda_powertools.event_handler.exceptions import NotFoundError +from aws_lambda_powertools.event_handler.openapi.params import Body +from glom import glom +from layercake.dateutils import now +from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair, SortKey, TransactKey + +from api_gateway import JSONResponse +from boto3clients import dynamodb_client +from config import ENROLLMENT_TABLE + +logger = Logger(__name__) +router = Router() +dyn = DynamoDBPersistenceLayer(ENROLLMENT_TABLE, dynamodb_client) + + +@router.get('//scorm') +def get_scorm(enrollment_id: str): + enrollment = dyn.collection.get_items( + TransactKey(enrollment_id) + + SortKey('0') + + SortKey('SCORM_COMMIT#LAST', rename_key='last_commit') + ) + + if not enrollment: + raise NotFoundError('Enrollment not found') + + spec = {'id': 'course.id', 'scormset': 'course.scormset'} + course_id, scormset_id = glom(enrollment, spec).values() + scormset = dyn.collection.get_item( + KeyPair(course_id, f'SCORMSET#{scormset_id}'), + raise_on_error=False, + default={}, + ) + + return enrollment | {'scormset': scormset} + + +@router.post('//scorm') +def post_scorm( + enrollment_id: str, + cmi: Annotated[dict, Body(embed=True)], +): + now_ = now() + + with dyn.transact_writer() as transact: + transact.condition( + key=KeyPair(enrollment_id, '0'), + cond_expr='attribute_exists(sk)', + exc_cls=NotFoundError, + ) + transact.put( + item={ + 'id': enrollment_id, + 'sk': 'SCORM_COMMIT#LAST', + 'cmi': cmi, + 'created_at': now_, + } + ) + transact.put( + item={ + 'id': f'SCORM_COMMIT#{enrollment_id}', + 'sk': now_.isoformat(), + 'cmi': cmi, + 'created_at': now_, + } + ) + + return JSONResponse(HTTPStatus.NO_CONTENT) diff --git a/api.saladeaula.digital/tests/routes/test_enrollments.py b/api.saladeaula.digital/tests/routes/test_enrollments.py index bf2830f..7a2b50a 100644 --- a/api.saladeaula.digital/tests/routes/test_enrollments.py +++ b/api.saladeaula.digital/tests/routes/test_enrollments.py @@ -82,7 +82,7 @@ def test_post_scormset( r = app.lambda_handler( http_api_proxy( - raw_path='/enrollments/578ec87f-94c7-4840-8780-bb4839cc7e64', + raw_path='/enrollments/578ec87f-94c7-4840-8780-bb4839cc7e64/scorm', method=HTTPMethod.POST, body=scormbody, ), diff --git a/apps/admin.saladeaula.digital/app/routes/_.$orgid.users.add/route.tsx b/apps/admin.saladeaula.digital/app/routes/_.$orgid.users.add/route.tsx index d88f201..9af05fa 100644 --- a/apps/admin.saladeaula.digital/app/routes/_.$orgid.users.add/route.tsx +++ b/apps/admin.saladeaula.digital/app/routes/_.$orgid.users.add/route.tsx @@ -48,18 +48,48 @@ import { useEffect } from 'react' const isName = (name: string) => name && name.includes(' ') -export const formSchema = z.object({ - name: z - .string() - .trim() - .nonempty('Digite um nome') - .refine(isName, { message: 'Nome inválido' }), - email: z.email('Email inválido').trim().toLowerCase().optional(), - cpf: z - .string('CPF obrigatório') - .refine(isValidCPF, { message: 'CPF inválido' }), - given_email: z.coerce.boolean() -}) +function randomEmail() { + const numberDict = NumberDictionary.generate({ min: 100, max: 999 }) + const randomName: string = uniqueNamesGenerator({ + dictionaries: [adjectives, colors, numberDict], + length: 3, + separator: '-' + }) + + return `${randomName}@users.noreply.saladeaula.digital` +} + +export const formSchema = z + .object({ + name: z + .string() + .trim() + .nonempty('Digite um nome') + .refine(isName, { message: 'Nome inválido' }), + email: z.string().trim().toLowerCase().optional(), + cpf: z + .string('CPF obrigatório') + .refine(isValidCPF, { message: 'CPF inválido' }), + given_email: z.coerce.boolean() + }) + .refine( + ({ given_email, email }) => { + if (given_email) { + return true + } + return email && z.email().safeParse(email).success + }, + { + message: 'Email inválido', + path: ['email'] + } + ) + .transform((data) => { + if (data.given_email) { + return { ...data, email: randomEmail() } + } + return data + }) export type Schema = z.infer @@ -93,7 +123,7 @@ export default function Route() { const form = useForm({ resolver: zodResolver(formSchema) }) - const { handleSubmit, control, formState, reset, watch } = form + const { handleSubmit, control, formState, reset, watch, setValue } = form const givenEmail = watch('given_email') as boolean const onSubmit = async (user: Schema) => { @@ -115,7 +145,11 @@ export default function Route() { } }, [fetcher.data]) - // console.log(randomEmail()) + useEffect(() => { + if (givenEmail) { + setValue('email', '', { shouldValidate: true }) + } + }, [givenEmail, setValue]) return (
@@ -166,13 +200,16 @@ export default function Route() { ( Email - + @@ -244,14 +281,3 @@ export default function Route() {
) } - -function randomEmail() { - const numberDict = NumberDictionary.generate({ min: 100, max: 999 }) - const randomName: string = uniqueNamesGenerator({ - dictionaries: [adjectives, colors, numberDict], - length: 3, - separator: '-' - }) - - return `${randomName}@users.noreply.saladeaula.digital` -} diff --git a/apps/saladeaula.digital/app/components/rrweb-recorder.tsx b/apps/saladeaula.digital/app/components/rrweb-recorder.tsx new file mode 100644 index 0000000..7fff6bc --- /dev/null +++ b/apps/saladeaula.digital/app/components/rrweb-recorder.tsx @@ -0,0 +1,42 @@ +import type { eventWithTime, listenerHandler } from '@rrweb/types' +import { useEffect, useRef } from 'react' +import { record } from 'rrweb' + +interface RrwebRecorderProps { + onEventBatch: (events: eventWithTime[]) => void +} + +export function RrwebRecorder({ onEventBatch }: RrwebRecorderProps) { + const stopRef = useRef(null) + + useEffect(() => { + const events: eventWithTime[] = [] + + const stopFn = record({ + emit(event: eventWithTime) { + events.push(event) + + if (events.length >= 50) { + onEventBatch(events.splice(0)) + } + } + }) + + if (stopFn) { + stopRef.current = stopFn + } + + return () => { + if (stopRef.current) { + stopRef.current() + stopRef.current = null + + if (events.length > 0) { + onEventBatch(events) + } + } + } + }, [onEventBatch]) + + return null +} diff --git a/apps/saladeaula.digital/app/routes/player.tsx b/apps/saladeaula.digital/app/routes/player.tsx index f6ff45f..6a1d354 100644 --- a/apps/saladeaula.digital/app/routes/player.tsx +++ b/apps/saladeaula.digital/app/routes/player.tsx @@ -1,11 +1,10 @@ import type { Route } from './+types' -import lzwCompress from 'lzwcompress' -import { useBlocker, useFetcher } from 'react-router' - import { HttpMethod, request as req } from '@repo/util/request' +import { useFetcher } from 'react-router' -import { ScormPlayer, type ScormVersion } from '@/components/scorm-player' +// import { RrwebRecorder } from '@/components/rrweb-recorder' +import { ScormPlayer } from '@/components/scorm-player' // import { useLocalStorage } from '@/hooks/useLocalStorage' // import SHA256 from 'crypto-js/sha256' @@ -64,24 +63,32 @@ export default function Route({ loaderData: { data } }: Route.ComponentProps) { // console.log(d2?.progress?.p ?? null) return ( - { - console.log(data) - await fetcher.submit(JSON.stringify(data), { - method: 'post', - encType: 'application/json' - }) - }} - /> + <> + {/* { + console.log(JSON.stringify(data)) + }} + />*/} + + { + console.log(data) + await fetcher.submit(JSON.stringify(data), { + method: 'post', + encType: 'application/json' + }) + }} + /> + ) }