This commit is contained in:
2025-06-03 20:13:07 -03:00
parent 957f9c4a72
commit c3e6ed4a50
13 changed files with 312 additions and 75 deletions

Binary file not shown.

View File

@@ -1,8 +1,12 @@
import base64
import io
import locale import locale
from datetime import date from datetime import date
from uuid import uuid4 from uuid import uuid4
import qrcode
from jinja2 import Template from jinja2 import Template
from PIL import Image
from weasyprint import HTML from weasyprint import HTML
locale.setlocale(locale.LC_TIME, 'pt_BR') locale.setlocale(locale.LC_TIME, 'pt_BR')
@@ -17,6 +21,22 @@ def cpf_fmt(s: str) -> str:
return '{}.{}.{}-{}'.format(s[:3], s[3:6], s[6:9], s[9:]) return '{}.{}.{}-{}'.format(s[:3], s[3:6], s[6:9], s[9:])
qr = qrcode.QRCode(
version=1,
error_correction=qrcode.constants.ERROR_CORRECT_H,
box_size=10,
border=3,
)
qr.add_data('https://eduseg.com.br')
qr.make(fit=True)
img = qr.make_image(fill_color='black', back_color='white')
img = img.resize((120, 120), Image.NEAREST)
buffer = io.BytesIO()
img.save(buffer, format='PNG')
img_str = base64.b64encode(buffer.getvalue()).decode('utf-8')
qrcode_base64 = f'data:image/png;base64,{img_str}'
template = Template(html) template = Template(html)
html_rendered = template.render( html_rendered = template.render(
id=uuid4(), id=uuid4(),
@@ -27,6 +47,7 @@ html_rendered = template.render(
today=today.strftime('%-d de %B de %Y'), today=today.strftime('%-d de %B de %Y'),
started_date=today.strftime('%d/%m/%Y'), started_date=today.strftime('%d/%m/%Y'),
finished_date=today.strftime('%d/%m/%Y'), finished_date=today.strftime('%d/%m/%Y'),
qrcode=qrcode_base64,
) )
HTML(string=html_rendered, base_url='').write_pdf('cert.pdf') HTML(string=html_rendered, base_url='').write_pdf('cert.pdf')

View File

@@ -3,14 +3,14 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<title>NR-10 Complementar (SEP)</title> <title>NR-10 Complementar (SEP)</title>
<link href="style.css" rel="stylesheet" />
<meta name="author" content="EDUSEG® <https://eduseg.com.br>" /> <meta name="author" content="EDUSEG® <https://eduseg.com.br>" />
<meta name="dcterms.created" content="{{ dcterms.created }}" />
<style> <style>
html, html,
body, body,
div, div,
h1, h1,
h2,
ul,
p, p,
a { a {
margin: 0; margin: 0;
@@ -43,9 +43,6 @@
break-after: page; break-after: page;
box-sizing: border-box; box-sizing: border-box;
padding: 5rem; padding: 5rem;
display: flex;
flex-direction: column;
gap: 1.5rem;
} }
strong { strong {
@@ -55,11 +52,35 @@
#cover { #cover {
background-color: #a7e400; background-color: #a7e400;
justify-content: center; justify-content: center;
position: relative;
display: flex;
flex-direction: column;
gap: 1rem;
} }
#cover h1 { #cover h1 {
font-weight: bolder; font-weight: bolder;
font-size: 24pt; font-size: 26pt;
}
#cover .qrcode {
width: 120px;
height: 120px;
background-color: #fff;
position: absolute;
top: 5rem;
right: 5rem;
}
#cover .signatures {
display: flex;
justify-content: space-between;
margin-top: 2.5rem;
}
.sign1 {
width: 250px;
border-top: #000 solid 1px;
} }
#back { #back {
@@ -79,8 +100,12 @@
padding-left: 1rem; padding-left: 1rem;
} }
.space-y > :not(:last-child) { .space-y-0\.5 > :not(:last-child) {
margin-bottom: 1rem; margin-bottom: 0.125rem;
}
.space-y-2\.5 > :not(:last-child) {
margin-bottom: 0.625rem;
} }
</style> </style>
</head> </head>
@@ -156,12 +181,24 @@
de de
<strong>{{ progress }}%</strong> <strong>{{ progress }}%</strong>
</p> </p>
<p>Realizado entre {{ start_date }} e {{ finish_date }}</p> <p>Realizado entre {{ started_date }} e {{ finished_date }}</p>
<p>Florianópolis, SC, {{ today }}</p> <p>Florianópolis, SC, {{ today }}</p>
<div class="signatures">
<div class="sign1"></div>
<div class="sign2">
<p>Tiago Maciel do Santos</p>
<p>CEO/Diretor</p>
</div>
</div>
<div class="qrcode">
<img src="{{ qrcode }}" />
</div>
</section> </section>
<section id="back"> <section id="back">
<div class="space-y"> <div class="space-y-2.5">
<h1>Conteúdo programático ministrado</h1> <h1>Conteúdo programático ministrado</h1>
<ul> <ul>
<li>Organização do sistema elétrico de potência</li> <li>Organização do sistema elétrico de potência</li>
@@ -193,18 +230,20 @@
</ul> </ul>
</div> </div>
<div class="space-y"> <div class="space-y-2.5">
<dd> <dd class="space-y-0.5">
<h2>Carga horária</h2> <h2>Carga horária</h2>
<p>40 horas</p> <p>40 horas</p>
</dd> </dd>
<dd> <dd class="space-y-0.5">
<h2>Instrutor e responsável técnico</h2> <h2>Instrutor e responsável técnico</h2>
<p>Francis Ricardo Baretta</p> <div>
<p>CPF 039.539.409-02</p> <p>Francis Ricardo Baretta</p>
<p>Eng. de Segurança no Trabalho Eng. Eletricista</p> <p>CPF 039.539.409-02</p>
<p>CREA/SC 126693-0</p> <p>Eng. de Segurança no Trabalho Eng. Eletricista</p>
<p>CREA/SC 126693-0</p>
</div>
</dd> </dd>
</div> </div>
</section> </section>

