add server to gen pdf
This commit is contained in:
@@ -11,3 +11,4 @@ def get_dynamodb_client():
|
||||
|
||||
|
||||
dynamodb_client = get_dynamodb_client()
|
||||
s3_client = boto3.client('s3')
|
||||
|
||||
@@ -5,6 +5,8 @@ ORDER_TABLE: str = os.getenv('ORDER_TABLE') # type: ignore
|
||||
COURSE_TABLE: str = os.getenv('COURSE_TABLE') # type: ignore
|
||||
ENROLLMENT_TABLE: str = os.getenv('ENROLLMENT_TABLE') # type: ignore
|
||||
|
||||
BUCKET_NAME: str = os.getenv('BUCKET_NAME') # type: ignore
|
||||
|
||||
# Post-migration: Remove the following lines
|
||||
if os.getenv('AWS_LAMBDA_FUNCTION_NAME'):
|
||||
SQLITE_DATABASE = 'courses_export_2025-06-18_110214.db'
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import json
|
||||
import sqlite3
|
||||
from datetime import datetime, time, timedelta
|
||||
|
||||
from aws_lambda_powertools import Logger
|
||||
@@ -14,9 +16,16 @@ from layercake.dynamodb import (
|
||||
TransactKey,
|
||||
)
|
||||
from layercake.funcs import pick
|
||||
from sqlite_utils import Database
|
||||
|
||||
from boto3clients import dynamodb_client
|
||||
from config import COURSE_TABLE, ENROLLMENT_TABLE, ORDER_TABLE
|
||||
from config import (
|
||||
COURSE_TABLE,
|
||||
ENROLLMENT_TABLE,
|
||||
ORDER_TABLE,
|
||||
SQLITE_DATABASE,
|
||||
SQLITE_TABLE,
|
||||
)
|
||||
from utils import get_billing_period
|
||||
|
||||
logger = Logger(__name__)
|
||||
@@ -24,6 +33,8 @@ order_layer = DynamoDBPersistenceLayer(ORDER_TABLE, dynamodb_client)
|
||||
enrollment_layer = DynamoDBPersistenceLayer(ENROLLMENT_TABLE, dynamodb_client)
|
||||
course_layer = DynamoDBPersistenceLayer(COURSE_TABLE, dynamodb_client)
|
||||
|
||||
sqlite3.register_converter('json', json.loads)
|
||||
|
||||
|
||||
@event_source(data_class=EventBridgeEvent)
|
||||
@logger.inject_lambda_context
|
||||
@@ -41,6 +52,11 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
|
||||
logger.debug('Enrollment not found')
|
||||
return False
|
||||
|
||||
# Keep it until the migration has been completed
|
||||
old_course = _get_course(data['course']['id'])
|
||||
if old_course:
|
||||
data['course'] = old_course
|
||||
|
||||
start_date, end_date = get_billing_period(
|
||||
new_image['billing_day'],
|
||||
year=created_at.year,
|
||||
@@ -52,6 +68,8 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
|
||||
end=end_date.isoformat(),
|
||||
)
|
||||
|
||||
logger.info('Enrollment found', data=data)
|
||||
|
||||
try:
|
||||
with order_layer.transact_writer() as transact:
|
||||
transact.put(
|
||||
@@ -78,47 +96,44 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
|
||||
pass
|
||||
|
||||
try:
|
||||
with order_layer.transact_writer() as transact:
|
||||
author = data['author']
|
||||
course_id = data['course']['id']
|
||||
course = course_layer.collection.get_items(
|
||||
KeyPair(
|
||||
pk=course_id,
|
||||
sk=SortKey('0', path_spec='metadata__unit_price'),
|
||||
rename_key='unit_price',
|
||||
)
|
||||
+ KeyPair(
|
||||
pk=f'CUSTOM_PRICING#ORG#{org_id}',
|
||||
sk=SortKey(f'COURSE#{course_id}', path_spec='unit_price'),
|
||||
rename_key='unit_price',
|
||||
),
|
||||
flatten_top=False,
|
||||
author = data['author']
|
||||
course_id = data['course']['id']
|
||||
course = course_layer.collection.get_items(
|
||||
KeyPair(
|
||||
pk=course_id,
|
||||
sk=SortKey('0', path_spec='metadata__unit_price'),
|
||||
rename_key='unit_price',
|
||||
)
|
||||
|
||||
transact.condition(
|
||||
key=KeyPair(pk, sk),
|
||||
cond_expr='attribute_exists(sk)',
|
||||
exc_cls=BillingNotFoundError,
|
||||
)
|
||||
transact.put(
|
||||
item={
|
||||
'id': pk,
|
||||
'sk': f'{sk}#ENROLLMENT#{enrollment_id}',
|
||||
'user': pick(('id', 'name'), data['user']),
|
||||
'course': pick(('id', 'name'), data['course']),
|
||||
'unit_price': course['unit_price'],
|
||||
'author': {
|
||||
'id': author['user_id'],
|
||||
'name': author['name'],
|
||||
},
|
||||
# Post-migration: uncomment the following line
|
||||
# 'enrolled_at': data['created_at'],
|
||||
'enrolled_at': data['create_date'],
|
||||
'created_at': now_,
|
||||
+ KeyPair(
|
||||
pk=f'CUSTOM_PRICING#ORG#{org_id}',
|
||||
sk=SortKey(f'COURSE#{course_id}', path_spec='unit_price'),
|
||||
rename_key='unit_price',
|
||||
),
|
||||
flatten_top=False,
|
||||
)
|
||||
order_layer.put_item(
|
||||
item={
|
||||
'id': pk,
|
||||
'sk': f'{sk}#ENROLLMENT#{enrollment_id}',
|
||||
'user': pick(('id', 'name'), data['user']),
|
||||
'course': pick(('id', 'name'), data['course']),
|
||||
'unit_price': course['unit_price'],
|
||||
'author': {
|
||||
'id': author['user_id'],
|
||||
'name': author['name'],
|
||||
},
|
||||
cond_expr='attribute_not_exists(sk)',
|
||||
)
|
||||
except Exception:
|
||||
# Post-migration: uncomment the following line
|
||||
# 'enrolled_at': data['created_at'],
|
||||
'enrolled_at': data['create_date'],
|
||||
'created_at': now_,
|
||||
},
|
||||
cond_expr='attribute_not_exists(sk)',
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.exception(
|
||||
exc,
|
||||
keypair={'pk': pk, 'sk': sk},
|
||||
)
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
@@ -128,3 +143,18 @@ class ExistingBillingConflictError(Exception): ...
|
||||
|
||||
|
||||
class BillingNotFoundError(Exception): ...
|
||||
|
||||
|
||||
def _get_course(course_id: str) -> dict | None:
|
||||
with sqlite3.connect(
|
||||
database=SQLITE_DATABASE, detect_types=sqlite3.PARSE_DECLTYPES
|
||||
) as conn:
|
||||
db = Database(conn)
|
||||
rows = db[SQLITE_TABLE].rows_where(
|
||||
"json->>'$.metadata__betaeducacao_id' = ?", [course_id]
|
||||
)
|
||||
|
||||
for row in rows:
|
||||
return row['json']
|
||||
|
||||
return None
|
||||
|
||||
@@ -1,68 +1,93 @@
|
||||
import locale
|
||||
import os
|
||||
from datetime import date
|
||||
import json
|
||||
|
||||
import requests
|
||||
from aws_lambda_powertools import Logger
|
||||
from aws_lambda_powertools.shared.json_encoder import Encoder
|
||||
from aws_lambda_powertools.utilities.data_classes import (
|
||||
EventBridgeEvent,
|
||||
event_source,
|
||||
)
|
||||
from aws_lambda_powertools.utilities.typing import LambdaContext
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
from layercake.dateutils import fromisoformat
|
||||
from layercake.dateutils import now
|
||||
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair
|
||||
from weasyprint import HTML
|
||||
|
||||
from boto3clients import dynamodb_client
|
||||
from config import ORDER_TABLE
|
||||
from boto3clients import dynamodb_client, s3_client
|
||||
from config import BUCKET_NAME, ORDER_TABLE
|
||||
|
||||
logger = Logger(__name__)
|
||||
order_layer = DynamoDBPersistenceLayer(ORDER_TABLE, dynamodb_client)
|
||||
curdir = os.path.dirname(__file__)
|
||||
env = Environment(loader=FileSystemLoader(curdir))
|
||||
locale.setlocale(locale.LC_ALL, 'pt_BR.UTF-8')
|
||||
|
||||
|
||||
def currency(value: float | int) -> str:
|
||||
return locale.currency(value, grouping=True)
|
||||
|
||||
|
||||
def datetime_format(dt: date, fmt='%H:%M %d-%m-%y'):
|
||||
if isinstance(dt, str):
|
||||
dt = fromisoformat(dt) # type: ignore
|
||||
|
||||
return dt.strftime(fmt)
|
||||
|
||||
|
||||
env.filters['datetime_format'] = datetime_format
|
||||
env.filters['currency'] = currency
|
||||
|
||||
|
||||
@event_source(data_class=EventBridgeEvent)
|
||||
@logger.inject_lambda_context
|
||||
def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
|
||||
new_image = event.detail['new_image']
|
||||
_, start_date, _, end_date, *_ = new_image['sk'].split('#')
|
||||
keys = event.detail['keys']
|
||||
now_ = now()
|
||||
# Key pattern `BILLING#ORG#{org_id}`
|
||||
*_, org_id = keys['id'].split('#')
|
||||
# Key pattern `START#{start_date}#END#{end_date}#SCHEDULE#AUTO_CLOSE`
|
||||
_, start_date, _, end_date, *_ = keys['sk'].split('#')
|
||||
|
||||
result = order_layer.collection.query(
|
||||
KeyPair(
|
||||
pk=new_image['id'],
|
||||
pk=keys['id'],
|
||||
sk=f'START#{start_date}#END#{end_date}#ENROLLMENT',
|
||||
),
|
||||
)
|
||||
|
||||
template = env.get_template('tmpl.html')
|
||||
html_rendered = template.render(
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
items=result['items'],
|
||||
r = requests.post(
|
||||
'https://weasyprint.saladeaula.digital',
|
||||
data=json.dumps(
|
||||
{
|
||||
'template_s3_uri': 's3://saladeaula.digital/billing/template.html',
|
||||
'template_vars': {
|
||||
'start_date': start_date,
|
||||
'end_date': end_date,
|
||||
'items': result['items'],
|
||||
},
|
||||
},
|
||||
cls=Encoder,
|
||||
),
|
||||
)
|
||||
r.raise_for_status()
|
||||
|
||||
HTML(string=html_rendered, base_url='').write_pdf('cert.pdf')
|
||||
object_key = f'billing/{org_id}/{start_date}_{end_date}.pdf'
|
||||
s3_uri = f's3://{BUCKET_NAME}/{object_key}'
|
||||
|
||||
return order_layer.update_item(
|
||||
key=KeyPair(new_image['id'], new_image['sk']),
|
||||
update_expr='SET #status = :status',
|
||||
expr_attr_names={'#status': 'status'},
|
||||
expr_attr_values={':status': 'CLOSED'},
|
||||
)
|
||||
try:
|
||||
s3_client.put_object(
|
||||
Bucket=BUCKET_NAME,
|
||||
Key=object_key,
|
||||
Body=r.content,
|
||||
ContentType='application/pdf',
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.exception(exc)
|
||||
raise
|
||||
|
||||
with order_layer.transact_writer() as transact:
|
||||
transact.update(
|
||||
key=KeyPair(
|
||||
pk=keys['id'],
|
||||
sk=f'START#{start_date}#END#{end_date}',
|
||||
),
|
||||
update_expr='SET #status = :status, s3_uri = :s3_uri, \
|
||||
updated_at = :updated_at',
|
||||
expr_attr_names={'#status': 'status'},
|
||||
expr_attr_values={
|
||||
':status': 'CLOSED',
|
||||
':s3_uri': s3_uri,
|
||||
':updated_at': now_,
|
||||
},
|
||||
cond_expr='attribute_exists(sk)',
|
||||
)
|
||||
transact.put(
|
||||
item={
|
||||
'id': keys['id'],
|
||||
'sk': '{sk}#EXECUTED'.format(sk=keys['sk']),
|
||||
'created_at': now_,
|
||||
}
|
||||
)
|
||||
|
||||
logger.info(f'PDF uploaded successfully to {s3_uri}')
|
||||
return True
|
||||
|
||||
Binary file not shown.
@@ -1,200 +0,0 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title></title>
|
||||
<meta name="author" content="EDUSEG® <https://eduseg.com.br>" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<style>
|
||||
@page {
|
||||
size: A4 portrait;
|
||||
margin: 0.458cm;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Arial";
|
||||
src: url("fonts/Arial.ttf") format("truetype");
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
div,
|
||||
h1,
|
||||
h2,
|
||||
ul,
|
||||
p,
|
||||
a {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
font-size: 100%;
|
||||
font: inherit;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
html {
|
||||
font-family: SF-Pro;
|
||||
font-size: 11pt;
|
||||
line-height: 1.4;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
section {
|
||||
width: 100%;
|
||||
break-after: page;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
table {
|
||||
table-layout: auto;
|
||||
width: 100%;
|
||||
border-spacing: 0;
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid #efefef;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 0.625rem;
|
||||
}
|
||||
|
||||
tbody > tr > td {
|
||||
border-top: 1px solid #efefef;
|
||||
}
|
||||
|
||||
thead {
|
||||
background-color: #f3f4f680;
|
||||
font-weight: 600;
|
||||
display: table-header-group;
|
||||
}
|
||||
tr {
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
.space-y-0\.5 > :not(:last-child) {
|
||||
margin-bottom: 0.125rem;
|
||||
}
|
||||
|
||||
.space-y-2\.5 > :not(:last-child) {
|
||||
margin-bottom: 0.625rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<section class="space-y-2.5">
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 1143.4 257.88"
|
||||
style="width: 10rem"
|
||||
>
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: #8cd366;
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
fill: #2e3524;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g>
|
||||
<g>
|
||||
<path
|
||||
class="cls-1"
|
||||
d="M119.06,170.25l-53.68-24.18c-1.47-.94-3.35-.94-4.82,0l-53.68,24.18c-2.98,1.9-6.89-.24-6.89-3.77V7.01C0,4.54,2.01,2.54,4.48,2.54h117c2.47,0,4.48,2.01,4.48,4.48v159.46c0,3.54-3.91,5.68-6.89,3.77Z"
|
||||
/>
|
||||
<g>
|
||||
<path
|
||||
class="cls-2"
|
||||
d="M73.52,57.89H20.82v15.77h52.7v-15.77Z"
|
||||
/>
|
||||
<path
|
||||
class="cls-2"
|
||||
d="M84.06,86.96H20.82v20.97h63.24v-20.97Z"
|
||||
/>
|
||||
<path
|
||||
class="cls-2"
|
||||
d="M84.06,23.67H20.82v20.97h63.24v-20.97Z"
|
||||
/>
|
||||
<path
|
||||
class="cls-2"
|
||||
d="M105.14,102.66c0-2.91-2.36-5.27-5.27-5.27s-5.27,2.36-5.27,5.27,2.36,5.27,5.27,5.27,5.27-2.36,5.27-5.27Z"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<path
|
||||
class="cls-2"
|
||||
d="M191.46,2.54h72.24v34.76h-34.54v69.52h30.81v31h-30.81v82.76h34.54v34.76h-72.24V2.54Z"
|
||||
/>
|
||||
<path
|
||||
class="cls-2"
|
||||
d="M283.8,2.54h45.22c8.38,0,16.01,1.84,22.92,5.52,6.91,3.68,12.35,8.75,16.33,15.17,3.98,6.42,5.97,13.65,5.97,21.65v168.12c0,8-1.94,15.23-5.8,21.65-3.88,6.42-9.26,11.49-16.18,15.17-6.91,3.68-14.65,5.54-23.23,5.54h-45.22V2.54ZM334.52,222.11c1.36-1.46,2.03-3.27,2.03-5.37V41.12c-.2-2.1-1.04-3.89-2.52-5.37-1.47-1.46-3.25-2.22-5.34-2.22h-10.96v190.79h11.27c2.29,0,4.12-.73,5.49-2.22h.02Z"
|
||||
/>
|
||||
<path
|
||||
class="cls-2"
|
||||
d="M415.84,252.32c-6.81-3.68-12.2-8.73-16.18-15.17-3.97-6.42-5.97-13.63-5.97-21.65V2.54h37.68v218.61c0,2.1.73,3.91,2.2,5.37,1.46,1.48,3.25,2.22,5.34,2.22,2.29,0,4.12-.73,5.49-2.22,1.36-1.46,2.03-3.27,2.03-5.37V2.54h37.68v212.98c0,8.02-1.94,15.23-5.8,21.65-3.88,6.42-9.26,11.49-16.18,15.17-6.92,3.68-14.65,5.54-23.23,5.54s-16.27-1.84-23.08-5.54l.04-.02Z"
|
||||
/>
|
||||
<path
|
||||
class="cls-2"
|
||||
d="M526.39,252.32c-6.81-3.68-12.2-8.73-16.18-15.17-3.97-6.42-5.97-13.63-5.97-21.65v-61.61h37.68v67.24c0,2.1.73,3.91,2.2,5.37,1.45,1.48,3.25,2.21,5.34,2.21,2.29,0,4.12-.73,5.49-2.21,1.36-1.47,2.03-3.27,2.03-5.37v-60.94c0-4.43-1.73-8.06-5.19-10.91-3.45-2.84-9.05-6.27-16.8-10.27-6.49-3.36-11.78-6.37-15.86-9.01-4.09-2.63-7.6-6.16-10.52-10.59-2.93-4.43-4.4-9.58-4.4-15.49v-61.57c0-8,1.98-15.21,5.97-21.65,3.98-6.42,9.37-11.47,16.18-15.17,6.79-3.68,14.5-5.52,23.08-5.52s16.33,1.84,23.23,5.52c6.91,3.68,12.3,8.75,16.18,15.17,3.88,6.42,5.8,13.65,5.8,21.65v55.62h-37.68v-61.25c0-2.1-.69-3.89-2.03-5.37-1.36-1.46-3.19-2.22-5.49-2.22-2.09,0-3.88.73-5.34,2.22-1.48,1.48-2.2,3.27-2.2,5.37v54.61c0,4.84,1.83,8.81,5.51,11.85,3.66,3.06,9.57,6.91,17.75,11.53,6.29,3.38,11.4,6.33,15.4,8.84,3.97,2.53,7.33,5.84,10.04,9.95,2.72,4.11,4.09,8.81,4.09,14.06v67.94c0,8.02-1.94,15.23-5.8,21.65-3.88,6.42-9.26,11.49-16.18,15.17-6.92,3.68-14.65,5.54-23.23,5.54s-16.27-1.84-23.08-5.54Z"
|
||||
/>
|
||||
<path
|
||||
class="cls-2"
|
||||
d="M613.84,2.54h72.24v34.76h-34.54v69.52h30.81v31h-30.81v82.76h34.54v34.76h-72.24V2.54Z"
|
||||
/>
|
||||
<path
|
||||
class="cls-2"
|
||||
d="M720.31,252.32c-4.4-3.27-7.91-7.9-10.52-13.9-2.61-6.01-3.92-12.9-3.92-20.69V42.03c0-7.79,1.98-14.91,5.97-21.33,3.98-6.42,9.43-11.47,16.33-15.17,6.91-3.68,14.56-5.52,22.94-5.52s16.07,1.84,23.08,5.52c7.02,3.68,12.45,8.75,16.33,15.17,3.88,6.42,5.8,13.54,5.8,21.33v73.95h-37.68V36.71c0-2.1-.69-3.89-2.03-5.37-1.36-1.46-3.19-2.22-5.49-2.22-2.09,0-3.88.73-5.34,2.22-1.48,1.48-2.2,3.27-2.2,5.37v181.02c0,2.1.73,3.91,2.2,5.37,1.45,1.48,3.25,2.22,5.34,2.22,2.3,0,4.13-.73,5.49-2.22,1.36-1.47,2.03-3.27,2.03-5.37v-36.02h-9.11v-40.13h46.78v113.76h-37.68v-11.06c-2.31,4.23-5.34,7.42-9.11,9.63-3.77,2.22-8.47,3.32-14.13,3.32s-10.67-1.63-15.08-4.9l-.02-.02Z"
|
||||
/>
|
||||
<path
|
||||
class="cls-2"
|
||||
d="M824.08,19.6h-4.8l-.04-2.89h4.29c.65-.01,1.26-.12,1.82-.31.56-.21,1.01-.5,1.36-.89.34-.4.51-.89.51-1.47,0-.73-.13-1.3-.38-1.73-.24-.43-.65-.73-1.24-.91-.58-.19-1.36-.29-2.35-.29h-2.96v15.98h-3.22V8.24h6.18c1.47,0,2.73.21,3.8.64,1.08.41,1.91,1.05,2.49,1.91.59.84.89,1.9.89,3.18,0,.8-.19,1.51-.56,2.13-.37.62-.92,1.16-1.64,1.62-.71.44-1.59.81-2.62,1.09-.04,0-.1.05-.16.16-.04.1-.09.16-.13.16-.25.15-.42.26-.49.33-.06.06-.13.1-.2.11-.06.01-.24.02-.53.02ZM823.86,19.6l.47-2.2c2.31,0,3.89.5,4.73,1.51.85.99,1.27,2.26,1.27,3.8v1.2c0,.55.02,1.07.07,1.58.06.49.16.9.31,1.24v.36h-3.31c-.15-.39-.24-.93-.27-1.64-.02-.71-.02-1.23-.02-1.56v-1.16c0-1.08-.24-1.87-.73-2.38-.49-.5-1.33-.76-2.51-.76ZM810.38,17.93c0,1.97.33,3.81,1,5.51.68,1.69,1.63,3.17,2.84,4.44,1.22,1.26,2.61,2.24,4.2,2.96,1.6.7,3.3,1.04,5.11,1.04s3.53-.35,5.11-1.04c1.59-.71,2.98-1.7,4.18-2.96,1.2-1.27,2.14-2.76,2.82-4.44.68-1.7,1.02-3.54,1.02-5.51s-.34-3.8-1.02-5.49c-.68-1.69-1.62-3.16-2.82-4.42-1.2-1.26-2.59-2.24-4.18-2.93-1.59-.71-3.29-1.07-5.11-1.07s-3.51.36-5.11,1.07c-1.59.7-2.98,1.67-4.2,2.93-1.22,1.26-2.16,2.73-2.84,4.42-.67,1.69-1,3.52-1,5.49ZM807.75,17.93c0-2.36.41-4.54,1.22-6.55.81-2.01,1.95-3.77,3.4-5.27,1.45-1.51,3.13-2.68,5.02-3.51,1.91-.84,3.96-1.27,6.13-1.27s4.21.42,6.11,1.27c1.91.83,3.59,2,5.02,3.51,1.45,1.5,2.59,3.25,3.4,5.27.83,2.01,1.25,4.2,1.25,6.55s-.42,4.54-1.25,6.55c-.81,2.01-1.95,3.78-3.4,5.31-1.44,1.51-3.11,2.69-5.02,3.53-1.9.84-3.93,1.27-6.11,1.27s-4.22-.42-6.13-1.27c-1.9-.84-3.57-2.02-5.02-3.53-1.45-1.53-2.58-3.3-3.4-5.31-.82-2.01-1.22-4.2-1.22-6.55Z"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
<p>
|
||||
Matrículas realizadas entre
|
||||
<strong>{{ start_date|datetime_format('%d/%m/%Y') }}</strong> e
|
||||
<strong>{{ end_date|datetime_format('%d/%m/%Y') }}</strong>
|
||||
</p>
|
||||
|
||||
<table class="table-layout border border-gray-300">
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Curso</td>
|
||||
<td>Colaborador</td>
|
||||
<td>Matrículado em</td>
|
||||
<td>Valor unit.</td>
|
||||
<td>Autor</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for x in items %}
|
||||
<tr>
|
||||
<td>{{ x.course.name }}</td>
|
||||
<td>{{ x.user.name }}</td>
|
||||
<td>
|
||||
{{ x.enrolled_at|datetime_format('%d/%m/%Y, %H:%M')
|
||||
}}
|
||||
</td>
|
||||
<td>{{ x.unit_price|currency }}</td>
|
||||
<td>{{ x.author.name }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user