From fa5ee6b49053985f82367b9912f9306bf01bf428 Mon Sep 17 00:00:00 2001 From: Dan Felder Date: Tue, 14 Nov 2023 16:43:24 -0500 Subject: [PATCH] Added functionality to email certificate. Unit test is pending --- training/services/__init__.py | 1 + training/services/quiz.py | 87 ++++++++++++++++++++++++++++- training/tests/conftest.py | 16 +++++- training/tests/test_quiz_service.py | 38 +++++++------ 4 files changed, 123 insertions(+), 19 deletions(-) diff --git a/training/services/__init__.py b/training/services/__init__.py index 318ae515..30894c41 100644 --- a/training/services/__init__.py +++ b/training/services/__init__.py @@ -1 +1,2 @@ +from .certificate import Certificate from .quiz import QuizService diff --git a/training/services/quiz.py b/training/services/quiz.py index 7d48d434..73bb48a4 100644 --- a/training/services/quiz.py +++ b/training/services/quiz.py @@ -1,15 +1,50 @@ -from training.errors import IncompleteQuizResponseError, QuizNotFoundError -from training.repositories import QuizRepository, QuizCompletionRepository +import logging +from email.message import EmailMessage +from smtplib import SMTP +from string import Template + +from fastapi import HTTPException, status + +from training.config import settings +from training.errors import IncompleteQuizResponseError, QuizNotFoundError, SendEmailError +from training.repositories import QuizRepository, QuizCompletionRepository, UserRepository, CertificateRepository from training.schemas import Quiz, QuizSubmission, QuizGrade, QuizCompletionCreate from sqlalchemy.orm import Session +from training.services import Certificate + +CERTIFICATE_EMAIL_TEMPLATE = Template(''' +

Hello $name,

+ +

+Congratulations! +

+

You've successfully passed the quiz for the $course_name.

+

Your certificate is attached below.

+

+If you have any questions or need further assistance, +email us at gsa_smartpay@gsa.gov. +

+

Thank you.

+''') + class QuizService(): def __init__(self, db: Session): self.quiz_repo = QuizRepository(db) self.quiz_completion_repo = QuizCompletionRepository(db) + self.user_repo = UserRepository(db) + self.certificate_repo = CertificateRepository(db) + self.certificate_service = Certificate() def grade(self, quiz_id: int, user_id: int, submission: QuizSubmission) -> QuizGrade: + """ + Grades quizzes submitted by user. Sends congratulation email if user passes the quiz. + :param quiz_id: Quiz ID + :param user_id: User ID + :param submission: Quiz submission object + :return: QuizGrade model which includes quiz results + """ db_quiz = self.quiz_repo.find_by_id(quiz_id) if db_quiz is None: raise QuizNotFoundError @@ -72,4 +107,52 @@ def grade(self, quiz_id: int, user_id: int, submission: QuizSubmission) -> QuizG grade.quiz_completion_id = result.id + if passed: + # Send email with quizz completion attached + try: + user = self.user_repo.find_by_id(user_id) + db_user_certificate = self.certificate_repo.get_certificate_by_id(result.id) + pdf_bytes = self.certificate_service.generate_pdf( + db_user_certificate.quiz_name, + db_user_certificate.user_name, + db_user_certificate.agency, + db_user_certificate.completion_date + ) + self.emailCertificate(user.name, quiz.name, user.email, pdf_bytes) + logging.info(f"Sent confirmation email to {user.email} for passing training quiz") + except Exception as e: + logging.error("Error sending quiz confirmation mail", e) + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Server Error" + ) + return grade + + def emailCertificate(self, user_name: str, course_name: str, to_email: str, certificate: bytes) -> None: + """ + Sends congratulatory email to user with certificate attached. + :param user_name: User's Name + :param course_name: Name of course user completed + :param to_email: User's email + :param certificate: Certificate PDF file + :return: N/A + """ + body = CERTIFICATE_EMAIL_TEMPLATE.substitute({"name": user_name, "course_name": course_name}) + message = EmailMessage() + message.set_content(body, subtype="html") + message["Subject"] = "Certificate - " + course_name + message["From"] = f"{settings.EMAIL_FROM_NAME} <{settings.EMAIL_FROM}>" + message["To"] = to_email + message.add_attachment(certificate, maintype="application", subtype="pdf", filename="SmartPayTraining.pdf") + + with SMTP(settings.SMTP_SERVER, port=settings.SMTP_PORT) as smtp: + smtp.starttls() + if settings.SMTP_USER and settings.SMTP_PASSWORD: + smtp.login(user=settings.SMTP_USER, password=settings.SMTP_PASSWORD) + try: + smtp.send_message(message) + except Exception as e: + raise SendEmailError from e + finally: + smtp.quit() diff --git a/training/tests/conftest.py b/training/tests/conftest.py index b7d39ba5..9e963949 100644 --- a/training/tests/conftest.py +++ b/training/tests/conftest.py @@ -11,7 +11,7 @@ from sqlalchemy.orm import sessionmaker, Session from sqlalchemy import event from training.repositories import AgencyRepository, UserRepository, QuizRepository, QuizCompletionRepository, CertificateRepository, RoleRepository -from training.schemas import AgencyCreate, RoleCreate +from training.schemas import AgencyCreate, RoleCreate, UserCertificate from training.services import QuizService from training.config import settings from . import factories @@ -337,3 +337,17 @@ def valid_role(testdata: dict) -> Generator[RoleCreate, None, None]: jsondata = testdata["roles"][0] role = RoleCreate(name=jsondata["name"]) yield role + + +@pytest.fixture +def valid_user_certificate() -> Generator[schemas.UserCertificate, None, None]: + testdata = { + 'id': 1, + 'user_id': 2, + 'user_name': "Molly", + 'agency': 'Freeman Journal', + 'quiz_id': 100, + 'quiz_name': "Dublin History", + 'completion_date': '2023-08-21T22:59:36' + } + yield UserCertificate.model_validate(testdata) diff --git a/training/tests/test_quiz_service.py b/training/tests/test_quiz_service.py index 572f6a63..925c5b88 100644 --- a/training/tests/test_quiz_service.py +++ b/training/tests/test_quiz_service.py @@ -1,25 +1,31 @@ +from datetime import datetime + import pytest from unittest.mock import MagicMock, patch from training import models, schemas from training.errors import IncompleteQuizResponseError from training.services import QuizService -from training.repositories import QuizRepository, QuizCompletionRepository +from training.repositories import QuizRepository, QuizCompletionRepository, CertificateRepository from sqlalchemy.orm import Session from .factories import QuizCompletionFactory @patch.object(QuizRepository, "find_by_id") @patch.object(QuizCompletionRepository, "create") +@patch.object(CertificateRepository, "get_certificate_by_id") def test_grade_passing( - mock_quiz_completion_repo_create: MagicMock, - mock_quiz_repo_find_by_id: MagicMock, - db_with_data: Session, - valid_passing_submission: schemas.QuizSubmission, - valid_quiz: models.Quiz + mock_quiz_completion_repo_create: MagicMock, + mock_quiz_repo_find_by_id: MagicMock, + mock_certificate_repo_get_certificate_by_id: MagicMock, + db_with_data: Session, + valid_passing_submission: schemas.QuizSubmission, + valid_quiz: models.Quiz, + valid_user_certificate: schemas.UserCertificate ): quiz_service = QuizService(db_with_data) mock_quiz_repo_find_by_id.return_value = valid_quiz mock_quiz_completion_repo_create.return_value = QuizCompletionFactory.build() + mock_certificate_repo_get_certificate_by_id.return_value = valid_user_certificate result = quiz_service.grade(quiz_id=123, user_id=123, submission=valid_passing_submission) @@ -35,11 +41,11 @@ def test_grade_passing( @patch.object(QuizRepository, "find_by_id") @patch.object(QuizCompletionRepository, "create") def test_grade_failing( - mock_quiz_completion_repo_create: MagicMock, - mock_quiz_repo_find_by_id: MagicMock, - db_with_data: Session, - valid_failing_submission: schemas.QuizSubmission, - valid_quiz: models.Quiz + mock_quiz_completion_repo_create: MagicMock, + mock_quiz_repo_find_by_id: MagicMock, + db_with_data: Session, + valid_failing_submission: schemas.QuizSubmission, + valid_quiz: models.Quiz ): quiz_service = QuizService(db_with_data) mock_quiz_repo_find_by_id.return_value = valid_quiz @@ -59,11 +65,11 @@ def test_grade_failing( @patch.object(QuizRepository, "find_by_id") @patch.object(QuizCompletionRepository, "create") def test_grade_invalid( - mock_quiz_completion_repo_create: MagicMock, - mock_quiz_repo_find_by_id: MagicMock, - db_with_data: Session, - invalid_submission: schemas.QuizSubmission, - valid_quiz: models.Quiz + mock_quiz_completion_repo_create: MagicMock, + mock_quiz_repo_find_by_id: MagicMock, + db_with_data: Session, + invalid_submission: schemas.QuizSubmission, + valid_quiz: models.Quiz ): quiz_service = QuizService(db_with_data) mock_quiz_repo_find_by_id.return_value = valid_quiz