Skip to content

Commit

Permalink
✨ Refactor team slug services (#74)
Browse files Browse the repository at this point in the history
  • Loading branch information
estebanx64 authored May 28, 2024
1 parent 4b34d63 commit c7b62e5
Show file tree
Hide file tree
Showing 8 changed files with 323 additions and 95 deletions.
25 changes: 25 additions & 0 deletions backend/app/alembic/versions/a17996bd5bff_merge_heads.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"""merge heads
Revision ID: a17996bd5bff
Revises: c5cc3b0f01d6, f686be54aec9
Create Date: 2024-05-27 22:37:55.225109
"""
from alembic import op
import sqlalchemy as sa
import sqlmodel.sql.sqltypes


# revision identifiers, used by Alembic.
revision = 'a17996bd5bff'
down_revision = ('c5cc3b0f01d6', 'f686be54aec9')
branch_labels = None
depends_on = None


def upgrade():
pass


def downgrade():
pass
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"""Add slug column with unique and index constraint in Team table
Revision ID: f686be54aec9
Revises: a9b76125b71a
Create Date: 2024-05-25 00:20:58.980226
"""
from alembic import op
import sqlalchemy as sa
import sqlmodel.sql.sqltypes


# revision identifiers, used by Alembic.
revision = 'f686be54aec9'
down_revision = 'a9b76125b71a'
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('invitation', 'email',
existing_type=sa.VARCHAR(),
nullable=False)
op.add_column('team', sa.Column('slug', sqlmodel.sql.sqltypes.AutoString(), nullable=False))
op.create_index(op.f('ix_team_slug'), 'team', ['slug'], unique=True)
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f('ix_team_slug'), table_name='team')
op.drop_column('team', 'slug')
op.alter_column('invitation', 'email',
existing_type=sa.VARCHAR(),
nullable=True)
# ### end Alembic commands ###
102 changes: 50 additions & 52 deletions backend/app/api/routes/teams.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from typing import Any

from fastapi import APIRouter, HTTPException
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import selectinload
from sqlmodel import col, func, select

