Skip to content

Commit

Permalink
Merge branch 'dev' into permission-middleware
Browse files Browse the repository at this point in the history
  • Loading branch information
reyniersbram committed May 17, 2024
2 parents f876ce1 + a813608 commit f65f4f4
Show file tree
Hide file tree
Showing 115 changed files with 3,361 additions and 888 deletions.
44 changes: 28 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,26 +1,38 @@
# UGent-5
# Apollo

## Rolverdeling
Apollo is an online submission platform where instructors can flexibly set requirements for
student submissions. These requirements can range from simple checks on the submitted
file structure to test scripts that run when a submission is made.

| Rol | Verantwoordelijke |
| ------------- | ------------- |
| Groepsleider | Marieke Sinnaeve |
| Technische lead | Bram Reyniers |
| Systeembeheerder | Xander Bil |
| Customer Relations Officer | Pieter Janin |
| Frontendbeheerder | Mattis Cauwel |
| Backendbeheerder | Dries Huybens |
| Documentatiebeheerder | Pieter Janin |
| Testbeheerder | Michaël Boelaert |
Students quickly receive feedback on their submission, allowing them to know if it meets
the project requirements.

This repository hosts the web application's source code. To use Apollo, visit https://sel2-5.ugent.be.

## Wiki

Informatie over de gebruikte technologieën, de gebruikershandleiding en meer kan je vinden in de [wiki](https://github.com/SELab-2/UGent-5/wiki).
Documentation, including a user manual for teachers, can be found in the
[Apollo wiki](https://github.com/SELab-2/UGent-5/wiki).

## For Developers

## Setup ontwikkelomgeving
Instructions for setting up the frontend development environment can be found
[here](frontend/README.md).

De instructies voor het opzetten van de ontwikkelomgeving van de frontend kan je [hier](frontend/README.md) vinden. De instructies voor de backend staan [hier](backend/REAMDE.md).
Instructions for the backend are located [here](backend/README.md).

## API

Geautomatiseerde clients kunnen interageren met de webapplicatie via de [API](https://sel2-5.ugent.be/api/docs).
Automated clients can interact with the web application via the [API](https://sel2-5.ugent.be/api/docs).

## The team

| | |
|------------------|---------------------------------------------------|
| Xander Bil | System Administrator |
| Michaël Boelaert | Test Manager |
| Mattis Cauwel | Frontend Manager |
| Dries Huybens | Backend Manager |
| Pieter Janin | Customer Relations Officer, Documentation Manager |
| Bram Reyniers | Technical Lead |
| Marieke Sinnaeve | Team Lead |
2 changes: 2 additions & 0 deletions backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ DATABASE_URI="postgresql://username:password@localhost:5432/dbname"
alembic upgrade head
```

You can find more info about alembic [here](alembic/README.md).

#### Managing the database
```sh
# Stop the database container
Expand Down
1 change: 0 additions & 1 deletion backend/alembic/README

This file was deleted.

43 changes: 43 additions & 0 deletions backend/alembic/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Alembic

From the docs:

> [Alembic](https://alembic.sqlalchemy.org/en/latest/) is a lightweight database
migration tool for usage with the [SQLAlchemy](https://www.sqlalchemy.org/)
Database Toolkit for Python.

It allows us to generate database schemas from Python SQLAlchemy code, found in each
`models.py` file.

## Usage

Here are some of the most commonly used commands you might need.

#### Automatically generate a revision script after modifying database models in Python:

```sh
alembic revision --autogenerate -m "my_revision_name"
```

Make sure to review the generated script in `alembic/versions`
and make adjustments if needed.

#### Run a migration: this will upgrade the database schema to the most recent revision.

```sh
alembic upgrade head
```

#### Undo the most recent revision:

```sh
alembic downgrade -1
```

#### Reset the database to its initial (empty) state:

```sh
alembic downgrade base
```

For more examples, see the [official Alembic tutorial](https://alembic.sqlalchemy.org/en/latest/tutorial.html).
35 changes: 35 additions & 0 deletions backend/alembic/versions/18fb90307213_project_requirements_fix.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"""project_requirements_fix: fixes bug where project requirements would remain in the database after the parent
project would be deleted.
Revision ID: 18fb90307213
Revises: e0c97995e669
Create Date: 2024-05-04 15:42:43.114843
"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision: str = '18fb90307213'
down_revision: Union[str, None] = 'e0c97995e669'
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('requirement', 'project_id',
existing_type=sa.INTEGER(),
nullable=False)
# ### end Alembic commands ###


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('requirement', 'project_id',
existing_type=sa.INTEGER(),
nullable=True)
# ### end Alembic commands ###
31 changes: 31 additions & 0 deletions backend/alembic/versions/566f33fb161f_add_user_surname.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""add user surname
Revision ID: 566f33fb161f
Revises: 18fb90307213
Create Date: 2024-05-06 15:32:33.617263
"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision: str = '566f33fb161f'
down_revision: Union[str, None] = '18fb90307213'
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('website_user', sa.Column('surname', sa.String(),
nullable=False, server_default='SURNAME_DEFAULT'))
# ### end Alembic commands ###


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('website_user', 'surname')
# ### end Alembic commands ###
6 changes: 5 additions & 1 deletion backend/src/auth/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,19 @@ async def token(
if not user or not attributes:
raise UnAuthenticated(detail="Invalid CAS ticket")
# Create user if not exists
if not await user_service.get_by_id(db, attributes["uid"]):
resolved_user = await user_service.get_by_id(db, attributes["uid"])
if not resolved_user:
await user_service.create_user(
db,
UserCreate(
given_name=attributes["givenname"],
surname=attributes["surname"],
uid=attributes["uid"],
mail=attributes["mail"],
),
)
elif resolved_user.surname == 'SURNAME_DEFAULT':
resolved_user.surname = attributes["surname"]

# Create JWT token
jwt_token = create_jwt_token(attributes["uid"])
Expand Down
14 changes: 13 additions & 1 deletion backend/src/group/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,24 @@ async def create_group_validation(
raise NotAuthorized()


async def groups_permission_validation(
group_id: int,
user: User = Depends(get_authenticated_user),
db: AsyncSession = Depends(get_async_db)
):

from src.group.utils import has_group_privileges
if not await has_group_privileges(group_id, user, db, False):
raise NotAuthorized()


async def join_group(
group_id: int,
uid: Optional[str] = None,
db: AsyncSession = Depends(get_async_db),
user: User = Depends(get_authenticated_user)
) -> Group:

if not uid:
uid = user.uid

Expand All @@ -67,4 +79,4 @@ async def join_group(
raise MaxCapacity()

await service.join_group(db, group_id, uid)
return group
return await service.get_group_by_id(db, group_id)
21 changes: 13 additions & 8 deletions backend/src/group/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
from src.dependencies import get_async_db
from src.group.dependencies import (
create_group_validation,
groups_permission_validation,
retrieve_group,
retrieve_groups_by_project,
)
from src.group.dependencies import join_group as join_group_dependency
from src.group.exceptions import GroupNotFound
Expand All @@ -27,11 +27,6 @@
)


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


@router.post("/", status_code=201, dependencies=[Depends(create_group_validation)])
async def create_group(group: GroupCreate, db: AsyncSession = Depends(get_async_db)):
return await service.create_group(db, group)
Expand All @@ -42,7 +37,12 @@ async def get_group(group: Group = Depends(retrieve_group)):
return group


@router.delete("/{group_id}", status_code=200)
@router.delete("/{group_id}", dependencies=[Depends(groups_permission_validation)])
async def delete_group(group_id: int, db: AsyncSession = Depends(get_async_db)):
await service.delete_group(db, group_id)


@router.post("/{group_id}/leave", status_code=200)
async def leave_group(
group: Group = Depends(retrieve_group),
user: User = Depends(get_authenticated_user),
Expand All @@ -66,6 +66,11 @@ async def list_submissions(group_id: int,
return await get_submissions_by_group(db, group_id)


@router.post("/{group_id}/{uid}", status_code=201,)
@router.post("/{group_id}/{uid}", status_code=201, dependencies=[Depends(groups_permission_validation)])
async def join_group_by_uid(group: Group = Depends(join_group_dependency)) -> Group:
return group


@router.delete("/{group_id}/{uid}", status_code=200, dependencies=[Depends(groups_permission_validation)])
async def remove_user_from_group(group_id: int, uid: str, db: AsyncSession = Depends(get_async_db)):
await service.leave_group(db, group_id, uid)
7 changes: 6 additions & 1 deletion backend/src/group/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,14 @@ async def create_group(db: AsyncSession, group: schemas.GroupCreate) -> 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.flush()
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()


async def delete_group(db: AsyncSession, group_id: int):
await db.execute(delete(Group).filter_by(id=group_id))
await db.commit()
5 changes: 3 additions & 2 deletions backend/src/group/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@
async def has_group_privileges(
group_id: int,
user: User,
db: AsyncSession
db: AsyncSession,
or_member=True
) -> bool:
group = await retrieve_group(group_id, db)
project = await retrieve_project(group.project_id, user, db)
return await has_subject_privileges(project.subject_id, user, db) or user in group.members
return await has_subject_privileges(project.subject_id, user, db) or (or_member and user in group.members)
1 change: 0 additions & 1 deletion backend/src/project/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,4 @@ async def patch_permission_validation(
):
await user_permission_validation(project.subject_id, user, db)


delete_permission_validation = patch_permission_validation
7 changes: 4 additions & 3 deletions backend/src/project/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class Project(Base):
deadline: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False)
publish_date: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=True, default=datetime.now()
DateTime(timezone=True), nullable=True, default=datetime.now
)
name: Mapped[str] = mapped_column(nullable=False)
subject_id: Mapped[int] = mapped_column(
Expand All @@ -27,7 +27,8 @@ class Project(Base):
)

requirements: Mapped[List["Requirement"]] = relationship(
back_populates="project", lazy="joined")
# see submission/models/Submission -> testresults
back_populates="project", lazy="joined", passive_deletes="all")

test_files_uuid: Mapped[str | None] = mapped_column(nullable=True)

Expand All @@ -49,7 +50,7 @@ class Requirement(Base):

id: Mapped[int] = mapped_column(primary_key=True)
project_id: Mapped[int] = mapped_column(ForeignKey(
"project.id", ondelete="CASCADE"), nullable=True)
"project.id", ondelete="CASCADE"), nullable=False)
project: Mapped["Project"] = relationship(back_populates="requirements")

# True for mandatory False for prohibited
Expand Down
10 changes: 1 addition & 9 deletions backend/src/project/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,10 @@ class ProjectBase(BaseModel):
deadline: datetime
description: str
subject_id: int
is_visible: bool = Field(default=False)
is_visible: bool = Field(default=True)
capacity: int = Field(gt=0)
requirements: List[Requirement] = []

@field_validator("description")
def validate_description(cls, value: str) -> str:
return escape(value, quote=False)


class ProjectCreate(ProjectBase):
pass
Expand Down Expand Up @@ -61,7 +57,3 @@ 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

@field_validator("description")
def validate_description(cls, value: str) -> str:
return escape(value, quote=False)
4 changes: 2 additions & 2 deletions backend/src/subject/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ class Subject(Base):

id: Mapped[int] = mapped_column(primary_key=True)
academic_year: Mapped[int] = mapped_column(
nullable=False, default=datetime.now().year)
uuid: Mapped[str] = mapped_column(default=str(uuid4()))
nullable=False, default=lambda _: datetime.now().year)
uuid: Mapped[str] = mapped_column(default=lambda _: str(uuid4()))
email: Mapped[str] = mapped_column(nullable=True)
name: Mapped[str]
2 changes: 1 addition & 1 deletion backend/src/submission/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class Submission(Base):
__tablename__ = "submission"

id: Mapped[int] = mapped_column(primary_key=True)
date: Mapped[datetime] = mapped_column(default=datetime.now(),
date: Mapped[datetime] = mapped_column(default=datetime.now,
nullable=False)
status: Mapped[Status] = mapped_column(default=Status.InProgress, nullable=False)
remarks: Mapped[str] = mapped_column(nullable=True)
Expand Down
2 changes: 1 addition & 1 deletion backend/src/user/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from src.project.schemas import ProjectList

from .exceptions import UserNotFound
from .schemas import User, UserSimple, UserSubjectList
from .schemas import User, UserSubjectList


async def get_authenticated_user(
Expand Down
Loading

0 comments on commit f65f4f4

Please sign in to comment.