blob: 8993a7a35690635209f868b30e3d4516a3f38341 [file] [log] [blame]
import datetime
import logging
import typing
from ninja import ModelSchema, Schema
from pydantic import Field
from trackingserver_auth.models import Team, User
from trackingserver_auth.schema import TeamOut, UserOut
from trackingserver_projects.models import (
Project,
ProjectAttribute,
ProjectMembership,
ProjectTeamMembership,
ProjectUserMembership,
)
logger = logging.getLogger(__name__)
class VisibilityFull(Schema):
pass
class ProjectAttributeIn(ModelSchema):
class Config:
model = ProjectAttribute
model_fields = ["name", "type", "schema_version", "value"]
class ProjectAttributeOut(ModelSchema):
class Config:
model = ProjectAttribute
model_fields = ["name", "type", "schema_version", "value", "id", "project"]
class ProjectTeamMembershipOut(ModelSchema):
class Config:
model = ProjectTeamMembership
model_fields = "__all__"
class ProjectUserMembershipOut(ModelSchema):
class Config:
model = ProjectUserMembership
model_fields = "__all__"
# This is currently the schema we take from the UI
# Its suboptimal, as we should be doing the following:
# 1. Creating invites if someone does not exist
# 2. Looking up the IDs of the users/teams on the FE so we can just pass them in as...
# 3. Pass them in as memberships, not as the VisibilityIn object
# For now this is easy enough to not have to worry about this and the API is not public -- TODO -- implement!
class Visibility(Schema):
# Note that the str for user_ids_visible can be an email, in which case we'll do a lookup
# It will always save an ID
user_ids_visible: typing.List[typing.Union[int, str]]
team_ids_visible: typing.List[int]
team_ids_writable: typing.List[int]
user_ids_writable: typing.List[typing.Union[int, str]]
async def resolve_ids(self) -> "Visibility":
"""Resolves the user_ids_visible and user_ids_writable to IDs
@param self:
@param user:
@return:
"""
all_emails = [
item for item in self.user_ids_visible + self.user_ids_writable if isinstance(item, str)
]
email_map = {
user.email: user.id async for user in User.objects.filter(email__in=all_emails)
}
non_existing_emails = [email for email in all_emails if email not in email_map]
if len(non_existing_emails) > 0:
logger.warning(
f"Could not find users with emails {non_existing_emails}, proceeding with edit anyway..."
)
user_ids_visible_to_save = [
email_map[user_id] if user_id in email_map else user_id
for user_id in self.user_ids_visible
if user_id not in non_existing_emails
]
user_ids_writable_to_save = [
email_map[user_id] if user_id in email_map else user_id
for user_id in self.user_ids_writable
if user_id not in non_existing_emails
]
return Visibility(
user_ids_visible=user_ids_visible_to_save,
team_ids_visible=self.team_ids_visible,
user_ids_writable=user_ids_writable_to_save,
team_ids_writable=self.team_ids_writable,
)
def to_memberships(
self, project: Project
) -> typing.Tuple[typing.List[ProjectTeamMembership], typing.List[ProjectUserMembership]]:
"""Converts the visibility object to a list of memberships
@param project:
@param self:
@return:
"""
team_memberships = []
user_memberships = []
for team_writable in self.team_ids_writable:
team_memberships.append(
ProjectTeamMembership(project=project, team_id=team_writable, role="write")
)
for user_writable in self.user_ids_writable:
user_memberships.append(
ProjectUserMembership(project=project, user_id=user_writable, role="write")
)
for team_visible in self.team_ids_visible:
team_memberships.append(
ProjectTeamMembership(project=project, team_id=team_visible, role="read")
)
for user_visible in self.user_ids_visible:
user_memberships.append(
ProjectUserMembership(project=project, user_id=user_visible, role="read")
)
return team_memberships, user_memberships
@staticmethod
def from_memberships(memberships: typing.List[ProjectMembership]) -> "Visibility":
"""Gets the visibility object from a list of memberships
@param memberships:
@return:
"""
visibility = Visibility(
user_ids_visible=[], team_ids_visible=[], team_ids_writable=[], user_ids_writable=[]
)
for membership in memberships:
# TODO -- implement
if isinstance(membership, ProjectTeamMembership):
if membership.role == "write":
visibility.team_ids_writable.append(membership.team_id)
elif membership.role == "read":
visibility.team_ids_visible.append(membership.team_id)
elif isinstance(membership, ProjectUserMembership):
if membership.role == "write":
visibility.user_ids_writable.append(membership.user_id)
elif membership.role == "read":
visibility.user_ids_visible.append(membership.user_id)
return visibility
class VisibilityOut(Schema):
users_visible: typing.List[UserOut]
users_writable: typing.List[UserOut]
teams_visible: typing.List[TeamOut]
teams_writable: typing.List[TeamOut]
@staticmethod
async def from_visibility(visibility: Visibility):
"""Creates a visibility out from a visibility
@param visibility:
@return:
"""
user_ids = visibility.user_ids_visible + visibility.user_ids_writable
team_ids = visibility.team_ids_visible + visibility.team_ids_writable
users = [UserOut.from_orm(user) async for user in User.objects.filter(id__in=user_ids)]
teams = [TeamOut.from_orm(team) async for team in Team.objects.filter(id__in=team_ids)]
return VisibilityOut(
users_visible=[user for user in users if user.id in visibility.user_ids_visible],
users_writable=[user for user in users if user.id in visibility.user_ids_writable],
teams_visible=[team for team in teams if team.id in visibility.team_ids_visible],
teams_writable=[team for team in teams if team.id in visibility.team_ids_writable],
)
class ProjectUpdate(Schema):
name: typing.Optional[str] = Field(description="The name of the project", default=None)
description: typing.Optional[str] = Field(
description="Description of the project", default=None
)
tags: typing.Optional[typing.Dict[str, str]] = Field(
description="Tags for the project", default=None
)
visibility: typing.Optional[Visibility] = Field(
description="Visibility of the project", default=None
)
class ProjectBase(Schema):
name: str = Field(description="The name of the project")
description: str = Field(description="Description of the project")
tags: typing.Dict[str, str] = Field(description="Tags for the project")
class ProjectIn(ProjectBase):
visibility: Visibility = Field(description="Visibility of the project")
attributes: typing.List[ProjectAttributeIn] = Field(
description="Attributes for the project", default=[]
)
class ProjectOut(ProjectBase):
id: int
role: str = Field(description="Role of the user in the project", default=None)
visibility: VisibilityOut = Field(description="Resolved visibility of the project")
created_at: datetime.datetime = Field(description="When the project was created")
updated_at: datetime.datetime = Field(description="When the project was last updated")
class Config:
model = Project
model_fields = "__all__"
@staticmethod
async def from_model(project: Project, role: str) -> "ProjectOut":
"""Creates a project out from a model
@param project:
@return:
"""
# TODO -- ensure that this isn't too slow...
visibility = Visibility.from_memberships(
[
membership
async for membership in ProjectTeamMembership.objects.filter(project=project)
]
+ [
membership
async for membership in ProjectUserMembership.objects.filter(project=project)
]
)
return ProjectOut(
name=project.name,
description=project.description,
tags=project.tags,
visibility=await VisibilityOut.from_visibility(visibility),
id=project.id,
role=role,
created_at=project.created_at,
updated_at=project.updated_at,
)
@staticmethod
async def from_models(
projects: typing.List[Project], user: User, teams: typing.List[Team]
) -> typing.List["ProjectOut"]:
"""
@return:
"""
# We could easily prefetch related upstream but this is fine for now...
all_team_memberships = [
item
async for item in ProjectTeamMembership.objects.filter(project__in=projects)
.all()
.prefetch_related("team")
]
all_user_memberships = [
item
async for item in ProjectUserMembership.objects.filter(project__in=projects)
.all()
.prefetch_related("user")
]
out = []
team_ids = [team.id for team in teams]
for project in projects:
relevant_team_memberships = [
item for item in all_team_memberships if item.project_id == project.id
]
relevant_user_memberships = [
item for item in all_user_memberships if item.project_id == project.id
]
team_can_write = (
len(
[
item
for item in relevant_team_memberships
if item.team_id in team_ids and item.role == "write"
]
)
> 0
)
user_can_write = (
len(
[
item
for item in relevant_user_memberships
if item.user_id == user.id and item.role == "write"
]
)
> 0
)
role = "write" if team_can_write or user_can_write else "read"
out.append(
ProjectOut(
id=project.id,
name=project.name,
description=project.description,
tags=project.tags,
visibility=VisibilityOut(
users_visible=[
UserOut.from_orm(membership.user)
for membership in relevant_user_memberships
if membership.role == "read"
],
users_writable=[
UserOut.from_orm(membership.user)
for membership in relevant_user_memberships
if membership.role == "write"
],
teams_visible=[
TeamOut.from_orm(membership.team)
for membership in relevant_team_memberships
if membership.role == "read"
],
teams_writable=[
TeamOut.from_orm(membership.team)
for membership in relevant_team_memberships
if membership.role == "write"
],
),
created_at=project.created_at,
updated_at=project.updated_at,
role=role,
)
)
return out
class ProjectOutWithAttributes(ProjectOut):
attributes: typing.List[ProjectAttributeOut]