diff --git a/handler.py b/handler.py index 81cd29a..9f960f4 100644 --- a/handler.py +++ b/handler.py @@ -17,5 +17,6 @@ def generate_certificate_handler(event, context): logger.info(record) message_body = json.loads(record['body']) event_id = message_body['eventId'] - certificate_usecase.generate_certficates(event_id=event_id) + registration_id = message_body['registrationId'] + certificate_usecase.generate_certficates(event_id=event_id, registration_id=registration_id) SQS.delete_message(QueueUrl=CERTIFICATE_QUEUE, ReceiptHandle=record['receiptHandle']) diff --git a/model/events/event.py b/model/events/event.py index 3fe178a..73023c6 100644 --- a/model/events/event.py +++ b/model/events/event.py @@ -1,16 +1,45 @@ +import os from datetime import datetime from typing import Optional +from model.events.events_constants import EventStatus from pydantic import BaseModel, EmailStr, Extra, Field from pynamodb.attributes import BooleanAttribute, NumberAttribute, UnicodeAttribute +from pynamodb.indexes import AllProjection, LocalSecondaryIndex +from pynamodb.models import Model + + +class EventIdIndex(LocalSecondaryIndex): + class Meta: + index_name = 'eventId-index' + projection = AllProjection() + read_capacity_units = 1 + write_capacity_units = 1 + + hashKey = UnicodeAttribute(hash_key=True) + eventId = UnicodeAttribute(range_key=True) -from model.entities import Entities -from model.events.events_constants import EventStatus +class Event(Model): + # hk: v + # rk: # + class Meta: + table_name = os.getenv('EVENTS_TABLE') + region = os.getenv('REGION') + billing_mode = 'PAY_PER_REQUEST' + + hashKey = UnicodeAttribute(hash_key=True) + rangeKey = UnicodeAttribute(range_key=True) + + latestVersion = NumberAttribute(null=False) + entryStatus = UnicodeAttribute(null=False) + eventId = UnicodeAttribute(null=False) + + createDate = UnicodeAttribute(null=True) + updateDate = UnicodeAttribute(null=True) + createdBy = UnicodeAttribute(null=True) + updatedBy = UnicodeAttribute(null=True) -class Event(Entities, discriminator='Event'): - # hk: Event - # rk: v# name = UnicodeAttribute(null=True) description = UnicodeAttribute(null=True) status = UnicodeAttribute(null=True) @@ -25,6 +54,8 @@ class Event(Entities, discriminator='Event'): price = NumberAttribute(null=True) certificateTemplate = UnicodeAttribute(null=True) + eventIdIndex = EventIdIndex() + class EventIn(BaseModel): class Config: @@ -49,11 +80,11 @@ class EventOut(EventIn): class Config: extra = Extra.ignore - entryId: str = Field(..., title="ID") + eventId: str = Field(..., title="ID") createDate: datetime = Field(..., title="Created At") updateDate: datetime = Field(..., title="Updated At") createdBy: str = Field(..., title="Created By") - updatedBy: str = Field(..., title="Updated By") + updatedBy: str = Field(None, title="Updated By") bannerUrl: Optional[str] = Field(None, title="Banner Pre-signed URL") logoUrl: Optional[str] = Field(None, title="Logo Pre-signed URL") certificateTemplateUrl: Optional[str] = Field(None, title="Certificate Template Pre-signed URL") diff --git a/repository/events_repository.py b/repository/events_repository.py index db8308b..fac8b3e 100644 --- a/repository/events_repository.py +++ b/repository/events_repository.py @@ -1,23 +1,17 @@ import logging import os -from copy import deepcopy from datetime import datetime from http import HTTPStatus from typing import List, Tuple +from constants.common_constants import EntryStatus +from model.events.event import Event from pynamodb.connection import Connection from pynamodb.exceptions import ( - PutError, PynamoDBConnectionError, QueryError, TableDoesNotExist, - TransactWriteError, ) -from pynamodb.transactions import TransactWrite - -from constants.common_constants import EntryStatus -from model.events.event import Event, EventIn -from repository.repository_utils import RepositoryUtils class EventsRepository: @@ -29,16 +23,10 @@ def __init__(self) -> None: def query_events(self, event_id: str = None) -> Tuple[HTTPStatus, List[Event], str]: try: - if event_id: - range_key_prefix = f'v{self.latest_version}#{event_id}' - range_key_condition = Event.rangeKey.__eq__(range_key_prefix) - else: - range_key_prefix = f'v{self.latest_version}#' - range_key_condition = Event.rangeKey.startswith(range_key_prefix) - + range_key_condition = Event.eventId == event_id if event_id else None event_entries = list( - Event.query( - hash_key=self.core_obj, + Event.eventIdIndex.query( + hash_key=f'v{self.latest_version}', range_key_condition=range_key_condition, filter_condition=Event.entryStatus == EntryStatus.ACTIVE.value, ) @@ -73,93 +61,3 @@ def query_events(self, event_id: str = None) -> Tuple[HTTPStatus, List[Event], s logging.info(f'[{self.core_obj}={event_id}] Fetch Event data successful') return HTTPStatus.OK, event_entries, None - - def update_event(self, event_entry: Event, event_in: EventIn) -> Tuple[HTTPStatus, Event, str]: - current_version = event_entry.latestVersion - new_version = current_version + 1 - - data = RepositoryUtils.load_data(pydantic_schema_in=event_in, exclude_unset=True) - has_update, updated_data = RepositoryUtils.get_update( - old_data=RepositoryUtils.db_model_to_dict(event_entry), new_data=data - ) - if not has_update: - return HTTPStatus.OK, event_entry, 'no update' - try: - with TransactWrite(connection=self.conn) as transaction: - # Update Entry ----------------------------------------------------------------------------- - # check if there's update or none - updated_data.update( - updateDate=self.current_date, - updatedBy=os.getenv('CURRENT_USER'), - latestVersion=new_version, - ) - actions = [getattr(Event, k).set(v) for k, v in updated_data.items()] - transaction.update(event_entry, actions=actions) - - # Store Old Entry -------------------------------------------------------------------------- - old_event_entry = deepcopy(event_entry) - old_event_entry.rangeKey = event_entry.rangeKey.replace('v0#', f'v{new_version}#') - old_event_entry.latestVersion = current_version - old_event_entry.updatedBy = old_event_entry.updatedBy or os.getenv('CURRENT_USER') - transaction.save(old_event_entry) - - event_entry.refresh() - logging.info(f'[{event_entry.rangeKey}] ' f'Update event data successful') - return HTTPStatus.OK, event_entry, '' - - except TransactWriteError as e: - message = f'Failed to update event data: {str(e)}' - logging.error(f'[{event_entry.rangeKey}] {message}') - - return HTTPStatus.INTERNAL_SERVER_ERROR, None, message - - def delete_event(self, event_entry: Event) -> Tuple[HTTPStatus, str]: - try: - # create new entry with old data - current_version = event_entry.latestVersion - new_version = current_version + 1 - old_event_entry = deepcopy(event_entry) - old_event_entry.rangeKey = event_entry.rangeKey.replace('v0#', f'v{new_version}#') - old_event_entry.updatedBy = old_event_entry.updatedBy or os.getenv('CURRENT_USER') - old_event_entry.save() - - # set entry status to deleted - event_entry.updateDate = self.current_date - event_entry.updatedBy = os.getenv('CURRENT_USER') - event_entry.latestVersion = new_version - event_entry.entryStatus = EntryStatus.DELETED.value - event_entry.save() - - logging.info(f'[{event_entry.rangeKey}] ' f'Delete event data successful') - return HTTPStatus.OK, None - except PutError as e: - message = f'Failed to delete event data: {str(e)}' - logging.error(f'[{event_entry.rangeKey}] {message}') - return HTTPStatus.INTERNAL_SERVER_ERROR, message - - def update_event_after_s3_upload(self, event_entry: Event, event_in: EventIn) -> Tuple[HTTPStatus, Event, str]: - """ - This method is almost the same as the update_event() method, - but excludes the metadata e.g updatedBy, updateDate etc. - This is needed so that the lambda handler that triggers when a file - is uploaded on S3 works properly. - """ - data = RepositoryUtils.load_data(pydantic_schema_in=event_in, exclude_unset=True) - _, updated_data = RepositoryUtils.get_update( - old_data=RepositoryUtils.db_model_to_dict(event_entry), new_data=data - ) - - try: - with TransactWrite(connection=self.conn) as transaction: - actions = [getattr(Event, k).set(v) for k, v in updated_data.items()] - transaction.update(event_entry, actions=actions) - - event_entry.refresh() - logging.info(f'[{event_entry.rangeKey}] ' f'Update event data successful') - return HTTPStatus.OK, event_entry, '' - - except TransactWriteError as e: - message = f'Failed to update event data: {str(e)}' - logging.error(f'[{event_entry.rangeKey}] {message}') - - return HTTPStatus.INTERNAL_SERVER_ERROR, None, message diff --git a/resources/generate_certificate.yml b/resources/generate_certificate.yml index 55712a8..48e2094 100644 --- a/resources/generate_certificate.yml +++ b/resources/generate_certificate.yml @@ -27,3 +27,5 @@ certMaker: Resource: - arn:aws:dynamodb:ap-southeast-1:192218445313:table/${self:custom.stage}-sparcs-events-entities - arn:aws:dynamodb:ap-southeast-1:192218445313:table/${self:custom.stage}-sparcs-events-registrations + - arn:aws:dynamodb:ap-southeast-1:192218445313:table/${self:custom.stage}-sparcs-events + - arn:aws:dynamodb:ap-southeast-1:192218445313:table/${self:custom.stage}-sparcs-events/index/* diff --git a/serverless.yml b/serverless.yml index 627016d..c99833e 100644 --- a/serverless.yml +++ b/serverless.yml @@ -8,6 +8,7 @@ custom: bucket: ${self:custom.stage}-${self:custom.projectName}-file-bucket registrations: ${self:custom.stage}-${self:custom.projectName}-registrations certificateQueue: ${self:custom.stage}-${self:custom.projectName}-certificate-queue.fifo + events: ${self:custom.stage}-${self:custom.projectName} pythonRequirements: dockerizePip: non-linux noDeploy: @@ -34,6 +35,7 @@ provider: CERTIFICATE_QUEUE: ${self:custom.certificateQueue} REGISTRATIONS_TABLE: ${self:custom.registrations} ENTITIES_TABLE: ${self:custom.entities} + EVENTS_TABLE: ${self:custom.events} S3_BUCKET: ${self:custom.bucket} package: ${file(resources/package.yml)} diff --git a/usecase/certificate_usecase.py b/usecase/certificate_usecase.py index 44fecc8..3d09f73 100644 --- a/usecase/certificate_usecase.py +++ b/usecase/certificate_usecase.py @@ -27,7 +27,7 @@ def generate_certificate_html(self, template_img: str, name: str): htmlTemplate = j2.from_string(template) return htmlTemplate.render(template_img=template_img, name=name) - def generate_certficates(self, event_id: str): + def generate_certficates(self, event_id: str, registration_id: str = None): logger.info(f"Generating certificates for event: {event_id}") # Get Events Data @@ -39,7 +39,14 @@ def generate_certficates(self, event_id: str): template_img = event.certificateTemplate # Get Registration Data - status, registrations, message = self.__registrations_repository.query_registrations(event_id=event_id) + if registration_id: + status, registration, message = self.__registrations_repository.query_registrations( + event_id=event_id, registration_id=registration_id + ) + registrations = [registration] + else: + status, registrations, message = self.__registrations_repository.query_registrations(event_id=event_id) + if status != HTTPStatus.OK: logger.error(message) return @@ -49,6 +56,7 @@ def generate_certficates(self, event_id: str): with tempfile.TemporaryDirectory() as tmpdir: template_img_path = os.path.join(tmpdir, 'template_img.png') self.__s3_data_store.download_file(object_name=template_img, file_name=template_img_path) + zoom = 4 for registration in registrations: logger.info( @@ -72,8 +80,7 @@ def generate_certficates(self, event_id: str): # Get only the first page of the PDF---------------------------------------------------------------------------------- certificate_doc = fitz.open(certificate_path) - first_page_index = 0 - first_page = certificate_doc.load_page(first_page_index) + first_page = certificate_doc.load_page(0) # Create a new PDF to store the first page doc_first_page = fitz.open() @@ -87,8 +94,6 @@ def generate_certficates(self, event_id: str): certificate_pdf_object_key = f'certificates/{event_id}/{name}/{certificate_name_pdf}' self.__s3_data_store.upload_file(file_name=certificate_path, object_name=certificate_pdf_object_key) - # Convert to png----------------------------------------------------------------------------------------------------- - zoom = 4 mat = fitz.Matrix(zoom, zoom) pix = first_page.get_pixmap(matrix=mat)