Skip to content

Commit

Permalink
Merge pull request #876 from ChildMindInstitute/M2-3741
Browse files Browse the repository at this point in the history
M2-3741: creator_id save and transfer ownership history
  • Loading branch information
Damirkhon committed Nov 29, 2023
2 parents b3ab85e + 591c71c commit fe493ec
Show file tree
Hide file tree
Showing 9 changed files with 206 additions and 15 deletions.
3 changes: 3 additions & 0 deletions src/apps/applets/db/schemas/applet.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ class AppletSchema(_BaseAppletSchema, Base):
retention_period = Column(Integer(), nullable=True)
retention_type = Column(String(20), nullable=True)
is_published = Column(Boolean(), default=False)
creator_id = Column(
ForeignKey("users.id", ondelete="RESTRICT"), nullable=True
)


class HistoryMixin:
Expand Down
9 changes: 6 additions & 3 deletions src/apps/applets/service/applet.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ async def create(
manager_id: uuid.UUID | None = None,
manager_role: Role | None = None,
) -> AppletFull:
applet = await self._create(create_data)
applet = await self._create(create_data, manager_id or self.user_id)

await self._create_applet_accesses(applet.id, manager_id, manager_role)

Expand All @@ -149,7 +149,9 @@ async def create(

return applet

async def _create(self, create_data: AppletCreate) -> AppletFull:
async def _create(
self, create_data: AppletCreate, creator_id: uuid.UUID
) -> AppletFull:
applet_id = uuid.uuid4()
await self._validate_applet_name(create_data.display_name)
if not create_data.theme_id:
Expand Down Expand Up @@ -177,6 +179,7 @@ async def _create(self, create_data: AppletCreate) -> AppletFull:
if create_data.encryption
else None,
extra_fields=create_data.extra_fields,
creator_id=creator_id,
)
)
return AppletFull.from_orm(schema)
Expand Down Expand Up @@ -250,7 +253,7 @@ async def duplicate(
applet_exist, new_name, encryption
)

applet = await self._create(create_data)
applet = await self._create(create_data, self.user_id)
# TODO: move to api level
await UserAppletAccessService(
self.session, applet_owner.user_id, applet.id
Expand Down
7 changes: 7 additions & 0 deletions src/apps/transfer_ownership/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from enum import Enum


class TransferOwnershipStatus(str, Enum):
PENDING = "pending"
APPROVED = "approved"
DECLINED = "declined"
33 changes: 27 additions & 6 deletions src/apps/transfer_ownership/crud.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import uuid

from sqlalchemy import delete
from sqlalchemy import select, update

from apps.transfer_ownership.constants import TransferOwnershipStatus
from apps.transfer_ownership.db.schemas import TransferSchema
from apps.transfer_ownership.domain import Transfer
from apps.transfer_ownership.errors import TransferNotFoundError
Expand All @@ -17,17 +18,37 @@ async def create(self, transfer: Transfer) -> TransferSchema:
return await self._create(TransferSchema(**transfer.dict()))

async def get_by_key(self, key: uuid.UUID) -> TransferSchema:
if not (instance := await self._get(key="key", value=key)):
query = select(self.schema_class)
query = query.where(self.schema_class.key == key)
query = query.where(
self.schema_class.status == TransferOwnershipStatus.PENDING
)
result = await self._execute(query)
instance = result.scalars().first()
if not instance:
raise TransferNotFoundError()

return instance

async def delete_all_by_applet_id(self, applet_id: uuid.UUID) -> None:
query = delete(self.schema_class)
async def decline_all_pending_by_applet_id(
self, applet_id: uuid.UUID
) -> None:
query = update(self.schema_class)
query = query.where(TransferSchema.applet_id == applet_id)
query = query.where(
self.schema_class.status == TransferOwnershipStatus.PENDING
)
query = query.values(status=TransferOwnershipStatus.DECLINED)
await self._execute(query)

async def decline_by_key(self, key: uuid.UUID) -> None:
query = update(self.schema_class)
query = query.where(self.schema_class.key == key)
query = query.values(status=TransferOwnershipStatus.DECLINED)
await self._execute(query)

async def delete_by_key(self, key: uuid.UUID) -> None:
query = delete(self.schema_class)
async def approve_by_key(self, key: uuid.UUID) -> None:
query = update(self.schema_class)
query = query.where(self.schema_class.key == key)
query = query.values(status=TransferOwnershipStatus.APPROVED)
await self._execute(query)
8 changes: 6 additions & 2 deletions src/apps/transfer_ownership/db/schemas.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
from sqlalchemy import Column, ForeignKey, String
from sqlalchemy import Column, ForeignKey, String, Unicode
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy_utils import StringEncryptedType

from apps.shared.encryption import get_key
from apps.transfer_ownership.constants import TransferOwnershipStatus
from infrastructure.database import Base


class TransferSchema(Base):
__tablename__ = "transfer_ownership"

email = Column(String())
email = Column(StringEncryptedType(Unicode, get_key))
applet_id = Column(
ForeignKey("applets.id", ondelete="RESTRICT"), nullable=False
)
key = Column(UUID(as_uuid=True))
status = Column(String(), server_default=TransferOwnershipStatus.PENDING)
2 changes: 2 additions & 0 deletions src/apps/transfer_ownership/domain.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from pydantic import EmailStr

from apps.shared.domain import InternalModel
from apps.transfer_ownership.constants import TransferOwnershipStatus

__all__ = [
"Transfer",
Expand All @@ -16,6 +17,7 @@ class Transfer(InternalModel):
email: EmailStr
applet_id: uuid.UUID
key: uuid.UUID
status: TransferOwnershipStatus


class InitiateTransfer(InternalModel):
Expand Down
5 changes: 4 additions & 1 deletion src/apps/transfer_ownership/fixtures/transfers.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@
"created_at": "2023-01-05T15:49:51.752113",
"updated_at": "2023-01-05T15:49:51.752113",
"is_deleted": false,
"email": "lucy@gmail.com",
"email": "VwkVdPjpEtq4bS35CpEtwg==",
"applet_id": "92917a56-d586-4613-b7aa-991f2c4b15b1",
"key": "6a3ab8e6-f2fa-49ae-b2db-197136677da7"
},
"note": {
"plain_email": "lucy@gmail.com"
}
}
]
8 changes: 5 additions & 3 deletions src/apps/transfer_ownership/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from apps.invitations.services import InvitationsService
from apps.mailing.domain import MessageSchema
from apps.mailing.services import MailingService
from apps.transfer_ownership.constants import TransferOwnershipStatus
from apps.transfer_ownership.crud import TransferCRUD
from apps.transfer_ownership.domain import InitiateTransfer, Transfer
from apps.transfer_ownership.errors import TransferEmailError
Expand Down Expand Up @@ -39,6 +40,7 @@ async def initiate_transfer(
email=transfer_request.email,
applet_id=applet_id,
key=uuid.uuid4(),
status=TransferOwnershipStatus.PENDING,
)
await TransferCRUD(self.session).create(transfer)
try:
Expand Down Expand Up @@ -100,8 +102,8 @@ async def accept_transfer(self, applet_id: uuid.UUID, key: uuid.UUID):
await AnswersCRUD(self.session).delete_by_applet_user(
applet_id=transfer.applet_id
)

