-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'dev' into user-endpoint
- Loading branch information
Showing
12 changed files
with
329 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -96,4 +96,4 @@ jobs: | |
- name: run pyright | ||
working-directory: backend | ||
run: | | ||
pyright src tests | ||
pyright |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
33
backend/alembic/versions/1d969eb58cf1_change_studentgroup.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 ### |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
{ | ||
"include": [ | ||
"src", | ||
"tests" | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters