Skip to content

Commit

Permalink
Added functionality to email certificate.
Browse files Browse the repository at this point in the history
Unit test is pending
  • Loading branch information
felder101 committed Nov 14, 2023
1 parent 6ebed49 commit fa5ee6b
Show file tree
Hide file tree
Showing 4 changed files with 123 additions and 19 deletions.
1 change: 1 addition & 0 deletions training/services/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
from .certificate import Certificate
from .quiz import QuizService
87 changes: 85 additions & 2 deletions training/services/quiz.py
Original file line number Diff line number Diff line change
@@ -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('''
<p>Hello $name,</p>
<p>
Congratulations!
</p>
<p>You've successfully passed the quiz for the $course_name.</p>
<p>Your certificate is attached below.</p>
<p>
If you have any questions or need further assistance,
email us at [email protected].
</p>
<p>Thank you.</p>
''')


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
Expand Down Expand Up @@ -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()
16 changes: 15 additions & 1 deletion training/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
38 changes: 22 additions & 16 deletions training/tests/test_quiz_service.py
Original file line number Diff line number Diff line change
@@ -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)

Expand All @@ -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
Expand All @@ -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
Expand Down

0 comments on commit fa5ee6b

Please sign in to comment.