Skip to content

Commit

Permalink
refactor[wip]: Implement review comments for #1751
Browse files Browse the repository at this point in the history
MoritzWeber0 committed Sep 19, 2024
1 parent d13bed2 commit 4d3e8de
Showing 66 changed files with 604 additions and 673 deletions.
5 changes: 4 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -191,7 +191,7 @@ reach-registry:
fi

dev:
$(MAKE) -j5 dev-frontend dev-backend dev-oauth-mock dev-docs dev-storybook
$(MAKE) -j6 dev-frontend dev-backend dev-oauth-mock dev-smtp-mock dev-docs dev-storybook

dev-frontend:
$(MAKE) -C frontend dev
@@ -202,6 +202,9 @@ dev-backend:
dev-oauth-mock:
$(MAKE) -C mocks/oauth start

dev-smtp-mock:
$(MAKE) -C mocks/smtp start

dev-docs:
$(MAKE) -C docs serve

13 changes: 9 additions & 4 deletions backend/capellacollab/config/models.py
Original file line number Diff line number Diff line change
@@ -330,25 +330,30 @@ class PrometheusConfig(BaseConfig):
)


class SmtpConfig(BaseConfig):
class SMTPConfig(BaseConfig):
enabled: bool = pydantic.Field(
default=False,
default=True,
description="Whether to enable SMTP. Necessary for feedback.",
examples=[True, False],
)
host: str = pydantic.Field(
description="The SMTP server host.",
default="localhost:587",
examples=["smtp.example.com:587"],
pattern=r"^(.*):(\d+)$",
)
user: str = pydantic.Field(
description="The SMTP server user.", examples=["username"]
default="username",
description="The SMTP server user.",
examples=["username"],
)
password: str = pydantic.Field(
default="password",
description="The SMTP server password.",
examples=["password"],
)
sender: str = pydantic.Field(
default="[email protected]",
description="The sender email address.",
examples=["[email protected]"],
)
@@ -366,4 +371,4 @@ class AppConfig(BaseConfig):
logging: LoggingConfig = LoggingConfig()
requests: RequestsConfig = RequestsConfig()
pipelines: PipelineConfig = PipelineConfig()
smtp: t.Optional[SmtpConfig] = None
smtp: SMTPConfig | None = SMTPConfig()
2 changes: 2 additions & 0 deletions backend/capellacollab/core/email/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors
# SPDX-License-Identifier: Apache-2.0
16 changes: 16 additions & 0 deletions backend/capellacollab/core/email/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors
# SPDX-License-Identifier: Apache-2.0

from fastapi import status

from capellacollab.core import exceptions as core_exceptions


class SMTPNotConfiguredError(core_exceptions.BaseError):
def __init__(self):
super().__init__(
status_code=status.HTTP_403_FORBIDDEN,
title="SMTP is not configured",
reason="SMTP must be configured in the application configuration before sending emails and activating related features.",
err_code="SMTP_NOT_CONFIGURED",
)
9 changes: 9 additions & 0 deletions backend/capellacollab/core/email/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors
# SPDX-License-Identifier: Apache-2.0

from capellacollab.core import pydantic as core_pydantic


class EMailContent(core_pydantic.BaseModelStrict):
subject: str
message: str
51 changes: 51 additions & 0 deletions backend/capellacollab/core/email/send.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors
# SPDX-License-Identifier: Apache-2.0

import logging
import smtplib
from email.mime import multipart, text

import pydantic

from capellacollab.config import config

from . import exceptions, models


def send_email(
recipients: list[pydantic.EmailStr],
email: models.EMailContent,
logger: logging.LoggerAdapter,
):
if not (config.smtp and config.smtp.enabled):
raise exceptions.SMTPNotConfiguredError()

Check warning on line 21 in backend/capellacollab/core/email/send.py

Codecov / codecov/patch

backend/capellacollab/core/email/send.py#L21

Added line #L21 was not covered by tests

try:

