-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add a feedback form that can be triggered after terminating a session, on a session card, in regular intervals, and in the footer. Feedback can optionally contain freeform text and include the users contact information. Feedback includes an anonymized version of any associated sessions. Closes #1742
- Loading branch information
Showing
65 changed files
with
1,876 additions
and
104 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
# 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 SmtpNotSetupError(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", | ||
) | ||
|
||
|
||
class FeedbackNotEnabledError(core_exceptions.BaseError): | ||
def __init__(self): | ||
super().__init__( | ||
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", | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 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.tools import models as tools_models | ||
|
||
|
||
class FeedbackRating(str, enum.Enum): | ||
GOOD = "good" | ||
OKAY = "okay" | ||
BAD = "bad" | ||
|
||
|
||
class AnonymizedSession(core_pydantic.BaseModel): | ||
id: str | ||
type: SessionType | ||
created_at: datetime.datetime | ||
|
||
version: tools_models.ToolVersionWithTool | ||
|
||
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 | ||
|
||
|
||
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" | ||
) | ||
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" | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors | ||
# SPDX-License-Identifier: Apache-2.0 | ||
|
||
|
||
import typing as t | ||
|
||
import fastapi | ||
from sqlalchemy import orm | ||
|
||
from capellacollab.core import database | ||
from capellacollab.feedback.models import Feedback | ||
from capellacollab.feedback.util import is_feedback_allowed, send_email | ||
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 | ||
|
||
router = fastapi.APIRouter() | ||
|
||
|
||
@router.get( | ||
"/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()) | ||
|
||
|
||
@router.post("/feedback") | ||
def submit_feedback( | ||
feedback: 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), | ||
): | ||
is_feedback_allowed(db) | ||
background_tasks.add_task(send_email, feedback, user, user_agent, db) | ||
return {"status": "sending"} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors | ||
# SPDX-License-Identifier: Apache-2.0 | ||
|
||
import smtplib | ||
import typing as t | ||
from email.mime.multipart import MIMEMultipart | ||
from email.mime.text import MIMEText | ||
|
||
from sqlalchemy import orm | ||
|
||
from capellacollab.config import config | ||
from capellacollab.feedback import exceptions | ||
from capellacollab.feedback.models import AnonymizedSession, Feedback | ||
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 ( | ||
FeedbackAnonymityPolicy, | ||
FeedbackConfiguration, | ||
) | ||
from capellacollab.users import models as users_models | ||
|
||
|
||
def format_session(session: AnonymizedSession): | ||
return f"{session.version.tool.name} ({session.version.name})" | ||
|
||
|
||
def is_feedback_allowed(db: orm.Session): | ||
if not config.smtp or not config.smtp.enabled: | ||
raise exceptions.SmtpNotSetupError() | ||
|
||
cfg = config_core.get_config(db, "global") | ||
assert isinstance(cfg, settings_config_models.GlobalConfiguration) | ||
feedback_config = FeedbackConfiguration.model_validate( | ||
cfg.feedback.model_dump() | ||
) | ||
if not feedback_config.enabled: | ||
raise exceptions.FeedbackNotEnabledError() | ||
|
||
|
||
def format_email( | ||
feedback: Feedback, | ||
user: t.Optional[users_models.DatabaseUser], | ||
user_agent: str | None, | ||
): | ||
message = "\n".join( | ||
[ | ||
f"Rating: {feedback.rating.value}", | ||
f"Text: {feedback.feedback_text or 'No feedback text provided'}", | ||
f"User: {f'{user.name} ({user.email})' if user else 'Anonymous User'}", | ||
f"User Agent: {user_agent or 'Unknown'}", | ||
f"Feedback Trigger: {feedback.trigger}", | ||
*[ | ||
session.model_dump_json(indent=2) | ||
for session in feedback.sessions | ||
], | ||
] | ||
) | ||
|
||
if len(feedback.sessions) > 0: | ||
return { | ||
"subject": f"New Feedback {feedback.rating.value.capitalize()} for {', '.join([format_session(session) for session in feedback.sessions])}", | ||
"message": message, | ||
} | ||
else: | ||
return { | ||
"subject": f"New General Feedback {feedback.rating.value.capitalize()}", | ||
"message": message, | ||
} | ||
|
||
|
||
def send_email( | ||
feedback: Feedback, | ||
user: users_models.DatabaseUser, | ||
user_agent: str | None, | ||
db: orm.Session, | ||
): | ||
is_feedback_allowed(db) | ||
assert config.smtp, "SMTP configuration is not set up" | ||
|
||
cfg = config_core.get_config(db, "global") | ||
assert isinstance(cfg, settings_config_models.GlobalConfiguration) | ||
feedback_config = FeedbackConfiguration.model_validate( | ||
cfg.feedback.model_dump() | ||
) | ||
|
||
match feedback_config.anonymity_policy: | ||
case FeedbackAnonymityPolicy.FORCE_ANONYMOUS: | ||
is_anonymous = True | ||
case FeedbackAnonymityPolicy.FORCE_IDENTIFIED: | ||
is_anonymous = False | ||
case _: | ||
is_anonymous = not feedback | ||
|
||
email_text = format_email( | ||
feedback, None if is_anonymous else user, user_agent | ||
) | ||
|
||
mailserver = smtplib.SMTP( | ||
config.smtp.host.split(":")[0], int(config.smtp.host.split(":")[1]) | ||
) | ||
mailserver.ehlo() | ||
mailserver.starttls() | ||
mailserver.ehlo() | ||
mailserver.login(config.smtp.user, config.smtp.password) | ||
|
||
for receiver in feedback_config.receivers: | ||
msg = MIMEMultipart() | ||
msg["From"] = config.smtp.sender | ||
msg["To"] = receiver | ||
msg["Subject"] = email_text["subject"] | ||
msg.attach(MIMEText(email_text["message"], "plain")) | ||
|
||
mailserver.sendmail(config.smtp.sender, receiver, msg.as_string()) | ||
|
||
mailserver.quit() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.