Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,7 @@ POSTGRES_DATABASE=
POSTGRES_USER=
POSTGRES_PASSWORD=
POSTGRES_DATABASE_URL=

AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_REGION=
SES_EMAIL_FROM=
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,18 @@ pdm run ruff format .

All code needs to pass ruff formatting and linting before it can be merged.

### Logging

To add a logger to a new service or file, use the `LOGGER_NAME` function in `app/utilities/constants.py`

```python
from app.utilities.constants import LOGGER_NAME

log = logging.getLogger(LOGGER_NAME("my_service"))
```

If you'd like to create a new logger name in the hierarchy, you'll need to add it to `alembic.ini` under the logger section. Following the pre-existing examples for `logger_uvicorn` for example.

### Frontend

#### Prettier
Expand Down
12 changes: 12 additions & 0 deletions backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,3 +136,15 @@ To apply the migration, run the following command:
```bash
pdm run alembic upgrade head
```

### Logging

To add a logger to a new service or file, use the `LOGGER_NAME` function in `app/utilities/constants.py`

```python
from app.utilities.constants import LOGGER_NAME

log = logging.getLogger(LOGGER_NAME("my_service"))
```

If you'd like to create a new logger name in the hierarchy, you'll need to add it to `alembic.ini` under the logger section. Following the pre-existing examples for `logger_uvicorn` for example.
13 changes: 10 additions & 3 deletions backend/alembic.ini
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ version_path_separator = os # Use os.pathsep. Default configuration used for ne
# output_encoding = utf-8

# Updated in env.py using the POSTGRES_DATABASE_URL environment variable
# sqlalchemy.url =
# sqlalchemy.url =


[post_write_hooks]
Expand All @@ -83,8 +83,10 @@ ruff.executable = %(here)s/.venv/bin/ruff
ruff.options = check --fix REVISION_SCRIPT_FILENAME

# Logging configuration
# Every time you want to define a new sub-logger, you need to add it to loggers or it won't show up.
# Would recommend just using uvicorn."name of area you want to log" to identify a smaller scope
[loggers]
keys = root,sqlalchemy,alembic
keys = root,sqlalchemy,alembic,uvicorn

[handlers]
keys = console
Expand All @@ -107,12 +109,17 @@ level = INFO
handlers =
qualname = alembic

[logger_uvicorn]
level = INFO
handlers =
qualname = uvicorn

