finish user

This commit is contained in:
2025-11-17 19:33:34 -03:00
parent 7f41704d90
commit a96dcb3e96
7 changed files with 204 additions and 120 deletions

View File

@@ -38,6 +38,7 @@ app.include_router(enrollments.cancel, prefix='/enrollments')
app.include_router(enrollments.dedup_window, prefix='/enrollments') app.include_router(enrollments.dedup_window, prefix='/enrollments')
app.include_router(enrollments.download_cert, prefix='/enrollments') app.include_router(enrollments.download_cert, prefix='/enrollments')
app.include_router(enrollments.enroll, 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.router, prefix='/users')
app.include_router(users.emails, prefix='/users') app.include_router(users.emails, prefix='/users')
app.include_router(users.orgs, prefix='/users') app.include_router(users.orgs, prefix='/users')

View File

@@ -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 import Logger
from aws_lambda_powertools.event_handler.api_gateway import Router from aws_lambda_powertools.event_handler.api_gateway import Router
from aws_lambda_powertools.event_handler.exceptions import NotFoundError from layercake.dynamodb import DynamoDBPersistenceLayer, SortKey, TransactKey
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 api_gateway import JSONResponse
from boto3clients import dynamodb_client from boto3clients import dynamodb_client
from config import ENROLLMENT_TABLE from config import ENROLLMENT_TABLE
@@ -18,8 +9,9 @@ from .cancel import router as cancel
from .dedup_window import router as dedup_window from .dedup_window import router as dedup_window
from .download_cert import router as download_cert from .download_cert import router as download_cert
from .enroll import router as enroll 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__) logger = Logger(__name__)
router = Router() router = Router()
@@ -29,62 +21,5 @@ dyn = DynamoDBPersistenceLayer(ENROLLMENT_TABLE, dynamodb_client)
@router.get('/<enrollment_id>') @router.get('/<enrollment_id>')
def get_enrollment(enrollment_id: str): def get_enrollment(enrollment_id: str):
return dyn.collection.get_items( return dyn.collection.get_items(
TransactKey(enrollment_id) TransactKey(enrollment_id) + SortKey('0') + SortKey('ORG') + SortKey('LOCK')
+ SortKey('0')
+ SortKey('ORG')
+ SortKey('LOCK')
+ SortKey('SCORM_COMMIT#LAST', rename_key='last_commit'),
) )
@router.get('/<enrollment_id>/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('/<enrollment_id>')
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)

View File

@@ -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('/<enrollment_id>/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('/<enrollment_id>/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)

View File

@@ -82,7 +82,7 @@ def test_post_scormset(
r = app.lambda_handler( r = app.lambda_handler(
http_api_proxy( http_api_proxy(
raw_path='/enrollments/578ec87f-94c7-4840-8780-bb4839cc7e64', raw_path='/enrollments/578ec87f-94c7-4840-8780-bb4839cc7e64/scorm',
method=HTTPMethod.POST, method=HTTPMethod.POST,
body=scormbody, body=scormbody,
), ),

View File

@@ -48,18 +48,48 @@ import { useEffect } from 'react'
const isName = (name: string) => name && name.includes(' ') const isName = (name: string) => name && name.includes(' ')
export const formSchema = z.object({ 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 name: z
.string() .string()
.trim() .trim()
.nonempty('Digite um nome') .nonempty('Digite um nome')
.refine(isName, { message: 'Nome inválido' }), .refine(isName, { message: 'Nome inválido' }),
email: z.email('Email inválido').trim().toLowerCase().optional(), email: z.string().trim().toLowerCase().optional(),
cpf: z cpf: z
.string('CPF obrigatório') .string('CPF obrigatório')
.refine(isValidCPF, { message: 'CPF inválido' }), .refine(isValidCPF, { message: 'CPF inválido' }),
given_email: z.coerce.boolean() 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<typeof formSchema> export type Schema = z.infer<typeof formSchema>
@@ -93,7 +123,7 @@ export default function Route() {
const form = useForm({ const form = useForm({
resolver: zodResolver(formSchema) 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 givenEmail = watch('given_email') as boolean
const onSubmit = async (user: Schema) => { const onSubmit = async (user: Schema) => {
@@ -115,7 +145,11 @@ export default function Route() {
} }
}, [fetcher.data]) }, [fetcher.data])
// console.log(randomEmail()) useEffect(() => {
if (givenEmail) {
setValue('email', '', { shouldValidate: true })
}
}, [givenEmail, setValue])
return ( return (
<div className="space-y-2.5"> <div className="space-y-2.5">
@@ -166,13 +200,16 @@ export default function Route() {
<FormField <FormField
control={control} control={control}
name="email" name="email"
disabled={givenEmail}
defaultValue="" defaultValue=""
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Email</FormLabel> <FormLabel>Email</FormLabel>
<FormControl> <FormControl>
<Input {...field} /> <Input
{...field}
readOnly={givenEmail}
className="read-only:pointer-events-none read-only:opacity-50"
/>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@@ -244,14 +281,3 @@ export default function Route() {
</div> </div>
) )
} }
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`
}

View File

@@ -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<listenerHandler | null>(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
}

View File

@@ -1,11 +1,10 @@
import type { Route } from './+types' 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 { 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 { useLocalStorage } from '@/hooks/useLocalStorage'
// import SHA256 from 'crypto-js/sha256' // import SHA256 from 'crypto-js/sha256'
@@ -64,12 +63,19 @@ export default function Route({ loaderData: { data } }: Route.ComponentProps) {
// console.log(d2?.progress?.p ?? null) // console.log(d2?.progress?.p ?? null)
return ( return (
<>
{/* <RrwebRecorder
onEventBatch={(data) => {
console.log(JSON.stringify(data))
}}
/>*/}
<ScormPlayer <ScormPlayer
settings={{ settings={{
autocommit: true, autocommit: true
throwExceptions: false, // throwExceptions: false,
logLevel: 2, // logLevel: 2,
compatibilityMode: 1 // compatibilityMode: 1
}} }}
scormVersion="2004" scormVersion="2004"
scormState={scormState} scormState={scormState}
@@ -83,5 +89,6 @@ export default function Route({ loaderData: { data } }: Route.ComponentProps) {
}) })
}} }}
/> />
</>
) )
} }