Check warning on line 23 in backend/capellacollab/core/email/send.py

Codecov / codecov/patch

backend/capellacollab/core/email/send.py#L23

Added line #L23 was not covered by tests
with smtplib.SMTP(
config.smtp.host.split(":")[0], int(config.smtp.host.split(":")[1])
) as smtp:
smtp.ehlo()
smtp.starttls()
smtp.ehlo()
smtp.login(config.smtp.user, config.smtp.password)

Check warning on line 30 in backend/capellacollab/core/email/send.py

Codecov / codecov/patch

backend/capellacollab/core/email/send.py#L27-L30

Added lines #L27 - L30 were not covered by tests

logger.info(

Check warning on line 32 in backend/capellacollab/core/email/send.py

Codecov / codecov/patch

backend/capellacollab/core/email/send.py#L32

Added line #L32 was not covered by tests
"Sending emails to recipients %s", ", ".join(recipients)
)

for recipient in recipients:
msg = multipart.MIMEMultipart()
msg["From"] = config.smtp.sender
msg["To"] = recipient
msg["Subject"] = email.subject
msg.attach(text.MIMEText(email.message, "plain"))

Check warning on line 41 in backend/capellacollab/core/email/send.py

Codecov / codecov/patch

backend/capellacollab/core/email/send.py#L37-L41

Added lines #L37 - L41 were not covered by tests

logger.info(

Check warning on line 43 in backend/capellacollab/core/email/send.py

Codecov / codecov/patch

backend/capellacollab/core/email/send.py#L43

Added line #L43 was not covered by tests
"Sending email to '%s' with subject '%s'",
recipient,
email.subject,
)

smtp.sendmail(config.smtp.sender, recipient, msg.as_string())
except Exception:
logger.exception("Error while sending email(s).")

Check warning on line 51 in backend/capellacollab/core/email/send.py

Codecov / codecov/patch

backend/capellacollab/core/email/send.py#L49-L51

Added lines #L49 - L51 were not covered by tests
20 changes: 10 additions & 10 deletions backend/capellacollab/feedback/exceptions.py
Original file line number Diff line number Diff line change
@@ -6,21 +6,21 @@
from capellacollab.core import exceptions as core_exceptions


class SmtpNotSetupError(core_exceptions.BaseError):
class FeedbackNotEnabledError(core_exceptions.BaseError):
def __init__(self):
super().__init__(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
title="SMTP is not set up",
reason="SMTP must be set up to perform this action",
err_code="SMTP_NOT_SETUP",
status_code=status.HTTP_403_FORBIDDEN,
title="Feedback is not enabled",
reason="Feedback must be set up to perform this action",
err_code="FEEDBACK_NOT_ENABLED",
)


class FeedbackNotEnabledError(core_exceptions.BaseError):
class NoFeedbackRecipientsError(core_exceptions.BaseError):
def __init__(self):
super().__init__(

Check warning on line 21 in backend/capellacollab/feedback/exceptions.py

Codecov / codecov/patch

backend/capellacollab/feedback/exceptions.py#L21

Added line #L21 was not covered by tests
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
title="Feedback is not set enabled",
reason="Feedback must be set up to perform this action",
err_code="FEEDBACK_NOT_SETUP",
status_code=status.HTTP_400_BAD_REQUEST,
title="The list of recipients is empty",
reason="Feedback can only be activated when there are recipients.",
err_code="FEEDBACK_MISSING_RECIPIENTS",
)
20 changes: 10 additions & 10 deletions backend/capellacollab/feedback/models.py
Original file line number Diff line number Diff line change
@@ -3,13 +3,12 @@

import datetime
import enum
import typing as t

import pydantic

from capellacollab.core import models as core_models
from capellacollab.core import pydantic as core_pydantic
from capellacollab.sessions.models import SessionType
from capellacollab.sessions import models as sessions_models
from capellacollab.tools import models as tools_models


@@ -21,31 +20,32 @@ class FeedbackRating(str, enum.Enum):