View File

@@ -7,6 +7,7 @@ requires-python = ">=3.13"
dependencies = [ dependencies = [
"jinja2>=3.1.6", "jinja2>=3.1.6",
"layercake", "layercake",
"qrcode>=8.2",
] ]
[tool.uv.sources] [tool.uv.sources]

26
certs/uv.lock generated
View File

@@ -184,6 +184,7 @@ source = { virtual = "." }
dependencies = [ dependencies = [
{ name = "jinja2" }, { name = "jinja2" },
{ name = "layercake" }, { name = "layercake" },
{ name = "qrcode" },
] ]
[package.dev-dependencies] [package.dev-dependencies]
@@ -195,6 +196,7 @@ dev = [
requires-dist = [ requires-dist = [
{ name = "jinja2", specifier = ">=3.1.6" }, { name = "jinja2", specifier = ">=3.1.6" },
{ name = "layercake", directory = "../layercake" }, { name = "layercake", directory = "../layercake" },
{ name = "qrcode", specifier = ">=8.2" },
] ]
[package.metadata.requires-dev] [package.metadata.requires-dev]
@@ -244,6 +246,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" }, { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" },
] ]
[[package]]
name = "colorama"
version = "0.4.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
]
[[package]] [[package]]
name = "cryptography" name = "cryptography"
version = "44.0.3" version = "44.0.3"
@@ -471,7 +482,7 @@ wheels = [
[[package]] [[package]]
name = "layercake" name = "layercake"
version = "0.2.15" version = "0.6.5"
source = { directory = "../layercake" } source = { directory = "../layercake" }
dependencies = [ dependencies = [
{ name = "arnparse" }, { name = "arnparse" },
@@ -517,7 +528,6 @@ dev = [
{ name = "jsonlines", specifier = ">=4.0.0" }, { name = "jsonlines", specifier = ">=4.0.0" },
{ name = "pytest", specifier = ">=8.3.5" }, { name = "pytest", specifier = ">=8.3.5" },
{ name = "pytest-cov", specifier = ">=6.0.0" }, { name = "pytest-cov", specifier = ">=6.0.0" },
{ name = "pytest-env", specifier = ">=1.1.5" },
{ name = "ruff", specifier = ">=0.11.1" }, { name = "ruff", specifier = ">=0.11.1" },
] ]
@@ -765,6 +775,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" },
] ]
[[package]]
name = "qrcode"
version = "8.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/8f/b2/7fc2931bfae0af02d5f53b174e9cf701adbb35f39d69c2af63d4a39f81a9/qrcode-8.2.tar.gz", hash = "sha256:35c3f2a4172b33136ab9f6b3ef1c00260dd2f66f858f24d88418a015f446506c", size = 43317, upload-time = "2025-05-01T15:44:24.726Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/dd/b8/d2d6d731733f51684bbf76bf34dab3b70a9148e8f2cef2bb544fccec681a/qrcode-8.2-py3-none-any.whl", hash = "sha256:16e64e0716c14960108e85d853062c9e8bba5ca8252c0b4d0231b9df4060ff4f", size = 45986, upload-time = "2025-05-01T15:44:22.781Z" },
]
[[package]] [[package]]
name = "requests" name = "requests"
version = "2.32.3" version = "2.32.3"

