wip
This commit is contained in:
BIN
certs/cert.pdf
BIN
certs/cert.pdf
Binary file not shown.
@@ -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')
|
||||||
|
|||||||
@@ -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>
|
||||||
|
<div>
|
||||||
<p>Francis Ricardo Baretta</p>
|
<p>Francis Ricardo Baretta</p>
|
||||||
<p>CPF 039.539.409-02</p>
|
<p>CPF 039.539.409-02</p>
|
||||||
<p>Eng. de Segurança no Trabalho Eng. Eletricista</p>
|
<p>Eng. de Segurança no Trabalho Eng. Eletricista</p>
|
||||||
<p>CREA/SC 126693-0</p>
|
<p>CREA/SC 126693-0</p>
|
||||||
|
</div>
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -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
26
certs/uv.lock
generated
@@ -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"
|
||||||
|
|||||||
@@ -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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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'),
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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"}}
|
||||||
@@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user