Skip to content

Commit 41cfd39

Browse files
JadeCaraJade Wibbelsgalvana
authored
LJ-365 New Comments models (#5833)
Co-authored-by: Jade Wibbels <[email protected]> Co-authored-by: Adrian Galvan <[email protected]>
1 parent 728b4c9 commit 41cfd39

File tree

7 files changed

+599
-54
lines changed

7 files changed

+599
-54
lines changed

.fides/db_dataset.yml

+40
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,46 @@ dataset:
143143
- name: user_id
144144
data_categories:
145145
- user.unique_id
146+
- name: comment
147+
fields:
148+
- name: comment_text
149+
data_categories:
150+
- system.operations
151+
- name: comment_type
152+
data_categories:
153+
- system.operations
154+
- name: created_at
155+
data_categories:
156+
- system.operations
157+
- name: id
158+
data_categories:
159+
- system.operations
160+
- name: updated_at
161+
data_categories:
162+
- system.operations
163+
- name: user_id
164+
data_categories:
165+
- system.operations
166+
- name: comment_reference
167+
fields:
168+
- name: comment_id
169+
data_categories:
170+
- system.operations
171+
- name: created_at
172+
data_categories:
173+
- system.operations
174+
- name: id
175+
data_categories:
176+
- system.operations
177+
- name: reference_id
178+
data_categories:
179+
- system.operations
180+
- name: reference_type
181+
data_categories:
182+
- system.operations
183+
- name: updated_at
184+
data_categories:
185+
- system.operations
146186
- name: client
147187
fields:
148188
- name: created_at

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ Changes can also be flagged with a GitHub label for tracking purposes. The URL o
2626
- DB model support for messages on `MonitorExecution` records [#5846](https://github.com/ethyca/fides/pull/5846) https://github.com/ethyca/fides/labels/db-migration
2727
- Added support for GPP String integration in Fides String [#5845](https://github.com/ethyca/fides/pull/5845)
2828
- Attachments storage capabilities (S3 or local) [#5812](https://github.com/ethyca/fides/pull/5812) https://github.com/ethyca/fides/labels/db-migration
29+
- DB model support for Comments [#5833](https://github.com/ethyca/fides/pull/5833/files) https://github.com/ethyca/fides/labels/db-migration
2930

3031
### Changed
3132
- Bumped supported Python versions to `3.10.16` and `3.9.21` [#5840](https://github.com/ethyca/fides/pull/5840)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
"""Add comments and comment references
2+
3+
Revision ID: 69ad6d844e21
4+
Revises: 6ea2171c544f
5+
Create Date: 2025-03-03 16:31:22.495305
6+
7+
"""
8+
9+
import sqlalchemy as sa
10+
from alembic import op
11+
12+
# revision identifiers, used by Alembic.
13+
revision = "69ad6d844e21"
14+
down_revision = "6ea2171c544f"
15+
branch_labels = None
16+
depends_on = None
17+
18+
19+
def upgrade():
20+
op.create_table(
21+
"comment",
22+
sa.Column("id", sa.String(), nullable=False),
23+
sa.Column("user_id", sa.String(), nullable=True),
24+
sa.Column("comment_text", sa.String(), nullable=False),
25+
sa.Column("comment_type", sa.String(), nullable=False),
26+
sa.Column(
27+
"created_at",
28+
sa.DateTime(timezone=True),
29+
server_default=sa.text("now()"),
30+
nullable=False,
31+
),
32+
sa.Column(
33+
"updated_at",
34+
sa.DateTime(timezone=True),
35+
server_default=sa.text("now()"),
36+
nullable=True,
37+
),
38+
sa.ForeignKeyConstraint(["user_id"], ["fidesuser.id"], ondelete="SET NULL"),
39+
sa.PrimaryKeyConstraint("id"),
40+
)
41+
op.create_table(
42+
"comment_reference",
43+
sa.Column("id", sa.String(), nullable=False),
44+
sa.Column("comment_id", sa.String(), nullable=False),
45+
sa.Column("reference_id", sa.String(), nullable=False),
46+
sa.Column("reference_type", sa.String(), nullable=False),
47+
sa.Column(
48+
"created_at",
49+
sa.DateTime(timezone=True),
50+
server_default=sa.text("now()"),
51+
nullable=False,
52+
),
53+
sa.Column(
54+
"updated_at",
55+
sa.DateTime(timezone=True),
56+
server_default=sa.text("now()"),
57+
nullable=True,
58+
),
59+
sa.ForeignKeyConstraint(["comment_id"], ["comment.id"], ondelete="CASCADE"),
60+
sa.PrimaryKeyConstraint("comment_id", "reference_id"),
61+
)
62+
# Add index on comment_reference.reference_id
63+
op.create_index(
64+
"ix_comment_reference_reference_id",
65+
"comment_reference",
66+
["reference_id"],
67+
)
68+
# Add index on comment_reference.reference_type
69+
op.create_index(
70+
"ix_comment_reference_reference_type",
71+
"comment_reference",
72+
["reference_type"],
73+
)
74+
# ### end Alembic commands ###
75+
76+
77+
def downgrade():
78+
# Drop the index on comment_reference.reference_id
79+
op.drop_index("ix_comment_reference_reference_id", table_name="comment_reference")
80+
# Drop the index on comment_reference.reference_type
81+
op.drop_index("ix_comment_reference_reference_type", table_name="comment_reference")
82+
op.drop_table("comment_reference")
83+
op.drop_table("comment")
84+
# ### end Alembic commands ###

src/fides/api/models/comment.py

+109
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
from enum import Enum as EnumType
2+
from typing import Any
3+
4+
from sqlalchemy import Column
5+
from sqlalchemy import Enum as EnumColumn
6+
from sqlalchemy import ForeignKey, String, UniqueConstraint
7+
from sqlalchemy.ext.declarative import declared_attr
8+
from sqlalchemy.orm import Session, relationship
9+
10+
from fides.api.db.base_class import Base
11+
from fides.api.models.attachment import Attachment, AttachmentReference
12+
from fides.api.models.fides_user import FidesUser # pylint: disable=unused-import
13+
14+
15+
class CommentType(str, EnumType):
16+
"""
17+
Enum for comment types. Indicates comment usage.
18+
19+
- notes are internal comments.
20+
- reply comments are public and may cause an email or other communciation to be sent
21+
"""
22+
23+
note = "note"
24+
reply = "reply"
25+
26+
27+
class CommentReferenceType(str, EnumType):
28+
"""
29+
Enum for comment reference types. Indicates where the comment is referenced.
30+
"""
31+
32+
manual_step = "manual_step"
33+
privacy_request = "privacy_request"
34+
35+
36+
class CommentReference(Base):
37+
"""
38+
Stores information about a comment and any other element which may reference that comment.
39+
"""
40+
41+
@declared_attr
42+
def __tablename__(cls) -> str:
43+
"""Overriding base class method to set the table name."""
44+
return "comment_reference"
45+
46+
comment_id = Column(String, ForeignKey("comment.id"), nullable=False)
47+
reference_id = Column(String, nullable=False)
48+
reference_type = Column(EnumColumn(CommentReferenceType), nullable=False)
49+
50+
__table_args__ = (
51+
UniqueConstraint("comment_id", "reference_id", name="comment_reference_uc"),
52+
)
53+
54+
comment = relationship(
55+
"Comment",
56+
back_populates="references",
57+
uselist=False,
58+
)
59+
60+
@classmethod
61+
def create(
62+
cls, db: Session, *, data: dict[str, Any], check_name: bool = False
63+
) -> "CommentReference":
64+
"""Creates a new comment reference record in the database."""
65+
return super().create(db=db, data=data, check_name=check_name)
66+
67+
68+
class Comment(Base):
69+
"""
70+
Stores information about a Comment.
71+
"""
72+
73+
user_id = Column(
74+
String, ForeignKey("fidesuser.id", ondelete="SET NULL"), nullable=True
75+
)
76+
comment_text = Column(String, nullable=False)
77+
comment_type = Column(EnumColumn(CommentType), nullable=False)
78+
79+
user = relationship(
80+
"FidesUser",
81+
backref="comments",
82+
lazy="selectin",
83+
uselist=False,
84+
)
85+
86+
references = relationship(
87+
"CommentReference",
88+
back_populates="comment",
89+
cascade="all, delete",
90+
uselist=True,
91+
)
92+
93+
def get_attachments(self, db: Session) -> list[Attachment]:
94+
"""Retrieve all attachments associated with this comment."""
95+
stmt = (
96+
db.query(Attachment)
97+
.join(
98+
AttachmentReference, Attachment.id == AttachmentReference.attachment_id
99+
)
100+
.where(AttachmentReference.reference_id == self.id)
101+
)
102+
return db.execute(stmt).scalars().all()
103+
104+
def delete(self, db: Session) -> None:
105+
"""Delete the comment and all associated references."""
106+
attachments = self.get_attachments(db)
107+
for attachment in attachments:
108+
attachment.delete(db)
109+
db.delete(self)

tests/ctl/models/conftest.py

+97
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import boto3
2+
import pytest
3+
from moto import mock_aws
4+
5+
from fides.api.models.attachment import (
6+
Attachment,
7+
AttachmentReference,
8+
AttachmentReferenceType,
9+
AttachmentType,
10+
)
11+
from fides.api.schemas.storage.storage import StorageDetails
12+
13+
14+
@pytest.fixture
15+
def s3_client(storage_config):
16+
with mock_aws():
17+
session = boto3.Session(
18+
aws_access_key_id="fake_access_key",
19+
aws_secret_access_key="fake_secret_key",
20+
region_name="us-east-1",
21+
)
22+
s3 = session.client("s3")
23+
s3.create_bucket(Bucket=storage_config.details[StorageDetails.BUCKET.value])
24+
yield s3
25+
26+
27+
@pytest.fixture
28+
def attachment_data(user, storage_config):
29+
"""Returns attachment data."""
30+
return {
31+
"user_id": user.id,
32+
"file_name": "file.txt",
33+
"attachment_type": AttachmentType.internal_use_only,
34+
"storage_key": storage_config.key,
35+
}
36+
37+
38+
@pytest.fixture
39+
def attachment(s3_client, db, attachment_data, monkeypatch):
40+
"""Creates an attachment."""
41+
42+
def mock_get_s3_client(auth_method, storage_secrets):
43+
return s3_client
44+
45+
monkeypatch.setattr("fides.api.tasks.storage.get_s3_client", mock_get_s3_client)
46+
attachment = Attachment.create_and_upload(
47+
db, data=attachment_data, attachment_file=b"file content"
48+
)
49+
yield attachment
50+
attachment.delete(db)
51+
52+
53+
@pytest.fixture
54+
def multiple_attachments(s3_client, db, attachment_data, user, monkeypatch):
55+
"""Creates multiple attachments."""
56+
57+
def mock_get_s3_client(auth_method, storage_secrets):
58+
return s3_client
59+
60+
monkeypatch.setattr("fides.api.tasks.storage.get_s3_client", mock_get_s3_client)
61+
62+
attachment_data["user_id"] = user.id
63+
attachment_data["file_name"] = "file_1.txt"
64+
attachment_1 = Attachment.create_and_upload(
65+
db, data=attachment_data, attachment_file=b"file content 1"
66+
)
67+
68+
attachment_data["file_name"] = "file_2.txt"
69+
attachment_2 = Attachment.create_and_upload(
70+
db, data=attachment_data, attachment_file=b"file content 2"
71+
)
72+
73+
attachment_data["file_name"] = "file_3.txt"
74+
attachment_3 = Attachment.create_and_upload(
75+
db, data=attachment_data, attachment_file=b"file content 3"
76+
)
77+
78+
yield attachment_1, attachment_2, attachment_3
79+
80+
attachment_1.delete(db)
81+
attachment_2.delete(db)
82+
attachment_3.delete(db)
83+
84+
85+
@pytest.fixture
86+
def attachment_reference(db, attachment):
87+
"""Creates an attachment reference."""
88+
attachment_reference = AttachmentReference.create(
89+
db=db,
90+
data={
91+
"attachment_id": attachment.id,
92+
"reference_id": "ref_1",
93+
"reference_type": AttachmentReferenceType.privacy_request,
94+
},
95+
)
96+
yield attachment_reference
97+
attachment_reference.delete(db)

0 commit comments

Comments
 (0)