Skip to content

Commit

Permalink
Fix problems with project
Browse files Browse the repository at this point in the history
  • Loading branch information
xerbalind committed Mar 12, 2024
1 parent 9137de1 commit 3c5e3be
Show file tree
Hide file tree
Showing 11 changed files with 122 additions and 104 deletions.
68 changes: 68 additions & 0 deletions backend/alembic/versions/ecbdc859aca6_update_project_scheme.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
"""update project scheme
Revision ID: ecbdc859aca6
Revises: 29a17fa183de
Create Date: 2024-03-12 16:43:51.794097
"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision: str = 'ecbdc859aca6'
down_revision: Union[str, None] = '29a17fa183de'
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.alter_column('project', 'id',
existing_type=sa.BIGINT(),
type_=sa.Integer(),
existing_nullable=False,
autoincrement=True)
op.alter_column('project', 'deadline',
existing_type=sa.DATE(),
type_=sa.DateTime(),
existing_nullable=False)
op.alter_column('project', 'subject_id',
existing_type=sa.BIGINT(),
type_=sa.Integer(),
existing_nullable=True)
op.alter_column('project', 'enroll_deadline',
existing_type=sa.DATE(),
type_=sa.DateTime(),
existing_nullable=True)
op.drop_constraint('project_subject_id_fkey', 'project', type_='foreignkey')
op.create_foreign_key(None, 'project', 'subject', [
'subject_id'], ['id'], ondelete='CASCADE')
# ### end Alembic commands ###


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(None, 'project', type_='foreignkey') #type: ignore
op.create_foreign_key('project_subject_id_fkey', 'project', 'subject', [
'subject_id'], ['id'], ondelete='SET NULL')
op.alter_column('project', 'enroll_deadline',
existing_type=sa.DateTime(),
type_=sa.DATE(),
existing_nullable=True)
op.alter_column('project', 'subject_id',
existing_type=sa.Integer(),
type_=sa.BIGINT(),
existing_nullable=True)
op.alter_column('project', 'deadline',
existing_type=sa.DateTime(),
type_=sa.DATE(),
existing_nullable=False)
op.alter_column('project', 'id',
existing_type=sa.Integer(),
type_=sa.BIGINT(),
existing_nullable=False,
autoincrement=True)
# ### end Alembic commands ###
5 changes: 5 additions & 0 deletions backend/src/database.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
from sqlalchemy import MetaData, create_engine
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
from sqlalchemy.orm import declarative_base
from sqlalchemy.orm import sessionmaker
from src import config

SQLALCHEMY_DATABASE_URL = config.CONFIG.database_uri

# TODO: migrate full codebase to async
engine = create_engine(SQLALCHEMY_DATABASE_URL)
async_engine = create_async_engine(SQLALCHEMY_DATABASE_URL[:len(
"postgresql")] + "+asyncpg" + SQLALCHEMY_DATABASE_URL[len("postgresql"):])

SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
AsyncSessionLocal = async_sessionmaker(async_engine, autoflush=False)

Base = declarative_base()
11 changes: 10 additions & 1 deletion backend/src/dependencies.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from .database import SessionLocal
from .database import SessionLocal, AsyncSessionLocal


def get_db():
Expand All @@ -8,3 +8,12 @@ def get_db():
yield db
finally:
db.close()


async def get_async_db():
"""Creates new async database session per request, which is closed afterwards"""
db = AsyncSessionLocal()
try:
yield db
finally:
await db.close()
1 change: 0 additions & 1 deletion backend/src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
app.include_router(user_router)
app.include_router(project_router)
app.include_router(auth_router)
app.include_router(project_router)


@app.get("/api")
Expand Down
12 changes: 1 addition & 11 deletions backend/src/project/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,9 @@
from src.dependencies import get_db
from src.user.dependencies import get_authenticated_user
from src.user.schemas import User
from src.subject.schemas import Subject
from src.subject.schemas import SubjectList


from ..auth.exceptions import NotAuthorized
from ..subject.service import get_subjects, get_teachers


async def retrieve_subjects(
user: User = Depends(get_authenticated_user), db: Session = Depends(get_db)
) -> SubjectList:
teacher_subjects, student_subjects = await get_subjects(db, user.uid)
return SubjectList(as_teacher=teacher_subjects, as_student=student_subjects)
from ..subject.service import get_teachers


async def user_permission_validation(
Expand Down
18 changes: 0 additions & 18 deletions backend/src/project/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,5 @@
# exceptions.py

from fastapi import HTTPException


def NoProjectsFoundException(subject_id: int):
return HTTPException(status_code=404, detail=f"No projects found for subject {subject_id}")


def UnauthorizedToCreateProjectException():
return HTTPException(status_code=403, detail="User is not authorized to create projects for this subject")


def ProjectNotFoundException():
return HTTPException(status_code=404, detail="Project not found")


def UnauthorizedToDeleteProjectException():
return HTTPException(status_code=403, detail="User is not authorized to delete this project")


def UnauthorizedToUpdateProjectException():
return HTTPException(status_code=403, detail="User is not authorized to update projects for this subject")
5 changes: 3 additions & 2 deletions backend/src/project/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,14 @@ class Project(Base):
__tablename__ = 'project'

id: Mapped[int] = mapped_column(primary_key=True)
deadline: Mapped[datetime] = mapped_column(nullable=False)
deadline: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
name: Mapped[str] = mapped_column(nullable=False)
subject_id: Mapped[int] = mapped_column(ForeignKey(
'subject.id', ondelete="CASCADE"), nullable=True)
description: Mapped[str] = mapped_column(nullable=True)

enroll_deadline: Mapped[datetime] = mapped_column(nullable=True)
enroll_deadline: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=True)

__table_args__ = (
CheckConstraint('deadline >= CURRENT_DATE', name='deadline_check'),
Expand Down
58 changes: 19 additions & 39 deletions backend/src/project/router.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
from fastapi import APIRouter, Depends, HTTPException
from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from src.user.models import User
from src.dependencies import get_db
from src.user.dependencies import get_authenticated_user
from src.subject.dependencies import user_permission_validation
from src.dependencies import get_async_db, get_db

from .exceptions import UnauthorizedToCreateProjectException, ProjectNotFoundException, \
UnauthorizedToUpdateProjectException, NoProjectsFoundException
from .exceptions import ProjectNotFoundException
from .schemas import ProjectCreate, ProjectResponse, ProjectUpdate
from .service import create_project, get_project, delete_project, update_project, get_projects_for_subject
from ..subject.service import is_teacher_of_subject
from . import exceptions

router = APIRouter(
prefix="/api/subjects/{subject_id}/projects",
Expand All @@ -21,67 +17,51 @@
@router.get("/", response_model=list[ProjectResponse])
async def list_projects_for_subject(
subject_id: int,
user: User = Depends(get_authenticated_user),
db: AsyncSession = Depends(get_db)
db: AsyncSession = Depends(get_async_db)
):
projects = await get_projects_for_subject(db, subject_id)
if not projects:
raise NoProjectsFoundException(subject_id)
return projects


@router.post("/", response_model=ProjectResponse)
@router.post("/",
response_model=ProjectResponse,
dependencies=[Depends(user_permission_validation)],
status_code=201)
async def create_project_for_subject(
subject_id: int,
project_in: ProjectCreate,
user: User = Depends(get_authenticated_user),
db: AsyncSession = Depends(get_db)
db: AsyncSession = Depends(get_async_db)
):
if not await is_teacher_of_subject(db, user.id, subject_id):
raise UnauthorizedToCreateProjectException()

project = await create_project(db=db, project_in=project_in, user_id=user.id)
project = await create_project(db, project_in, subject_id)
return project


@router.get("/{project_id}", response_model=ProjectResponse)
async def get_project_for_subject(
project_id: int,
db: AsyncSession = Depends(get_db)
db: AsyncSession = Depends(get_async_db)
):
project = await get_project(db, project_id)
if not project:
raise ProjectNotFoundException()
return project


@router.delete("/{project_id}")
@router.delete("/{project_id}", dependencies=[Depends(user_permission_validation)])
async def delete_project_for_subject(
subject_id: int,
project_id: int,
user: User = Depends(get_authenticated_user),
db: AsyncSession = Depends(get_db)
db: AsyncSession = Depends(get_async_db)
):
if not await is_teacher_of_subject(db, user.id, subject_id):
raise UnauthorizedToUpdateProjectException()

await delete_project(db, project_id)
return {"message": "Project deleted successfully"}


@router.patch("/{project_id}", response_model=ProjectResponse)
@router.patch("/{project_id}",
response_model=ProjectResponse,
dependencies=[Depends(user_permission_validation)])
async def patch_project_for_subject(
subject_id: int,
project_id: int,
project_update: ProjectUpdate,
user: User = Depends(get_authenticated_user),
db: AsyncSession = Depends(get_db)
db: AsyncSession = Depends(get_async_db)
):
# Check if the user is authorized to update the project
if not await is_teacher_of_subject(db, user.id, subject_id):
raise UnauthorizedToUpdateProjectException()

updated_project = await update_project(db, project_id, project_update)
if not updated_project:
raise ProjectNotFoundException()
return updated_project
return await update_project(db, project_id, project_update)
24 changes: 10 additions & 14 deletions backend/src/project/schemas.py
Original file line number Diff line number Diff line change
@@ -1,42 +1,38 @@
from datetime import datetime, date
from datetime import datetime, date, timezone
from typing import Optional

from pydantic import BaseModel, Field, validator
from pydantic import BaseModel, Field, validator, ConfigDict, field_validator


class ProjectCreate(BaseModel):
name: str = Field(..., min_length=1)
deadline: datetime
subject_id: int
description: str

# Check if deadline is not in the past
@validator('deadline', pre=True, always=True)
def validate_deadline(cls, value):
if value < date.today():
@field_validator('deadline')
def validate_deadline(cls, value: datetime) -> datetime:
if value < datetime.now(value.tzinfo):
raise ValueError('The deadline cannot be in the past')
return value


class ProjectResponse(BaseModel):
model_config = ConfigDict(from_attributes=True)

id: int
name: str
deadline: datetime
subject_id: int
description: str

class Config:
orm_mode = True


class ProjectUpdate(BaseModel):
name: Optional[str] = Field(None, min_length=1)
deadline: Optional[datetime] = None
subject_id: Optional[int] = None
description: Optional[str] = None

@validator('deadline', pre=True, always=True)
def validate_deadline(cls, value):
if value is not None and value < datetime.now():
@field_validator('deadline')
def validate_deadline(cls, value: datetime) -> datetime:
if value is not None and value < datetime.now(value.tzinfo):
raise ValueError('The deadline cannot be in the past')
return value
16 changes: 6 additions & 10 deletions backend/src/project/service.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import List
from typing import List, Sequence
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.future import select
from . import models
Expand All @@ -7,11 +7,11 @@
from .schemas import ProjectCreate, ProjectUpdate


async def create_project(db: AsyncSession, project_in: ProjectCreate, user_id: str) -> Project:
async def create_project(db: AsyncSession, project_in: ProjectCreate, subject_id: int) -> Project:
new_project = Project(
name=project_in.name,
deadline=project_in.deadline,
subject_id=project_in.subject_id,
subject_id=subject_id,
description=project_in.description
)
db.add(new_project)
Expand All @@ -25,10 +25,10 @@ async def get_project(db: AsyncSession, project_id: int) -> models.Project:
return result.scalars().first()


async def get_projects_for_subject(db: AsyncSession, subject_id: int) -> List[models.Project]:
result = await db.execute(select(models.Project).filter(models.Project.subject_id == subject_id))
async def get_projects_for_subject(db: AsyncSession, subject_id: int) -> Sequence[models.Project]:
result = await db.execute(select(models.Project).filter_by(subject_id=subject_id))
projects = result.scalars().all()
return list(projects) # Explicitly convert to list
return projects


async def delete_project(db: AsyncSession, project_id: int):
Expand All @@ -43,16 +43,12 @@ async def update_project(db: AsyncSession, project_id: int, project_update: Proj
result = await db.execute(select(Project).filter(Project.id == project_id))
project = result.scalars().first()
if not project:
# Handle the case where the project doesn't exist
raise ProjectNotFoundException()

# Update fields only if they're provided in the update payload
if project_update.name is not None:
project.name = project_update.name
if project_update.deadline is not None:
project.deadline = project_update.deadline
if project_update.subject_id is not None:
project.subject_id = project_update.subject_id
if project_update.description is not None:
project.description = project_update.description

Expand Down
8 changes: 0 additions & 8 deletions backend/src/subject/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,3 @@ async def delete_subject(db: Session, subject_id: int):
"""Remove a subject"""
db.query(models.Subject).filter_by(id=subject_id).delete()
db.commit()


async def is_teacher_of_subject(db: AsyncSession, user_id: str, subject_id: int) -> bool:
"""Check if a user is a teacher of the subject."""
query = select(models.TeacherSubject).filter_by(
uid=user_id, subject_id=subject_id)
result = await db.execute(query)
return result.scalars().first() is not None

0 comments on commit 3c5e3be

Please sign in to comment.