Skip to content

Commit

Permalink
Merge branch 'dev' into user-endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
reyniersbram committed Mar 13, 2024
2 parents d2972c0 + 54fb0c1 commit 24a3c91
Show file tree
Hide file tree
Showing 12 changed files with 329 additions and 6 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -96,4 +96,4 @@ jobs:
- name: run pyright
working-directory: backend
run: |
pyright src tests
pyright
3 changes: 2 additions & 1 deletion backend/alembic/env.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from src.user.models import Base as UserBase
from src.subject.models import Base as SubjectBase
from src.project.models import Base as ProjectBase
from src.group.models import Base as GroupBase
import os
import sys
from logging.config import fileConfig
Expand Down Expand Up @@ -33,7 +34,7 @@
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
combined_metadata = MetaData()
for base in [ProjectBase, SubjectBase, UserBase]:
for base in [ProjectBase, SubjectBase, UserBase, GroupBase]:
for table in base.metadata.tables.values():
combined_metadata._add_table(table.name, table.schema, table)

Expand Down
43 changes: 43 additions & 0 deletions backend/alembic/versions/0b71bf916b62_add_groups.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"""add groups
Revision ID: 0b71bf916b62
Revises: 5a1ed3366cce
Create Date: 2024-03-13 16:58:06.437972
"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision: str = '0b71bf916b62'
down_revision: Union[str, None] = '5a1ed3366cce'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('team',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('team_name', sa.String(), nullable=False),
sa.Column('score', sa.Integer(), nullable=False),
sa.Column('project_id', sa.Integer(), nullable=False),
sa.PrimaryKeyConstraint('id')
)
op.create_table('student_group',
sa.Column('uid', sa.String(), nullable=True),
sa.Column('team_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['team_id'], ['team.id'], ),
sa.ForeignKeyConstraint(['uid'], ['website_user.uid'], )
)
# ### end Alembic commands ###


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('student_group')
op.drop_table('team')
# ### end Alembic commands ###
33 changes: 33 additions & 0 deletions backend/alembic/versions/1d969eb58cf1_change_studentgroup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"""change studentgroup
Revision ID: 1d969eb58cf1
Revises: 0b71bf916b62
Create Date: 2024-03-13 21:17:13.264653
"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision: str = '1d969eb58cf1'
down_revision: Union[str, None] = '0b71bf916b62'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('student_group', sa.Column(
'project_id', sa.NullType(), nullable=True))
op.add_column('student_group', sa.Column('score', sa.NullType(), nullable=True))
# ### end Alembic commands ###


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('student_group', 'score')
op.drop_column('student_group', 'project_id')
# ### end Alembic commands ###
6 changes: 6 additions & 0 deletions backend/pyrightconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"include": [
"src",
"tests"
]
}
56 changes: 56 additions & 0 deletions backend/src/group/dependencies.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
from typing import Sequence

from fastapi import Depends
from sqlalchemy.ext.asyncio import AsyncSession
from src.dependencies import get_async_db
from src.group.schemas import Group
from src.user.dependencies import get_authenticated_user
from src.user.schemas import User

from ..auth.exceptions import NotAuthorized
from . import service
from .exceptions import AlreadyInGroup, GroupNotFound


async def retrieve_group(
group_id: int, db: AsyncSession = Depends(get_async_db)
) -> Group:
group = await service.get_group_by_id(db, group_id)
if not group:
raise GroupNotFound()
return group


async def retrieve_groups_by_user(
user: User, db: AsyncSession = Depends(get_async_db)
) -> Sequence[Group]:
return await service.get_groups_by_user(db, user.uid)


async def retrieve_groups_by_project(
project_id: int, db: AsyncSession = Depends(get_async_db)
) -> Sequence[Group]:
return await service.get_groups_by_project(db, project_id)


async def is_authorized_to_leave(
group_id: int,
user: User = Depends(get_authenticated_user),
db: AsyncSession = Depends(get_async_db),
):
groups = await service.get_groups_by_user(db, user.uid)
teachers = await service.get_teachers_by_group(db, group_id)
if not any(user.uid == teacher.uid for teacher in teachers):
if not any(group.id == group_id for group in groups):
raise NotAuthorized()


# TODO: take enroll_date into consideration
async def is_authorized_to_join(
group_id: int,
user: User = Depends(get_authenticated_user),
db: AsyncSession = Depends(get_async_db),
):
groups = await service.get_groups_by_user(db, user.uid)
if any(group.id == group_id for group in groups):
raise AlreadyInGroup()
13 changes: 13 additions & 0 deletions backend/src/group/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from fastapi import HTTPException


class GroupNotFound(HTTPException):
def __init__(self):
"""Raised when group is not found in database"""
super().__init__(status_code=404, detail="Group not found")


class AlreadyInGroup(HTTPException):
def __init__(self):
"""Raised when person is already in group"""
super().__init__(status_code=403, detail="Already in Group")
22 changes: 22 additions & 0 deletions backend/src/group/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from sqlalchemy import Column, ForeignKey, Table
from sqlalchemy.orm import Mapped, mapped_column
from src.database import Base