class AnonymizedSession(core_pydantic.BaseModel):
id: str
type: SessionType
type: sessions_models.SessionType
created_at: datetime.datetime

version: tools_models.ToolVersionWithTool
version: tools_models.MinimalToolVersionWithTool

state: str = pydantic.Field(default="UNKNOWN")
warnings: list[core_models.Message] = pydantic.Field(default=[])

connection_method_id: str
connection_method: tools_models.ToolSessionConnectionMethod | None = None
connection_method: (
tools_models.MinimalToolSessionConnectionMethod | None
) = None


class Feedback(core_pydantic.BaseModel):
rating: FeedbackRating = pydantic.Field(
description="The rating of the feedback"
)
feedback_text: t.Optional[str] = pydantic.Field(
description="The feedback text"
feedback_text: str | None = pydantic.Field(
description="The feedback text", max_length=255
)
share_contact: bool = pydantic.Field(
description="Whether the user wants to share their contact information"
)
sessions: list[AnonymizedSession] = pydantic.Field(
description="The sessions the feedback is for"
)
trigger: str = pydantic.Field(
description="What triggered the feedback form"
trigger: str | None = pydantic.Field(
description="What triggered the feedback form", max_length=255
)
48 changes: 32 additions & 16 deletions backend/capellacollab/feedback/routes.py
Original file line number Diff line number Diff line change
@@ -2,46 +2,62 @@
# SPDX-License-Identifier: Apache-2.0


import logging
import typing as t

import fastapi
from sqlalchemy import orm

from capellacollab.config import config
from capellacollab.core import database
from capellacollab.feedback.models import Feedback
from capellacollab.feedback.util import is_feedback_allowed, send_email
from capellacollab.core import logging as log
from capellacollab.core.authentication import injectables as auth_injectables
from capellacollab.settings.configuration import core as config_core
from capellacollab.settings.configuration import (
models as settings_config_models,
)
from capellacollab.settings.configuration.models import FeedbackConfiguration
from capellacollab.users import injectables as user_injectables
from capellacollab.users import models as users_models

from . import models, util

router = fastapi.APIRouter()


@router.get(
"/feedback",
"/configurations/feedback",
response_model=FeedbackConfiguration,
)
def get_feedback(db: orm.Session = fastapi.Depends(database.get_db)):
cfg = config_core.get_config(db, "global")
assert isinstance(cfg, settings_config_models.GlobalConfiguration)

return FeedbackConfiguration.model_validate(cfg.feedback.model_dump())
def get_feedback_configuration(
db: orm.Session = fastapi.Depends(database.get_db),
):
feedback = config_core.get_global_configuration(db).feedback
if not (config.smtp and config.smtp.enabled):
feedback.enabled = False

Check warning on line 34 in backend/capellacollab/feedback/routes.py

Codecov / codecov/patch

backend/capellacollab/feedback/routes.py#L34

Added line #L34 was not covered by tests
return feedback


@router.post("/feedback")
@router.post(
"/feedback",
status_code=204,
dependencies=[
fastapi.Depends(
auth_injectables.RoleVerification(
required_role=users_models.Role.USER
)
)
],
)
def submit_feedback(
feedback: Feedback,
feedback: models.Feedback,
background_tasks: fastapi.BackgroundTasks,
user_agent: t.Annotated[str | None, fastapi.Header()] = None,
user: users_models.DatabaseUser = fastapi.Depends(
user_injectables.get_own_user
),
db: orm.Session = fastapi.Depends(database.get_db),
logger: logging.LoggerAdapter = fastapi.Depends(log.get_request_logger),
):
is_feedback_allowed(db)
background_tasks.add_task(send_email, feedback, user, user_agent, db)
return {"status": "sending"}
util.check_if_feedback_is_allowed(db)

background_tasks.add_task(
util.send_feedback_email, db, feedback, user, user_agent, logger
)
Loading

0 comments on commit 4d3e8de

Please sign in to comment.