from app.api.deps import CurrentUser, SessionDep
from app.api.deps import CurrentUser, SessionDep, get_current_user
from app.api.utils.teams import verify_and_generate_slug_name
from app.models import (
Message,
Role,
Expand Down Expand Up @@ -46,16 +47,16 @@ def read_teams(
return TeamsPublic(data=teams, count=count)


@router.get("/{team_id}", response_model=TeamWithUserPublic)
def read_team(session: SessionDep, current_user: CurrentUser, team_id: int) -> Any:
@router.get("/{team_slug}", response_model=TeamWithUserPublic)
def read_team(session: SessionDep, current_user: CurrentUser, team_slug: str) -> Any:
"""
Retrieve an team by its ID and returns it along with its associated users.
Retrieve a team by its name and returns it along with its associated users.
"""
query = select(Team).options(
selectinload(Team.user_links).selectinload(UserTeamLink.user) # type: ignore
)
query = query.where(
Team.id == team_id,
Team.slug == team_slug,
col(Team.user_links).any(col(UserTeamLink.user) == current_user),
)
team = session.exec(query).first()
Expand All @@ -74,31 +75,30 @@ def create_team(
"""
Create a new team with the provided details.
"""
team = Team.model_validate(team_in)
team_slug = verify_and_generate_slug_name(team_in.name, session)

team = Team.model_validate(team_in, update={"slug": team_slug})
user_team = UserTeamLink(user=current_user, team=team, role=Role.admin)
session.add(user_team)
session.commit()
session.refresh(team)
return team


@router.put("/{team_id}", response_model=TeamPublic)
@router.put("/{team_slug}", response_model=TeamPublic)
def update_team(
session: SessionDep,
current_user: CurrentUser,
team_id: int,
team_slug: str,
team_in: TeamUpdate,
) -> Any:
"""
Update an team by its ID.
Update an team by its name.
"""
query = (
select(UserTeamLink)
.options(selectinload(UserTeamLink.team)) # type: ignore
.where(
UserTeamLink.team_id == team_id,
UserTeamLink.user == current_user,
)
.join(Team, col(Team.id) == UserTeamLink.team_id)
.where(Team.slug == team_slug, UserTeamLink.user_id == current_user.id)
)
link = session.exec(query).first()
if not link:
Expand All @@ -111,28 +111,25 @@ def update_team(
status_code=400, detail="Not enough permissions to execute this action"
)
update_dict = team_in.model_dump(exclude_unset=True)
org = link.team
org.sqlmodel_update(update_dict)
session.add(org)
team = link.team
team.sqlmodel_update(update_dict)
session.add(team)
session.commit()
session.refresh(org)
return org
session.refresh(team)
return team


@router.delete("/{team_id}", response_model=Message)
@router.delete("/{team_slug}", response_model=Message)
def delete_team(
session: SessionDep, current_user: CurrentUser, team_id: int
session: SessionDep, current_user: CurrentUser, team_slug: str
) -> Message:
"""
Delete an team from the database.
Delete a team from the database by its name.
"""
query = (
select(UserTeamLink)
.options(selectinload(UserTeamLink.team)) # type: ignore
.where(
UserTeamLink.team_id == team_id,
UserTeamLink.user == current_user,
)
.join(Team, col(Team.id) == UserTeamLink.team_id)
.where(Team.slug == team_slug, UserTeamLink.user_id == current_user.id)
)
link = session.exec(query).first()
if not link:
Expand All @@ -153,28 +150,21 @@ def delete_team(
return Message(message="Team deleted")


@router.put("/{team_id}/users/{user_id}", response_model=UserTeamLinkPublic)
@router.put("/{team_slug}/users/{user_id}", response_model=UserTeamLinkPublic)
def update_member_in_team(
session: SessionDep,
current_user: CurrentUser,
team_id: int,
team_slug: str,
user_id: int,
member_in: TeamUpdateMember,
) -> Any:
"""
Update a member in an team.
Update a member in a team.
"""
query = (
select(UserTeamLink)
.options(
selectinload(UserTeamLink.team).selectinload( # type: ignore
Team.user_links # type: ignore
)
)
.where(
UserTeamLink.team_id == team_id,
UserTeamLink.user == current_user,
)
.join(Team, col(Team.id) == UserTeamLink.team_id)
.where(Team.slug == team_slug, UserTeamLink.user_id == current_user.id)
)
link = session.exec(query).first()
if not link:
Expand Down Expand Up @@ -203,15 +193,15 @@ def update_member_in_team(
return member_link


@router.delete("/{team_id}/users/{user_id}", response_model=Message)
@router.delete("/{team_slug}/users/{user_id}", response_model=Message)
def remove_member_from_team(
session: SessionDep,
current_user: CurrentUser,
team_id: int,
team_slug: str,
user_id: int,
) -> Message:
"""
Remove a member from an team.
Remove a member from a team.
"""
if current_user.id == user_id:
raise HTTPException(
Expand All @@ -220,15 +210,8 @@ def remove_member_from_team(

query = (
select(UserTeamLink)
.options(
selectinload(UserTeamLink.team).selectinload( # type: ignore
Team.user_links # type: ignore
)
)
.where(
UserTeamLink.team_id == team_id,
UserTeamLink.user == current_user,
)
.join(Team, col(Team.id) == UserTeamLink.team_id)
.where(Team.slug == team_slug, UserTeamLink.user_id == current_user.id)
)
link = session.exec(query).first()
if not link:
Expand All @@ -252,3 +235,18 @@ def remove_member_from_team(
session.delete(member_link)
session.commit()
return Message(message="User removed from team")


@router.get(
"/validate-team-name/{team_slug}",
response_model=Message,
dependencies=[Depends(get_current_user)],
)
def validate_team_name(session: SessionDep, team_slug: str) -> Any:
"""
Validate if team name is unique
"""
team = session.exec(select(Team).where(Team.slug == team_slug)).first()
if team:
raise HTTPException(status_code=400, detail="Team name already in use")
return Message(message="Team name is valid")
14 changes: 14 additions & 0 deletions backend/app/api/utils/teams.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import secrets

from sqlmodel import Session, select

from app.models import Team
from app.utils import slugify


def verify_and_generate_slug_name(name: str, session: Session) -> str:
slug_name = slugify(name)
while session.exec(select(Team).where(Team.slug == slug_name)).first():
slug_name = f"{slug_name}-{secrets.token_hex(4)}"

return slug_name
2 changes: 2 additions & 0 deletions backend/app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ class TeamBase(SQLModel):

class Team(TeamBase, table=True):
id: int | None = Field(default=None, primary_key=True)
slug: str = Field(unique=True, index=True)

user_links: list[UserTeamLink] = Relationship(back_populates="team")
invitations: list["Invitation"] = Relationship(back_populates="team")
Expand All @@ -139,6 +140,7 @@ class TeamUpdate(SQLModel):

class TeamPublic(TeamBase):
id: int
slug: str


class TeamsPublic(SQLModel):
Expand Down
Loading

0 comments on commit c7b62e5

Please sign in to comment.