# TODO: set right primary keys
StudentGroup = Table(
"student_group",
Base.metadata,
Column("uid", ForeignKey("website_user.uid")),
Column("team_id", ForeignKey("team.id")),
)


class Group(Base):
__tablename__ = "team"

id: Mapped[int] = mapped_column(primary_key=True)
team_name: Mapped[str] = mapped_column(nullable=False)
score: Mapped[int] = mapped_column(nullable=False)
project_id: Mapped[int] = mapped_column(
ForeignKey("project.id", ondelete="CASCADE"), nullable=False
)
50 changes: 50 additions & 0 deletions backend/src/group/router.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from src.dependencies import get_async_db
from src.group.dependencies import (
is_authorized_to_join,
is_authorized_to_leave,
retrieve_group,
retrieve_groups_by_project,
)
from src.group.schemas import Group

from . import service

router = APIRouter(
prefix="/api/projects/{project_id}/groups",
tags=["groups"],
responses={404: {"description": "Not found"}},
)


@router.get("/")
async def get_groups(groups: list[Group] = Depends(retrieve_groups_by_project)):
return groups


@router.get("/{group_id}")
async def get_group(group: Group = Depends(retrieve_group)):
return group


@router.delete(
"/{group_id}", dependencies=[Depends(is_authorized_to_leave)], status_code=200
)
async def leave_group(
group_id: int, user_id: str, db: AsyncSession = Depends(get_async_db)
):
await service.leave_group(db, group_id, user_id)
return "Successfully deleted"


@router.post(
"/{group_id}",
dependencies=[Depends(is_authorized_to_join), Depends(retrieve_group)],
status_code=201,
)
async def join_group(
group_id: int, user_id: str, db: AsyncSession = Depends(get_async_db)
):
await service.join_group(db, group_id, user_id)
return "Successfully joined"
26 changes: 26 additions & 0 deletions backend/src/group/schemas.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from typing import List

from pydantic import BaseModel, Field

from src.user.schemas import User


class Groupbase(BaseModel):
project_id: int
score: int


class GroupCreate(Groupbase):
pass


class Group(Groupbase):
id: int
team_name: str = Field(min_length=1)


class GroupPreview(Group):
memberlist: List[User]

class Config:
from_attributes = True
70 changes: 70 additions & 0 deletions backend/src/group/service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
from typing import Sequence

from sqlalchemy import delete
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.future import select
from src.user.models import User

from src.project import models as projectModels
from src.subject import models as subjectModels
from . import schemas
from .models import Group, StudentGroup


async def get_group_by_id(db: AsyncSession, group_id: int) -> Group | None:
return (await db.execute(select(Group).filter_by(id=group_id))).scalar_one_or_none()


async def get_groups_by_project(db: AsyncSession, project_id: int) -> Sequence[Group]:
return (
(await db.execute(select(Group).filter_by(project_id=project_id)))
.scalars()
.all()
)


async def get_groups_by_user(db: AsyncSession, user_id: str) -> Sequence[Group]:
return (
(
await db.execute(
select(Group).join(StudentGroup, StudentGroup.c.uid == user_id)
)
)
.scalars()
.all()
)


async def get_teachers_by_group(db: AsyncSession, group_id: int) -> Sequence[User]:
return (
(
await db.execute(
select(User)
.join(subjectModels.TeacherSubject)
.join(subjectModels.Subject)
.join(projectModels.Project)
.join(Group, Group.id == group_id)
)
)
.scalars()
.all()
)


async def create_group(db: AsyncSession, group: schemas.GroupCreate) -> Group:
db_group = Group(**group.model_dump())
db.add(db_group)
await db.commit()
await db.refresh(db_group)
return db_group


async def join_group(db: AsyncSession, team_id: int, user_id: str):
insert_stmnt = StudentGroup.insert().values(team_id=team_id, uid=user_id)
await db.execute(insert_stmnt)
await db.commit()


async def leave_group(db: AsyncSession, team_id: int, user_id: str):
await db.execute(delete(StudentGroup).filter_by(team_id=team_id, uid=user_id))
await db.commit()
11 changes: 7 additions & 4 deletions backend/src/main.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from starlette.middleware.sessions import SessionMiddleware

from src import config
from src.auth.router import router as auth_router
from src.group.router import router as group_router
from src.project.router import router as project_router
from src.subject.router import router as subject_router
from src.user.router import router as user_router
from src.project.router import router as project_router
from src.auth.router import router as auth_router
from fastapi.middleware.cors import CORSMiddleware
from src import config

app = FastAPI()

Expand All @@ -24,6 +26,7 @@
app.include_router(user_router)
app.include_router(project_router)
app.include_router(auth_router)
app.include_router(group_router)


@app.get("/api")
Expand Down

0 comments on commit 24a3c91

Please sign in to comment.