await TransferCRUD(self.session).delete_all_by_applet_id(
await TransferCRUD(self.session).approve_by_key(key=key)
await TransferCRUD(self.session).decline_all_pending_by_applet_id(
applet_id=transfer.applet_id
)

Expand Down Expand Up @@ -148,4 +150,4 @@ async def decline_transfer(self, applet_id: uuid.UUID, key: uuid.UUID):
raise PermissionsError()

# delete transfer
await TransferCRUD(self.session).delete_by_key(key=key)
await TransferCRUD(self.session).decline_by_key(key=key)
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
"""Creator id to applet, transfer status, email encryption
Revision ID: 75c9ca1f506b
Revises: 93087521e7ee
Create Date: 2023-11-28 11:51:29.381770
"""
import uuid

import sqlalchemy as sa
from alembic import op
from sqlalchemy.dialects import postgresql
from sqlalchemy_utils.types.encrypted.encrypted_type import StringEncryptedType

from apps.shared.encryption import get_key

# revision identifiers, used by Alembic.
revision = "75c9ca1f506b"
down_revision = "93087521e7ee"
branch_labels = None
depends_on = None


def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column(
"applets",
sa.Column("creator_id", postgresql.UUID(as_uuid=True), nullable=True),
)
op.create_foreign_key(
op.f("fk_applets_creator_id_users"),
"applets",
"users",
["creator_id"],
["id"],
ondelete="RESTRICT",
)
conn = op.get_bind()
result = conn.execute(
sa.text(
f"""
SELECT DISTINCT a.id, uaa.user_id, a.extra_fields->>'creator' FROM applets a
JOIN user_applet_accesses uaa on a.id = uaa.applet_id
WHERE a.is_deleted is false and uaa.role='owner'
and uaa.is_deleted is false;
"""
)
)
for row in result:
pk, owner_id, creator_id = row
if creator_id:
creator_id = uuid.UUID(str(creator_id) + "00000000")
conn.execute(
sa.text(
f"""
UPDATE applets
SET creator_id = :creator_id
WHERE id = :pk
"""
),
{"creator_id": creator_id, "pk": pk},
)
else:
if owner_id:
conn.execute(
sa.text(
f"""
UPDATE applets
SET creator_id = :owner_id
WHERE id = :pk
"""
),
{"owner_id": owner_id, "pk": pk},
)

op.add_column(
"transfer_ownership",
sa.Column(
"status", sa.String(), server_default="pending", nullable=True
),
)

# encrypt email in transfer_ownership table
result_emails = conn.execute(
sa.text(
"SELECT id, email FROM transfer_ownership WHERE email IS NOT NULL"
)
)
op.alter_column(
"transfer_ownership",
"email",
type_=StringEncryptedType(sa.Unicode, get_key),
default=None,
)
for row in result_emails:
pk, email = row
encrypted_field = StringEncryptedType(
sa.Unicode, get_key
).process_bind_param(email, dialect=conn.dialect)
conn.execute(
sa.text(
f"""
UPDATE transfer_ownership
SET email = :encrypted_field
WHERE id = :pk
"""
),
{"encrypted_field": encrypted_field, "pk": pk},
)
# ### end Alembic commands ###


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column("transfer_ownership", "status")
op.drop_constraint(
op.f("fk_applets_creator_id_users"), "applets", type_="foreignkey"
)
op.drop_column("applets", "creator_id")

# decrypt email in transfer_ownership table
conn = op.get_bind()
result_emails = conn.execute(
sa.text(
"SELECT id, email FROM transfer_ownership WHERE email IS NOT NULL"
)
)
op.alter_column(
"transfer_ownership", "email", type_=sa.String(), default=None
)
for row in result_emails:
pk, email = row
decrypted_field = StringEncryptedType(
sa.Unicode, get_key
).process_result_value(email, dialect=conn.dialect)
conn.execute(
sa.text(
f"""
UPDATE transfer_ownership
SET email = :decrypted_field
WHERE id = :pk
"""
),
{"decrypted_field": decrypted_field, "pk": pk},
)
# ### end Alembic commands ###

0 comments on commit fe493ec

Please sign in to comment.