[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic

[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S
77 changes: 17 additions & 60 deletions backend/app/interfaces/email_service.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from abc import ABC, abstractmethod

from app.schemas.email_template import EmailContent, EmailTemplateType


class IEmailService(ABC):
"""
Expand All @@ -8,65 +10,20 @@ class IEmailService(ABC):
"""

@abstractmethod
def send_email(self, to: str, subject: str, body: str) -> dict:
"""
Sends an email with the given parameters.

:param to: Recipient's email address
:type to: str
:param subject: Subject of the email
:type subject: str
:param body: HTML body content of the email
:type body: str
:return: Provider-specific metadata (like message ID, thread ID, label IDs)
:rtype: dict
:raises Exception: if email was not sent successfully
"""
pass

@abstractmethod
def send_welcome_email(self, recipient: str, user_name: str) -> dict:
"""
Sends a welcome email to the specified user.

:param recipient: Email address of the user
:type recipient: str
:param user_name: Name of the user
:type user_name: str
:return: Provider-specific metadata for the sent email
:rtype: dict
:raises Exception: if email was not sent successfully
"""
pass

@abstractmethod
def send_password_reset_email(self, recipient: str, reset_link: str) -> dict:
"""
Sends a password reset email with the provided reset link.

:param recipient: Email address of the user requesting the reset
:type recipient: str
:param reset_link: Password reset link
:type reset_link: str
:return: Provider-specific metadata for the sent email
:rtype: dict
:raises Exception: if email was not sent successfully
"""
pass

@abstractmethod
def send_notification_email(self, recipient: str, notification_text: str) -> dict:
"""
Sends a notification email to the user with the provided notification text.
Examples of use case include matches completed and ready to view, new messages,
meeting time scheduled, etc.

:param recipient: Email address of the user
:type recipient: str
:param notification_text: The notification content
:type notification_text: str
:return: Provider-specific metadata for the sent email
:rtype: dict
:raises Exception: if email was not sent successfully
def send_email(
self, templateType: EmailTemplateType, content: EmailContent
) -> dict:
"""Send an email using the given template and content with a
respective service provider.

Args:
templateType (EmailTemplateType): Specifies the template
to be used for the email
content (EmailContent): Contains the recipient and data
to be used in the email

Returns:
dict: Provider-specific metadata if any
(like message ID, thread ID, label IDs)
"""
pass
28 changes: 13 additions & 15 deletions backend/app/interfaces/email_service_provider.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from abc import ABC, abstractmethod

from app.schemas.email_template import EmailContent, EmailTemplateType


class IEmailServiceProvider(ABC):
"""
Expand All @@ -9,22 +11,18 @@ class IEmailServiceProvider(ABC):

@abstractmethod
def send_email(
self, recipient: str, subject: str, body_html: str, body_text: str
self, templateType: EmailTemplateType, content: EmailContent
) -> dict:
"""
Sends an email using the provider's service.
"""_summary_

Args:
templateType (EmailTemplate): Helps provider determine which
template to use for the given email
content (EmailContent): Contains the recipient and data to be
used in the email

:param recipient: Email address of the recipient
:type recipient: str
:param subject: Subject of the email
:type subject: str
:param body_html: HTML body content of the email
:type body_html: str
:param body_text: Plain text content of the email
:type body_text: str
:return: Provider-specific metadata related to the sent email
(like message ID, status, etc.)
:rtype: dict
:raises Exception: if the email fails to send
Returns:
dict: Provider-specific metadata if any
(like message ID, thread ID, label IDs)
"""
pass
46 changes: 46 additions & 0 deletions backend/app/models/Matches.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import enum
import uuid

from sqlalchemy import Column, DateTime, Enum, ForeignKey
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func

from .Base import Base


class MatchStatus(enum.Enum):
PENDING_ADMIN_APPROVAL = "pending_admin_approval"
APPROVED = "approved"
REJECTED = "rejected"


class ScheduleStatus(enum.Enum):
SCHEDULED = "scheduled"
COMPLETED = "completed"
DECLINED = "declined" # This status is for when a user declines a scheduled match
PENDING_PARTICIPANT_RESPONSE = "pending_participant_response"
PENDING_VOLUNTEER_RESPONSE = "pending_volunteer_response"
CANCELLED = "cancelled" # This status is for when a match is cancelled by an admin


class Matches(Base):
__tablename__ = "matches"

id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)

participant_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
volunteer_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)

match_status = Column(
Enum(MatchStatus), nullable=True, default=MatchStatus.PENDING_ADMIN_APPROVAL
)
schedule_status = Column(Enum(ScheduleStatus), nullable=True)

created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
)

participant = relationship("User", foreign_keys=[participant_id])
volunteer = relationship("User", foreign_keys=[volunteer_id])
8 changes: 8 additions & 0 deletions backend/app/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import logging

from alembic import command
from alembic.config import Config

from app.utilities.constants import LOGGER_NAME

# Make sure all models are here to reflect all current models
# when autogenerating new migration
from .Base import Base
Expand All @@ -10,8 +14,12 @@
# Used to avoid import errors for the models
__all__ = ["Base", "User", "Role"]

log = logging.getLogger(LOGGER_NAME("models"))


def run_migrations():
log.info("Running run_migrations in models/__init__ on server startup")

alembic_cfg = Config("alembic.ini")
# Emulates `alembic upgrade head` to migrate up to latest revision
command.upgrade(alembic_cfg, "head")
18 changes: 12 additions & 6 deletions backend/app/routes/email.py → backend/app/routes/send_email.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@
from fastapi import APIRouter, Depends

from app.interfaces.email_service import IEmailService
from app.schemas.email_template import EmailContent, EmailTemplateType, MockEmailData
from app.services.email.amazon_ses_provider import (
get_email_service_provider,
)
from app.services.email.email_service import EmailService
from app.services.email.email_service_provider import AmazonSESEmailProvider

router = APIRouter(
prefix="/email",
Expand All @@ -13,16 +16,19 @@


def get_email_service() -> IEmailService:
email_provider = AmazonSESEmailProvider(aws_access_key="", aws_secret_key="")
return EmailService(email_provider)
return EmailService(provider=get_email_service_provider())


# TODO (Mayank, Nov 30th) - Remove test emails once email service is fully implemented
@router.post("/send-test-email/")
@router.post("/send-test")
async def send_welcome_email(
recipient: str,
user_name: str,
email_service: Annotated[IEmailService, Depends(get_email_service)],
):
email_service.send_welcome_email(recipient, user_name)
return {"message": f"Welcome email sent to {user_name}!"}
return email_service.send_email(
templateType=EmailTemplateType.TEST,
content=EmailContent[MockEmailData](
recipient=recipient, data=MockEmailData(name=user_name, date="2021-12-01")
),
)
34 changes: 34 additions & 0 deletions backend/app/schemas/email_template.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import json
from abc import ABC
from dataclasses import dataclass
from enum import Enum
from typing import Generic, TypeVar

EmailData = TypeVar("EmailData")


class TemplateData(ABC):
def get_formatted_string(self) -> str:
class_dict = self.__dict__
try:
formatted_string = json.dumps(class_dict) # Try to convert to a JSON string
except (TypeError, ValueError) as e:
raise Exception(f"Error converting class to JSON: {e}")

return formatted_string


@dataclass
class MockEmailData(TemplateData):
name: str
date: str


class EmailTemplateType(Enum):
TEST = "Test"


@dataclass
class EmailContent(Generic[EmailData]):
recipient: str
data: EmailData
Loading