ref
This commit is contained in:
@@ -20,6 +20,7 @@ __all__ = (
|
|||||||
'PurchasePlanStatus',
|
'PurchasePlanStatus',
|
||||||
'PurchasePlanChannelStatus',
|
'PurchasePlanChannelStatus',
|
||||||
'Creative',
|
'Creative',
|
||||||
|
'replace_invite_link_with_tag',
|
||||||
'Placement',
|
'Placement',
|
||||||
'Post',
|
'Post',
|
||||||
'Subscription',
|
'Subscription',
|
||||||
@@ -52,17 +53,21 @@ __all__ = (
|
|||||||
'TelegramChannelNotFound',
|
'TelegramChannelNotFound',
|
||||||
'CreativeNotFound',
|
'CreativeNotFound',
|
||||||
'CreativeInUse',
|
'CreativeInUse',
|
||||||
|
'CreativeInviteLinkNotFound',
|
||||||
|
'CreativeMultipleInviteLinks',
|
||||||
'PlacementNotFound',
|
'PlacementNotFound',
|
||||||
'UserByUsernameNotFound',
|
'UserByUsernameNotFound',
|
||||||
)
|
)
|
||||||
|
|
||||||
from .channel import Channel
|
from .channel import Channel
|
||||||
from .creative import Creative, CreativeStatus
|
from .creative import Creative, CreativeStatus, replace_invite_link_with_tag
|
||||||
from .error import (
|
from .error import (
|
||||||
ChannelAlreadyExists,
|
ChannelAlreadyExists,
|
||||||
ChannelNoAdminRights,
|
ChannelNoAdminRights,
|
||||||
ChannelNotFound,
|
ChannelNotFound,
|
||||||
CreativeInUse,
|
CreativeInUse,
|
||||||
|
CreativeInviteLinkNotFound,
|
||||||
|
CreativeMultipleInviteLinks,
|
||||||
CreativeNotFound,
|
CreativeNotFound,
|
||||||
LoginTokenAlreadyUsed,
|
LoginTokenAlreadyUsed,
|
||||||
LoginTokenExpired,
|
LoginTokenExpired,
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import enum
|
import enum
|
||||||
|
import re
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from tortoise import fields
|
from tortoise import fields
|
||||||
|
|
||||||
from .base import TimestampedModel
|
from .base import TimestampedModel
|
||||||
|
from .error import CreativeInviteLinkNotFound, CreativeMultipleInviteLinks
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .project import Project
|
from .project import Project
|
||||||
@@ -34,3 +36,24 @@ class Creative(TimestampedModel):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
table = 'creative'
|
table = 'creative'
|
||||||
|
|
||||||
|
|
||||||
|
_INVITE_LINK_HTML = re.compile(r'<a\s+href=["\']https?://t\.me/\+[a-zA-Z0-9_-]+["\']>([^<]*)</a>', re.IGNORECASE)
|
||||||
|
_INVITE_LINK_PLAIN = re.compile(r'https?://t\.me/\+[a-zA-Z0-9_-]+', re.IGNORECASE)
|
||||||
|
|
||||||
|
|
||||||
|
def replace_invite_link_with_tag(text: str) -> str:
|
||||||
|
"""Replace Telegram invite link (t.me/+xxx) with tracking tag."""
|
||||||
|
|
||||||
|
html_count = len(_INVITE_LINK_HTML.findall(text))
|
||||||
|
plain_count = len(_INVITE_LINK_PLAIN.findall(text))
|
||||||
|
total = html_count + plain_count
|
||||||
|
|
||||||
|
if total == 0:
|
||||||
|
raise CreativeInviteLinkNotFound()
|
||||||
|
if total > 1:
|
||||||
|
raise CreativeMultipleInviteLinks()
|
||||||
|
|
||||||
|
if html_count == 1:
|
||||||
|
return _INVITE_LINK_HTML.sub(r'<a class="tg-link">\1</a>', text)
|
||||||
|
return _INVITE_LINK_PLAIN.sub('<a class="tg-link"></a>', text)
|
||||||
|
|||||||
@@ -71,6 +71,14 @@ def CreativeNotFound(creative_id: uuid.UUID | None = None) -> HTTPException:
|
|||||||
return HTTPException(status.HTTP_404_NOT_FOUND, f'Creative {creative_id} not found')
|
return HTTPException(status.HTTP_404_NOT_FOUND, f'Creative {creative_id} not found')
|
||||||
|
|
||||||
|
|
||||||
|
def CreativeInviteLinkNotFound() -> HTTPException:
|
||||||
|
return HTTPException(status.HTTP_400_BAD_REQUEST, 'Creative text must contain one invite link (t.me/+xxx)')
|
||||||
|
|
||||||
|
|
||||||
|
def CreativeMultipleInviteLinks() -> HTTPException:
|
||||||
|
return HTTPException(status.HTTP_400_BAD_REQUEST, 'Creative text must contain only one invite link')
|
||||||
|
|
||||||
|
|
||||||
def CreativeInUse(creative_id: uuid.UUID) -> HTTPException:
|
def CreativeInUse(creative_id: uuid.UUID) -> HTTPException:
|
||||||
return HTTPException(
|
return HTTPException(
|
||||||
status.HTTP_400_BAD_REQUEST,
|
status.HTTP_400_BAD_REQUEST,
|
||||||
|
|||||||
@@ -24,3 +24,9 @@ class Post(TimestampedModel):
|
|||||||
class Meta:
|
class Meta:
|
||||||
table = 'post'
|
table = 'post'
|
||||||
unique_together = (('channel_id', 'message_id'),)
|
unique_together = (('channel_id', 'message_id'),)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def url(self) -> str | None:
|
||||||
|
if not self.channel.username:
|
||||||
|
return None
|
||||||
|
return f'https://t.me/{self.channel.username}/{self.message_id}'
|
||||||
|
|||||||
@@ -101,10 +101,14 @@ class PermissionScopeType(enum.StrEnum):
|
|||||||
|
|
||||||
|
|
||||||
class WorkspaceUserPermission(TimestampedModel):
|
class WorkspaceUserPermission(TimestampedModel):
|
||||||
|
permission = fields.CharEnumField(PermissionKey)
|
||||||
|
|
||||||
workspace_user: fields.ForeignKeyRelation[WorkspaceUser] = fields.ForeignKeyField(
|
workspace_user: fields.ForeignKeyRelation[WorkspaceUser] = fields.ForeignKeyField(
|
||||||
'models.WorkspaceUser', related_name='permissions', on_delete=fields.CASCADE, index=True
|
'models.WorkspaceUser', related_name='permissions', on_delete=fields.CASCADE, index=True
|
||||||
)
|
)
|
||||||
permission = fields.CharEnumField(PermissionKey)
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
workspace_user_id: uuid.UUID
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
table = 'workspace_user_permission'
|
table = 'workspace_user_permission'
|
||||||
@@ -120,6 +124,9 @@ class WorkspaceUserPermissionScope(TimestampedModel):
|
|||||||
'models.WorkspaceUser', related_name='permission_scopes', on_delete=fields.CASCADE, index=True
|
'models.WorkspaceUser', related_name='permission_scopes', on_delete=fields.CASCADE, index=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
workspace_user_id: uuid.UUID
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
table = 'workspace_user_permission_scope'
|
table = 'workspace_user_permission_scope'
|
||||||
unique_together = (('workspace_user_id', 'permission', 'scope_type', 'scope_id'),)
|
unique_together = (('workspace_user_id', 'permission', 'scope_type', 'scope_id'),)
|
||||||
|
|||||||
@@ -47,6 +47,35 @@ class WorkspaceMemberOutput(pydantic.BaseModel):
|
|||||||
user: WorkspaceMemberUserOutput
|
user: WorkspaceMemberUserOutput
|
||||||
permissions: list[WorkspacePermissionOutput]
|
permissions: list[WorkspacePermissionOutput]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_domain(cls, member: domain.WorkspaceUser) -> 'WorkspaceMemberOutput':
|
||||||
|
if member.user is None:
|
||||||
|
raise ValueError('Workspace member user relation is not loaded')
|
||||||
|
|
||||||
|
permissions_map: dict[domain.PermissionKey, list[WorkspacePermissionScopeOutput]] = {}
|
||||||
|
|
||||||
|
for permission in getattr(member, 'permissions', []) or []:
|
||||||
|
permissions_map.setdefault(permission.permission, [])
|
||||||
|
|
||||||
|
for scope in getattr(member, 'permission_scopes', []) or []:
|
||||||
|
permissions_map.setdefault(scope.permission, []).append(
|
||||||
|
WorkspacePermissionScopeOutput(type=scope.scope_type, id=scope.scope_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
return cls(
|
||||||
|
id=member.id,
|
||||||
|
status=member.status,
|
||||||
|
user=WorkspaceMemberUserOutput(
|
||||||
|
id=member.user.id,
|
||||||
|
telegram_id=member.user.telegram_id,
|
||||||
|
username=member.user.username,
|
||||||
|
),
|
||||||
|
permissions=[
|
||||||
|
WorkspacePermissionOutput(key=key, scopes=scopes)
|
||||||
|
for key, scopes in sorted(permissions_map.items(), key=lambda item: item[0].value)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class GetWorkspaceMembersOutput(pydantic.BaseModel):
|
class GetWorkspaceMembersOutput(pydantic.BaseModel):
|
||||||
members: list[WorkspaceMemberOutput]
|
members: list[WorkspaceMemberOutput]
|
||||||
@@ -119,6 +148,26 @@ class WorkspaceInviteOutput(pydantic.BaseModel):
|
|||||||
user: WorkspaceMemberUserOutput
|
user: WorkspaceMemberUserOutput
|
||||||
invited_by: WorkspaceMemberUserOutput
|
invited_by: WorkspaceMemberUserOutput
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_domain(cls, invite: domain.WorkspaceInvite) -> 'WorkspaceInviteOutput':
|
||||||
|
if invite.user is None or invite.invited_by is None:
|
||||||
|
raise ValueError('Workspace invite relations are not loaded')
|
||||||
|
|
||||||
|
return cls(
|
||||||
|
id=invite.id,
|
||||||
|
status=invite.status,
|
||||||
|
user=WorkspaceMemberUserOutput(
|
||||||
|
id=invite.user.id,
|
||||||
|
telegram_id=invite.user.telegram_id,
|
||||||
|
username=invite.user.username,
|
||||||
|
),
|
||||||
|
invited_by=WorkspaceMemberUserOutput(
|
||||||
|
id=invite.invited_by.id,
|
||||||
|
telegram_id=invite.invited_by.telegram_id,
|
||||||
|
username=invite.invited_by.username,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class GetWorkspaceInvitesOutput(pydantic.BaseModel):
|
class GetWorkspaceInvitesOutput(pydantic.BaseModel):
|
||||||
invites: list[WorkspaceInviteOutput]
|
invites: list[WorkspaceInviteOutput]
|
||||||
|
|||||||
@@ -14,10 +14,7 @@ async def create_creative(
|
|||||||
self: 'Usecase', input: dto.CreateCreativeInput, project_id: uuid.UUID, user_id: uuid.UUID, workspace_id: uuid.UUID
|
self: 'Usecase', input: dto.CreateCreativeInput, project_id: uuid.UUID, user_id: uuid.UUID, workspace_id: uuid.UUID
|
||||||
) -> dto.CreativeOutput:
|
) -> dto.CreativeOutput:
|
||||||
await self.ensure_workspace_permission(
|
await self.ensure_workspace_permission(
|
||||||
workspace_id,
|
workspace_id, user_id, domain.PermissionKey.PROJECTS_WRITE, for_project_id=project_id
|
||||||
user_id,
|
|
||||||
domain.PermissionKey.PROJECTS_WRITE,
|
|
||||||
for_project_id=project_id,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
project = await self.database.get_project(workspace_id, project_id=project_id)
|
project = await self.database.get_project(workspace_id, project_id=project_id)
|
||||||
@@ -25,9 +22,11 @@ async def create_creative(
|
|||||||
log.warning('User %s attempted to create creative for unavailable project %s', user_id, project_id)
|
log.warning('User %s attempted to create creative for unavailable project %s', user_id, project_id)
|
||||||
raise domain.ProjectNotFound(project_id)
|
raise domain.ProjectNotFound(project_id)
|
||||||
|
|
||||||
|
creative_text = domain.replace_invite_link_with_tag(input.text)
|
||||||
|
|
||||||
creative = domain.Creative(
|
creative = domain.Creative(
|
||||||
name=input.name,
|
name=input.name,
|
||||||
text=input.text,
|
text=creative_text,
|
||||||
status=domain.CreativeStatus.ACTIVE,
|
status=domain.CreativeStatus.ACTIVE,
|
||||||
project_id=project.id,
|
project_id=project.id,
|
||||||
workspace_id=workspace_id,
|
workspace_id=workspace_id,
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
import re
|
|
||||||
|
|
||||||
_LINK_WITH_LABEL_PATTERN: re.Pattern[str] = re.compile(r'\[(?P<label>[^\[\]]+?)\]\s*\{link\}', re.IGNORECASE)
|
|
||||||
_LINK_PLACEHOLDER_PATTERN: re.Pattern[str] = re.compile(r'\{link\}', re.IGNORECASE)
|
|
||||||
_LINK_CLASS = 'tg-link'
|
|
||||||
|
|
||||||
|
|
||||||
def _replace_label_placeholder(match: re.Match[str]) -> str:
|
|
||||||
label = match.group('label')
|
|
||||||
return f'<a class="{_LINK_CLASS}">{label}</a>'
|
|
||||||
|
|
||||||
|
|
||||||
def prepare_creative_text_html(source: str) -> str:
|
|
||||||
"""Normalize Telegram HTML and replace {link} placeholder with custom elements.
|
|
||||||
|
|
||||||
Supported patterns:
|
|
||||||
- ``{link}`` → ``<a class="tg-link"></a>``
|
|
||||||
- ``[Label]{link}`` → ``<a class="tg-link">Label</a>``
|
|
||||||
"""
|
|
||||||
|
|
||||||
with_labels = _LINK_WITH_LABEL_PATTERN.sub(_replace_label_placeholder, source)
|
|
||||||
return _LINK_PLACEHOLDER_PATTERN.sub(f'<a class="{_LINK_CLASS}"></a>', with_labels)
|
|
||||||
@@ -4,8 +4,6 @@ from typing import TYPE_CHECKING
|
|||||||
|
|
||||||
from src import domain
|
from src import domain
|
||||||
|
|
||||||
from .text_formatting import prepare_creative_text_html
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .. import Usecase
|
from .. import Usecase
|
||||||
|
|
||||||
@@ -32,8 +30,8 @@ async def tg_create_creative_4(
|
|||||||
await self.telegram_bot.send_message(
|
await self.telegram_bot.send_message(
|
||||||
'✅ Название сохранено!\n\n'
|
'✅ Название сохранено!\n\n'
|
||||||
'💬 Теперь отправьте или перешлите текст креатива.\n'
|
'💬 Теперь отправьте или перешлите текст креатива.\n'
|
||||||
'Используйте {link} для обозначения места вставки ссылки на ваш канал.\n'
|
'Пригласительная ссылка на ваш канал будет автоматически '
|
||||||
'Вы также можете указать текст ссылки в формате [текст]{link}.',
|
'заменена на уникальную ссылку для отслеживания.',
|
||||||
chat_id,
|
chat_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -85,7 +83,7 @@ async def tg_create_creative_4(
|
|||||||
return
|
return
|
||||||
|
|
||||||
message_html = text_html or text
|
message_html = text_html or text
|
||||||
creative_text = prepare_creative_text_html(message_html)
|
creative_text = domain.replace_invite_link_with_tag(message_html)
|
||||||
|
|
||||||
creative = domain.Creative(
|
creative = domain.Creative(
|
||||||
name=creative_name,
|
name=creative_name,
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ async def update_creative(
|
|||||||
if input.name:
|
if input.name:
|
||||||
creative.name = input.name
|
creative.name = input.name
|
||||||
if input.text:
|
if input.text:
|
||||||
creative.text = input.text
|
creative.text = domain.replace_invite_link_with_tag(input.text)
|
||||||
if input.status:
|
if input.status:
|
||||||
creative.status = input.status
|
creative.status = input.status
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ from typing import TYPE_CHECKING
|
|||||||
|
|
||||||
from src import domain, dto
|
from src import domain, dto
|
||||||
|
|
||||||
from .helpers import generate_post_url
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .. import Usecase
|
from .. import Usecase
|
||||||
|
|
||||||
@@ -23,11 +21,7 @@ async def get_placement(self: 'Usecase', input: dto.GetPlacementInput) -> dto.Pl
|
|||||||
|
|
||||||
context.ensure_project_permission(domain.PermissionKey.PLACEMENTS_READ, placement.project_id)
|
context.ensure_project_permission(domain.PermissionKey.PLACEMENTS_READ, placement.project_id)
|
||||||
|
|
||||||
# Compute post-related fields
|
ad_post_url = placement.post.url if placement.post else None
|
||||||
ad_post_url = None
|
|
||||||
|
|
||||||
if placement.post:
|
|
||||||
ad_post_url = generate_post_url(placement.post.channel.username, placement.post.message_id)
|
|
||||||
|
|
||||||
subscriptions_count = await self.database.count_subscriptions_by_placement(placement.id)
|
subscriptions_count = await self.database.count_subscriptions_by_placement(placement.id)
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ from typing import TYPE_CHECKING
|
|||||||
|
|
||||||
from src import domain, dto
|
from src import domain, dto
|
||||||
|
|
||||||
from .helpers import generate_post_url
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .. import Usecase
|
from .. import Usecase
|
||||||
|
|
||||||
@@ -33,10 +31,7 @@ async def get_placements(self: 'Usecase', input: dto.GetPlacementsInput) -> list
|
|||||||
|
|
||||||
placement_outputs = []
|
placement_outputs = []
|
||||||
for placement in placements:
|
for placement in placements:
|
||||||
ad_post_url = None
|
ad_post_url = placement.post.url if placement.post else None
|
||||||
|
|
||||||
if placement.post:
|
|
||||||
ad_post_url = generate_post_url(placement.post.channel.username, placement.post.message_id)
|
|
||||||
|
|
||||||
placement_outputs.append(
|
placement_outputs.append(
|
||||||
dto.PlacementOutput(
|
dto.PlacementOutput(
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
def generate_post_url(channel_username: str | None, message_id: int) -> str | None:
|
|
||||||
"""Generate Telegram post URL from channel username and message ID."""
|
|
||||||
if not channel_username:
|
|
||||||
return None
|
|
||||||
# Remove @ prefix if present
|
|
||||||
username = channel_username.lstrip('@')
|
|
||||||
return f'https://t.me/{username}/{message_id}'
|
|
||||||
@@ -4,8 +4,6 @@ from typing import TYPE_CHECKING
|
|||||||
|
|
||||||
from src import domain, dto
|
from src import domain, dto
|
||||||
|
|
||||||
from .helpers import generate_post_url
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .. import Usecase
|
from .. import Usecase
|
||||||
|
|
||||||
@@ -39,10 +37,7 @@ async def update_placement(
|
|||||||
|
|
||||||
await self.database.update_placement(placement)
|
await self.database.update_placement(placement)
|
||||||
|
|
||||||
ad_post_url = None
|
ad_post_url = placement.post.url if placement.post else None
|
||||||
|
|
||||||
if placement.post:
|
|
||||||
ad_post_url = generate_post_url(placement.post.channel.username, placement.post.message_id)
|
|
||||||
|
|
||||||
subscriptions_count = await self.database.count_subscriptions_by_placement(placement.id)
|
subscriptions_count = await self.database.count_subscriptions_by_placement(placement.id)
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ from typing import TYPE_CHECKING
|
|||||||
from aiogram.types import InlineKeyboardButton
|
from aiogram.types import InlineKeyboardButton
|
||||||
|
|
||||||
from src import domain, dto
|
from src import domain, dto
|
||||||
from src.usecase.workspace.mappers import workspace_invite_to_dto
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .. import Usecase
|
from .. import Usecase
|
||||||
@@ -73,4 +72,4 @@ async def create_workspace_invite(
|
|||||||
buttons=[[accept_button]],
|
buttons=[[accept_button]],
|
||||||
)
|
)
|
||||||
|
|
||||||
return workspace_invite_to_dto(invite)
|
return dto.WorkspaceInviteOutput.from_domain(invite)
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import uuid
|
|||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from src import domain, dto
|
from src import domain, dto
|
||||||
from src.usecase.workspace.mappers import workspace_invite_to_dto
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .. import Usecase
|
from .. import Usecase
|
||||||
@@ -17,4 +16,4 @@ async def get_workspace_invites(
|
|||||||
|
|
||||||
invites = await self.database.get_workspace_invites(workspace_id)
|
invites = await self.database.get_workspace_invites(workspace_id)
|
||||||
|
|
||||||
return [workspace_invite_to_dto(invite) for invite in invites]
|
return [dto.WorkspaceInviteOutput.from_domain(invite) for invite in invites]
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import uuid
|
|||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from src import domain, dto
|
from src import domain, dto
|
||||||
from src.usecase.workspace.mappers import workspace_member_to_dto
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .. import Usecase
|
from .. import Usecase
|
||||||
@@ -17,4 +16,4 @@ async def get_workspace_members(
|
|||||||
|
|
||||||
members = await self.database.get_workspace_members(workspace_id)
|
members = await self.database.get_workspace_members(workspace_id)
|
||||||
|
|
||||||
return [workspace_member_to_dto(member) for member in members]
|
return [dto.WorkspaceMemberOutput.from_domain(member) for member in members]
|
||||||
|
|||||||
@@ -1,57 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from src import domain, dto
|
|
||||||
|
|
||||||
|
|
||||||
def workspace_member_to_dto(member: domain.WorkspaceUser) -> dto.WorkspaceMemberOutput:
|
|
||||||
if member.user is None:
|
|
||||||
raise ValueError('Workspace member user relation is not loaded')
|
|
||||||
|
|
||||||
permissions_map: dict[domain.PermissionKey, list[dto.WorkspacePermissionScopeOutput]] = {}
|
|
||||||
|
|
||||||
for permission in getattr(member, 'permissions', []) or []:
|
|
||||||
permissions_map.setdefault(permission.permission, [])
|
|
||||||
|
|
||||||
for scope in getattr(member, 'permission_scopes', []) or []:
|
|
||||||
permissions_map.setdefault(scope.permission, []).append(
|
|
||||||
dto.WorkspacePermissionScopeOutput(
|
|
||||||
type=scope.scope_type,
|
|
||||||
id=scope.scope_id,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
permissions_output = [
|
|
||||||
dto.WorkspacePermissionOutput(key=key, scopes=scopes)
|
|
||||||
for key, scopes in sorted(permissions_map.items(), key=lambda item: item[0].value)
|
|
||||||
]
|
|
||||||
|
|
||||||
return dto.WorkspaceMemberOutput(
|
|
||||||
id=member.id,
|
|
||||||
status=member.status,
|
|
||||||
user=dto.WorkspaceMemberUserOutput(
|
|
||||||
id=member.user.id,
|
|
||||||
telegram_id=member.user.telegram_id,
|
|
||||||
username=member.user.username,
|
|
||||||
),
|
|
||||||
permissions=permissions_output,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def workspace_invite_to_dto(invite: domain.WorkspaceInvite) -> dto.WorkspaceInviteOutput:
|
|
||||||
if invite.user is None or invite.invited_by is None:
|
|
||||||
raise ValueError('Workspace invite relations are not loaded')
|
|
||||||
|
|
||||||
return dto.WorkspaceInviteOutput(
|
|
||||||
id=invite.id,
|
|
||||||
status=invite.status,
|
|
||||||
user=dto.WorkspaceMemberUserOutput(
|
|
||||||
id=invite.user.id,
|
|
||||||
telegram_id=invite.user.telegram_id,
|
|
||||||
username=invite.user.username,
|
|
||||||
),
|
|
||||||
invited_by=dto.WorkspaceMemberUserOutput(
|
|
||||||
id=invite.invited_by.id,
|
|
||||||
telegram_id=invite.invited_by.telegram_id,
|
|
||||||
username=invite.invited_by.username,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
@@ -4,7 +4,6 @@ import uuid
|
|||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from src import domain, dto
|
from src import domain, dto
|
||||||
from src.usecase.workspace.mappers import workspace_member_to_dto
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .. import Usecase
|
from .. import Usecase
|
||||||
@@ -43,4 +42,4 @@ async def update_workspace_member_permissions(
|
|||||||
if not updated_member:
|
if not updated_member:
|
||||||
raise domain.WorkspaceAccessDenied(workspace_id)
|
raise domain.WorkspaceAccessDenied(workspace_id)
|
||||||
|
|
||||||
return workspace_member_to_dto(updated_member)
|
return dto.WorkspaceMemberOutput.from_domain(updated_member)
|
||||||
|
|||||||
Reference in New Issue
Block a user