View File

@@ -1,3 +1,4 @@
import uuid
from datetime import datetime from datetime import datetime
from http import HTTPStatus from http import HTTPStatus
@@ -17,7 +18,7 @@ from config import (
) )
from middlewares import Tenant, TenantMiddleware from middlewares import Tenant, TenantMiddleware
from models import Course, Enrollment, User from models import Course, Enrollment, User
from rules.enrollment import enroll from rules.enrollment import DeduplicationWindow, Vacancy, enroll
router = Router() router = Router()
@@ -31,9 +32,18 @@ processor = BatchProcessor()
class Item(BaseModel): class Item(BaseModel):
user: User user: User
course: Course course: Course
deduplication_window: dict = {} vacancy: Vacancy | None = None
deduplication_window: DeduplicationWindow | None = None
schedule_date: datetime | None = None schedule_date: datetime | None = None
@property
def id(self) -> str:
if not self.vacancy:
return str(uuid.uuid4())
_, idx = self.vacancy.sk.split('#')
return idx
class Payload(BaseModel): class Payload(BaseModel):
items: tuple[Item, ...] items: tuple[Item, ...]
@@ -59,6 +69,7 @@ def enroll_(payload: Payload):
def handler(record: Item, context: dict): def handler(record: Item, context: dict):
tenant: Tenant = context['tenant'] tenant: Tenant = context['tenant']
enrollment = Enrollment( enrollment = Enrollment(
id=record.id,
user=record.user, user=record.user,
course=record.course, course=record.course,
) )
@@ -69,7 +80,8 @@ def handler(record: Item, context: dict):
'id': str(tenant.id), 'id': str(tenant.id),
'name': tenant.name, 'name': tenant.name,
}, },
deduplication_window=record.deduplication_window, # type: ignore deduplication_window=record.deduplication_window,
vacancy=record.vacancy,
persistence_layer=enrollment_layer, persistence_layer=enrollment_layer,
) )

View File

