import os import logging from io import BytesIO from typing import Any, Mapping from django.core.files import File from django.conf import settings from django.template import Template, Context from django.utils.text import slugify from labs.models import CertificateIssuance, CertificateType from labs.certificate.exceptions import CertificateFileGenerationError logger = logging.getLogger(__name__) # Remove this function after we have migrated all the certificates from tlms to labs. def deprecated_upload_certificate_file_to_aliyun_oss( file_path: str, file: File ) -> None: import opendal retry_layer = opendal.layers.RetryLayer(max_times=3, factor=2.0, jitter=True) operator = opendal.Operator( "oss", bucket=settings.ALIYUN_OSS_BUCKET, endpoint=f"https://oss-{settings.ALIYUN_OSS_REGION}.aliyuncs.com", access_key_id=settings.ALIYUN_OSS_ACCESS_KEY_ID, access_key_secret=settings.ALIYUN_OSS_ACCESS_KEY_SECRET, ).layer(retry_layer) file.seek(0) operator.write(file_path, file.read(), content_type="application/pdf") class CertificateStorage: storage_provider: str issuance: CertificateIssuance def __init__(self, issuance: CertificateIssuance): self.issuance = issuance def get_file_path(self) -> str: cert_type: CertificateType = self.issuance.cert_type cert_number: str = self.issuance.cert_number file_path = f"certificates/{slugify(f'{cert_type.short_name}_{cert_number}')}.pdf" if settings.DEBUG: file_path = f"debug/{file_path}" return file_path def save(self, file: File, content_type: str) -> None: raise NotImplementedError("Subclass must implement this method") class CeritificateOSSStorage(CertificateStorage): storage_provider = "aliyun" def save(self, file: File, content_type: str) -> None: import opendal retry_layer = opendal.layers.RetryLayer(max_times=3, factor=2.0, jitter=True) operator = opendal.Operator( "oss", bucket=settings.ALIYUN_OSS_BUCKET, endpoint=f"https://oss-{settings.ALIYUN_OSS_REGION}.aliyuncs.com", access_key_id=settings.ALIYUN_OSS_ACCESS_KEY_ID, access_key_secret=settings.ALIYUN_OSS_ACCESS_KEY_SECRET, ).layer(retry_layer) file.seek(0) file_path = self.get_file_path() operator.write(file_path, file.read(), content_type=content_type) self.issuance.file_storage_provider = self.storage_provider self.issuance.file_bucket = settings.ALIYUN_OSS_BUCKET self.issuance.file_object_key = file_path self.issuance.file_content_type = content_type self.issuance.save() class CertificateLocalStorage(CertificateStorage): storage_provider = "local" local_storage_path = "labs/certificates_local_storage/" def get_file_path(self) -> str: cert_type: CertificateType = self.issuance.cert_type cert_number: str = self.issuance.cert_number file_name = slugify(f"{cert_type.short_name}_{cert_number}") + ".pdf" return f"{self.local_storage_path}{file_name}" def save(self, file: File, content_type: str) -> None: file.seek(0) file_path = self.get_file_path() if not os.path.exists(os.path.dirname(file_path)): os.makedirs(os.path.dirname(file_path)) with open(file_path, "wb") as f: f.write(file.read()) self.issuance.file_storage_provider = self.storage_provider self.issuance.file_bucket = "" self.issuance.file_object_key = file_path self.issuance.file_content_type = content_type self.issuance.save() def get_storage_provider(storage_provider: str) -> CertificateStorage: if storage_provider == "aliyun": return CeritificateOSSStorage elif storage_provider == "local": return CertificateLocalStorage else: raise ValueError(f"Invalid storage provider: {storage_provider}") def render_certificate_pdf(template_str: str, context: Mapping[str, Any]) -> bytes: from weasyprint import HTML from weasyprint.text.fonts import FontConfiguration template = Template(template_str) html = template.render(Context(dict(context))) font_config = FontConfiguration() return HTML(string=html).write_pdf(font_config=font_config) def generate_issuance_pdf(issuance: CertificateIssuance): # generate_issuance_pdf is used to generate a PDF file for a certificate issuance record and upload it to Aliyun OSS. # Raise `CertificateFileGenerationError` if failed to generate the PDF file. cert_type = issuance.cert_type if not cert_type.template: raise CertificateFileGenerationError( f"Certificate type {cert_type.short_name} does not have a template" ) pdf = render_certificate_pdf( cert_type.template, { "name": issuance.recipient.first_name + " " + issuance.recipient.last_name, "issued_at": issuance.issued_at.strftime("%d/%m/%Y"), "certificate_number": issuance.cert_number.upper(), }, ) pdf_file = BytesIO(pdf) content_type = "application/pdf" storage: CertificateStorage = get_storage_provider( settings.CERTIFICATE_STORAGE_PROVIDER )(issuance) storage.save(pdf_file, content_type) logger.info( f"Successfully generated certificate PDF file for issuance {issuance.id}" )