@@ -40,6 +40,7 @@ def update_course(
persistence_layer: DynamoDBPersistenceLayer, persistence_layer: DynamoDBPersistenceLayer,
): ):
now_ = now() now_ = now()
with persistence_layer.transact_writer() as transact: with persistence_layer.transact_writer() as transact:
transact.update( transact.update(
key=KeyPair(id, '0'), key=KeyPair(id, '0'),

View File

@@ -1,6 +1,7 @@
from dataclasses import asdict, dataclass
from datetime import timedelta from datetime import timedelta
from enum import Enum from enum import Enum
from typing import TypedDict from typing import Self, TypedDict
from uuid import uuid4 from uuid import uuid4
from layercake.dateutils import now, ttl from layercake.dateutils import now, ttl
@@ -10,22 +11,31 @@ from layercake.strutils import md5_hash
from config import ORDER_TABLE from config import ORDER_TABLE
from models import Course, Enrollment from models import Course, Enrollment
Tenant = TypedDict('Tenant', {'id': str, 'name': str})
Author = TypedDict('Author', {'id': str, 'name': str})
DeduplicationWindow = TypedDict('DeduplicationWindow', {'offset_days': int})
class Tenant(TypedDict):
class RelatedId(str):
def __new__(cls, id: str, kind: str) -> Self:
return super().__new__(cls, id)
def __init__(self, id: str, kind: str) -> None:
# __init__ is used to store the parameters for later reference.
# For immutable types like str, __init__ cannot change the instance's value.
self.id = id
self.kind = kind
@dataclass(frozen=True)
class Vacancy:
id: str id: str
name: str sk: str
@property
class Author(TypedDict): def order_id(self) -> RelatedId:
id: str idx, _ = self.sk.split('#')
name: str return RelatedId(idx, 'order')
class Vacancy(TypedDict): ...
class DeduplicationWindow(TypedDict):
offset_days: int
class LifecycleEvents(str, Enum): class LifecycleEvents(str, Enum):
@@ -55,6 +65,8 @@ def enroll(
*, *,
tenant: Tenant, tenant: Tenant,
vacancy: Vacancy | None = None, vacancy: Vacancy | None = None,
author: Author | None = None,
related_ids: frozenset[RelatedId] = frozenset(),
deduplication_window: DeduplicationWindow | None = None, deduplication_window: DeduplicationWindow | None = None,
persistence_layer: DynamoDBPersistenceLayer, persistence_layer: DynamoDBPersistenceLayer,
) -> bool: ) -> bool:
@@ -66,12 +78,15 @@ def enroll(
lock_hash = md5_hash('%s%s' % (user.id, course.id)) lock_hash = md5_hash('%s%s' % (user.id, course.id))
with persistence_layer.transact_writer() as transact: with persistence_layer.transact_writer() as transact:
if vacancy:
related_ids = frozenset({vacancy.order_id}) | related_ids
transact.put( transact.put(
item={ item={
'sk': '0', 'sk': '0',
'create_date': now_, 'create_date': now_,
'metadata__tenant_id': tenant_id, 'metadata__tenant_id': tenant_id,
'metadata__related_ids': {tenant_id, user.id}, 'metadata__related_ids': {tenant_id, user.id} | related_ids,
**enrollment.model_dump(), **enrollment.model_dump(),
}, },
) )
@@ -95,17 +110,9 @@ def enroll(
'ttl': ttl(days=3, start_dt=now_), 'ttl': ttl(days=3, start_dt=now_),
}, },
) )
transact.put( # Enrollment expires by default when the access period ends.
item={ # When the course is finished, it is automatically removed,
'id': enrollment.id, # and the `schedules#course_archived` event is created.
'sk': LifecycleEvents.ACCESS_PERIOD_REMINDER_30_DAYS,
'name': user.name,
'email': user.email,
'course': course.name,
'create_date': now_,
'ttl': ttl(start_dt=now_ + timedelta(days=course.access_period - 30)),
},
)
transact.put( transact.put(
item={ item={
'id': enrollment.id, 'id': enrollment.id,
@@ -117,25 +124,78 @@ def enroll(
'ttl': ttl(start_dt=now_ + timedelta(days=course.access_period)), 'ttl': ttl(start_dt=now_ + timedelta(days=course.access_period)),
}, },
) )
transact.put(
item={
'id': enrollment.id,
'sk': LifecycleEvents.ACCESS_PERIOD_REMINDER_30_DAYS,
'name': user.name,
'email': user.email,
'course': course.name,
'create_date': now_,
'ttl': ttl(start_dt=now_ + timedelta(days=course.access_period - 30)),
},
)
for related_id in related_ids:
kind = related_id.kind.lower()
transact.put(
item={
'id': enrollment.id,
'sk': f'related_ids#{kind}',
'create_date': now_,
f'{kind}_id': related_id,
}
)
if vacancy:
transact.put(
item={
'id': enrollment.id,
'sk': 'parent_vacancy',
'vacancy': asdict(vacancy),
'create_date': now_,
}
)
class VacancyDoesNotExistError(Exception):
def __init__(self, *args):
super().__init__('Vacancy does not exist')
transact.delete(
key=KeyPair(vacancy.id, vacancy.sk),
cond_expr='attribute_exists(sk)',
exc_cls=VacancyDoesNotExistError,
)
transact.put(
item={
'id': enrollment.id,
'sk': 'metadata#cancel_policy',
'create_date': now_,
}
)
if author:
transact.put(
item={
'id': enrollment.id,
'sk': 'metadata#author',
'user_id': author['id'],
'name': author['name'],
'create_date': now_,
},
)
class DeduplicationConflictError(Exception): class DeduplicationConflictError(Exception):
def __init__(self, *args): def __init__(self, *args):
super().__init__('Enrollment already exists') super().__init__('Enrollment already exists')
# Prevents the user from enrolling in the same course again until # Prevents the user from enrolling in the same course again until
# the deduplication window expires or is removed # the deduplication window expires or is removed.
transact.condition(
key=KeyPair('lock', lock_hash),
cond_expr='attribute_not_exists(sk)',
exc_cls=DeduplicationConflictError,
)
if deduplication_window: if deduplication_window:
offset_days = deduplication_window['offset_days'] offset_days = deduplication_window['offset_days']
ttl_expiration = ttl( ttl_expiration = ttl(
start_dt=now_ + timedelta(days=course.access_period - offset_days) start_dt=now_ + timedelta(days=course.access_period - offset_days)
) )
transact.put( transact.put(
item={ item={
'id': 'lock', 'id': 'lock',
@@ -144,6 +204,8 @@ def enroll(
'create_date': now_, 'create_date': now_,
'ttl': ttl_expiration, 'ttl': ttl_expiration,
}, },
cond_expr='attribute_not_exists(sk)',
exc_cls=DeduplicationConflictError,
) )
transact.put( transact.put(
item={ item={
@@ -163,6 +225,12 @@ def enroll(
'create_date': now_, 'create_date': now_,
}, },
) )
else:
transact.condition(
key=KeyPair('lock', lock_hash),
cond_expr='attribute_not_exists(sk)',
exc_cls=DeduplicationConflictError,
)
return True return True

View File

@@ -1,3 +1,4 @@
from datetime import timedelta
from types import SimpleNamespace from types import SimpleNamespace
from typing import TypedDict from typing import TypedDict
@@ -9,6 +10,7 @@ from layercake.dynamodb import (
ComposeKey, ComposeKey,
DynamoDBPersistenceLayer, DynamoDBPersistenceLayer,
KeyPair, KeyPair,
SortKey,
) )
User = TypedDict('User', {'id': str, 'name': str, 'cpf': str}) User = TypedDict('User', {'id': str, 'name': str, 'cpf': str})
@@ -23,7 +25,12 @@ def update_user(
now_ = now() now_ = now()
user = SimpleNamespace(**data) user = SimpleNamespace(**data)
# Get the user's CPF, if it exists. # Get the user's CPF, if it exists.
old_cpf = persistence_layer.get_item(KeyPair(user.id, '0')).get('cpf', None) old_cpf = persistence_layer.collection.get_item(
KeyPair(
pk=user.id,
sk=SortKey('0', path_spec='cpf'),
)
)
with persistence_layer.transact_writer() as transact: with persistence_layer.transact_writer() as transact:
transact.update( transact.update(
@@ -128,9 +135,7 @@ def del_email(
) -> bool: ) -> bool:
"""Delete any email except the primary email.""" """Delete any email except the primary email."""
with persistence_layer.transact_writer() as transact: with persistence_layer.transact_writer() as transact:
transact.delete( transact.delete(key=KeyPair('email', email))
key=KeyPair('email', email),
)
transact.delete( transact.delete(
key=KeyPair(id, ComposeKey(email, prefix='emails')), key=KeyPair(id, ComposeKey(email, prefix='emails')),
cond_expr='email_primary <> :primary', cond_expr='email_primary <> :primary',

View File

@@ -3,10 +3,11 @@ from http import HTTPMethod, HTTPStatus
from layercake.dynamodb import ( from layercake.dynamodb import (
ComposeKey, ComposeKey,
DynamoDBCollection,
DynamoDBPersistenceLayer, DynamoDBPersistenceLayer,
KeyPair, KeyPair,
PartitionKey, PartitionKey,
SortKey,
TransactKey,
) )
from ..conftest import HttpApiProxy, LambdaContext from ..conftest import HttpApiProxy, LambdaContext
@@ -53,6 +54,10 @@ def test_enroll(
'deduplication_window': { 'deduplication_window': {
'offset_days': 60, 'offset_days': 60,
}, },
'vacancy': {
'id': 'vacancies#cJtK9SsnJhKPyxESe7g3DG',
'sk': '3CNrFB9dy2RLit2pdeUWy4#8c9b55ef-e988-43ee-b2da-8594850605d7',
},
}, },
], ],
}, },
@@ -62,12 +67,75 @@ def test_enroll(
assert r['statusCode'] == HTTPStatus.OK assert r['statusCode'] == HTTPStatus.OK
fail, _ = json.loads(r['body']) fail, succ = json.loads(r['body'])
assert fail['cause'] == { assert fail['cause'] == {
'type': 'DeduplicationConflictError', 'type': 'DeduplicationConflictError',
'message': 'Enrollment already exists', 'message': 'Enrollment already exists',
} }
enrollment = dynamodb_persistence_layer.collection.get_items(
TransactKey(
'8c9b55ef-e988-43ee-b2da-8594850605d7',
)
+ SortKey('0')
+ SortKey('parent_vacancy', path_spec='vacancy')
+ SortKey('related_ids#order', path_spec='order_id'),
)
assert succ['output']['id'] == '8c9b55ef-e988-43ee-b2da-8594850605d7'
assert enrollment['related_ids#order'] == '3CNrFB9dy2RLit2pdeUWy4'
assert enrollment['parent_vacancy'] == {
'sk': '3CNrFB9dy2RLit2pdeUWy4#8c9b55ef-e988-43ee-b2da-8594850605d7',
'id': 'vacancies#cJtK9SsnJhKPyxESe7g3DG',
}
def test_enroll_vacancy(
mock_app,
dynamodb_seeds,
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
http_api_proxy: HttpApiProxy,
lambda_context: LambdaContext,
):
r = mock_app.lambda_handler(
http_api_proxy(
raw_path='/enrollments',
method=HTTPMethod.POST,
headers={'X-Tenant': 'cJtK9SsnJhKPyxESe7g3DG'},
body={
'items': [
{
'user': {
'id': '9a41e867-55e1-4573-bd27-7b5d1d1bcfde',
'name': 'Tiago Maciel',
'email': 'tiago@somosbeta.com.br',
'cpf': '08679004901',
},
'course': {
'id': '6d69a34a-cefd-40aa-a89b-dceb694c3e61',
'name': 'pytest',
},
'deduplication_window': {
'offset_days': 60,
},
'vacancy': {
'id': 'vacancies#cJtK9SsnJhKPyxESe7g3DG',
'sk': '3CNrFB9dy2RLit2pdeUWy4#does_not_exist',
},
},
],
},
),
lambda_context,
)
assert r['statusCode'] == HTTPStatus.OK
fail, *_ = json.loads(r['body'])
assert fail['cause'] == {
'type': 'VacancyDoesNotExistError',
'message': 'Vacancy does not exist',
}
def test_vacancies( def test_vacancies(
mock_app, mock_app,
@@ -119,11 +187,12 @@ def test_cancel_enrollment(
assert r['statusCode'] == HTTPStatus.OK assert r['statusCode'] == HTTPStatus.OK
collect = DynamoDBCollection(dynamodb_persistence_layer) enrollment = dynamodb_persistence_layer.collection.get_item(
enrollment = collect.get_item(KeyPair('43ea4475-c369-4f90-b576-135b7df5106b', '0')) KeyPair('43ea4475-c369-4f90-b576-135b7df5106b', '0')
)
assert enrollment['status'] == 'CANCELED' assert enrollment['status'] == 'CANCELED'
vacancies = collect.query( vacancies = dynamodb_persistence_layer.collection.query(
PartitionKey(ComposeKey('cJtK9SsnJhKPyxESe7g3DG', 'vacancies')) PartitionKey(ComposeKey('cJtK9SsnJhKPyxESe7g3DG', 'vacancies'))
) )
assert len(vacancies['items']) == 1 assert len(vacancies['items']) == 2

View File

@@ -247,12 +247,10 @@ def test_patch_email(
assert r['statusCode'] == HTTPStatus.OK assert r['statusCode'] == HTTPStatus.OK
collect = DynamoDBCollection(dynamodb_persistence_layer) user = dynamodb_persistence_layer.collection.get_item(
user = collect.get_item(KeyPair('5OxmMjL-ujoR5IMGegQz', '0')) KeyPair('5OxmMjL-ujoR5IMGegQz', '0')
print(user) )
# assert user['emails'] == { assert user['email'] == 'osergiosiqueira@gmail.com'
# 'sergio@somosbeta.com.br',
# }
def test_delete_email( def test_delete_email(

View File

@@ -22,3 +22,4 @@
{"id": {"S": "cpf"}, "sk": {"S": "07879819908"}} {"id": {"S": "cpf"}, "sk": {"S": "07879819908"}}
{"id": {"S": "cpf"}, "sk": {"S": "08679004901"}} {"id": {"S": "cpf"}, "sk": {"S": "08679004901"}}
{"id": {"S": "lock"}, "sk": {"S": "c2116a43f8f1aed659a10c83dab17ed3"}} {"id": {"S": "lock"}, "sk": {"S": "c2116a43f8f1aed659a10c83dab17ed3"}}
{"id": {"S": "vacancies#cJtK9SsnJhKPyxESe7g3DG"}, "sk": {"S": "3CNrFB9dy2RLit2pdeUWy4#8c9b55ef-e988-43ee-b2da-8594850605d7"}}

View File

@@ -65,11 +65,11 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
) )
for k, v in ids.items(): for k, v in ids.items():
kind = k.removesuffix('_id')
transact.put( transact.put(
item={ item={
'id': new_image['id'], 'id': new_image['id'],
'sk': 'related_ids#%s' 'sk': f'related_ids#{kind}', # e.g. related_ids#user
% k.removesuffix('_id'), # e.g. related_ids#user
'create_date': now_, 'create_date': now_,
k: v, k: v,
} }