Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Schools #641

Merged
merged 66 commits into from
Jan 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
81941a4
Feat: refactoring providers and auth
Rotheem Nov 13, 2024
5150011
Feat: add migration file
Rotheem Nov 13, 2024
6234208
Fix: remove excess lines
Rotheem Nov 13, 2024
6b42927
Fix: correct migration
Rotheem Nov 13, 2024
2df9b05
Fix: test and migration
Rotheem Nov 16, 2024
413be4d
Fix: migration
Rotheem Nov 16, 2024
aec086b
Fix: reorder migrations
Rotheem Nov 16, 2024
a62101d
Feat: user dependencies refacto and minor fix
Rotheem Nov 16, 2024
eec420b
Fix: tools
Rotheem Nov 16, 2024
9dfa35c
Fix: import
Rotheem Nov 16, 2024
eaeb6a1
Fix: rename arguments
Rotheem Nov 16, 2024
70e12f6
Fix: correct is_user dependency
Rotheem Nov 16, 2024
13cb4e0
Fix: argument name
Rotheem Nov 16, 2024
1bad4fb
Fix: auth test
Rotheem Nov 17, 2024
78a9f60
Fix: migrations
Rotheem Nov 17, 2024
93ae13a
Fix: remove unused migration
Rotheem Nov 17, 2024
aac740b
Fix: reorder migrations
Rotheem Nov 17, 2024
15b94df
Feat: add test to exclude group in user dependency
Rotheem Dec 2, 2024
90016ab
Fix: remove useless override
Rotheem Dec 2, 2024
c8ac4d9
Fix: linting
Rotheem Dec 2, 2024
b9d6696
Feat: refactoring providers and auth
Rotheem Nov 13, 2024
66b1f74
Feat: add schools
Rotheem Oct 27, 2024
d29f0f6
Fix: rebase
Rotheem Nov 18, 2024
2c4dd00
Fix: reorder migrations
Rotheem Nov 18, 2024
628706d
Fix: migration
Rotheem Nov 18, 2024
8b739e7
Fix: migration
Rotheem Nov 19, 2024
f0ed111
Fix: migration
Rotheem Nov 19, 2024
60a2b87
Fix: migration
Rotheem Nov 19, 2024
ad02571
Fix: migration
Rotheem Nov 19, 2024
2c0cd0f
Fix: migration
Rotheem Nov 19, 2024
3c2801c
Fix: student regex
Rotheem Nov 19, 2024
db91640
Fix: tests
Rotheem Nov 19, 2024
513308a
Fix: remove school from core_user init
Rotheem Nov 19, 2024
ff1bc8a
Fix: initialise schools on launch
Rotheem Nov 19, 2024
c5619ed
Fix: migration and user declaration
Rotheem Nov 19, 2024
fdf01db
Fix: add router
Rotheem Nov 19, 2024
dd74405
Feat: remove school_id from user creation
Rotheem Nov 19, 2024
b355a39
Feat: tests
Rotheem Nov 19, 2024
8b7885e
Feat: add users to school on school creation
Rotheem Nov 19, 2024
b5c11e0
Fix: add comment
Rotheem Nov 20, 2024
f477b61
Fix: correct rebase
Rotheem Nov 27, 2024
1045aa0
Fix: change tests name
Rotheem Dec 22, 2024
83f1556
Fix: correct rebase
Rotheem Dec 22, 2024
0c8decf
Fix: format
Rotheem Dec 22, 2024
adb35ad
Fix: rename group member test
Rotheem Dec 22, 2024
dea91e9
Fix: apply suggested changes from Daihecyy
Rotheem Dec 23, 2024
b2519d2
Feat: add users in school creation and deletion tests
Rotheem Dec 23, 2024
2189e33
Fix: add explicit verification for users during tests
Rotheem Dec 23, 2024
09398cd
Feat: update users' school on school's regex update
Rotheem Dec 25, 2024
4d16300
Fix code scanning alert no. 20: Regular expression injection
Rotheem Jan 2, 2025
e20d12d
Fix: apply suggestions
Rotheem Jan 2, 2025
4331600
Fix: remove selectinload
Rotheem Jan 2, 2025
8ed9504
Fix: use school as user filter
Rotheem Jan 2, 2025
b34aa67
Fix: linting
Rotheem Jan 2, 2025
4eb66ad
Fix: test
Rotheem Jan 2, 2025
91b48ef
Fix: tests
Rotheem Jan 2, 2025
2c1b185
Feat: add test and remove security suggestion
Rotheem Jan 2, 2025
8cf1879
Fix: use UUID and apply suggestions
Rotheem Jan 7, 2025
eac2242
Fix: use UUID
Rotheem Jan 7, 2025
720921e
Feat: prevent school change through email migration
Rotheem Jan 7, 2025
7ab8bad
Fix: user update on school update
Rotheem Jan 7, 2025
01a7540
Fix: migration
Rotheem Jan 7, 2025
53bfc14
Fix: remove school name index
Rotheem Jan 7, 2025
16ff296
Fix: missing argument
Rotheem Jan 7, 2025
4411f58
Fix: migration
Rotheem Jan 7, 2025
176ccf9
Fix: apply suggestion
Rotheem Jan 13, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from app.core.groups import endpoints_groups
from app.core.notification import endpoints_notification
from app.core.payment import endpoints_payment
from app.core.schools import endpoints_schools
from app.core.users import endpoints_users
from app.modules.module_list import module_list

Expand All @@ -24,6 +25,7 @@
api_router.include_router(endpoints_notification.router)
api_router.include_router(endpoints_payment.router)
api_router.include_router(endpoints_users.router)
api_router.include_router(endpoints_schools.router)

for module in module_list:
api_router.include_router(module.router)
31 changes: 31 additions & 0 deletions app/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
from app.core.google_api.google_api import GoogleAPI
from app.core.groups.groups_type import GroupType
from app.core.log import LogConfig
from app.core.schools.schools_type import SchoolType
from app.dependencies import (
get_db,
get_redis_client,
Expand Down Expand Up @@ -189,6 +190,32 @@ def initialize_groups(
)


def initialize_schools(
sync_engine: Engine,
hyperion_error_logger: logging.Logger,
) -> None:
"""Add the necessary shools"""

hyperion_error_logger.info("Startup: Adding new groups to the database")
with Session(sync_engine) as db:
for school in SchoolType:
exists = initialization.get_school_by_id_sync(school_id=school.value, db=db)
# We don't want to recreate the groups if they already exist
if not exists:
db_school = models_core.CoreSchool(
id=school.value,
name=school.name,
email_regex="null",
)

try:
initialization.create_school_sync(school=db_school, db=db)
except IntegrityError as error:
hyperion_error_logger.fatal(
f"Startup: Could not add school {db_school.name}<{db_school.id}> in the database: {error}",
)


def initialize_module_visibility(
sync_engine: Engine,
hyperion_error_logger: logging.Logger,
Expand Down Expand Up @@ -301,6 +328,10 @@ def init_db(
sync_engine=sync_engine,
hyperion_error_logger=hyperion_error_logger,
)
initialize_schools(
sync_engine=sync_engine,
hyperion_error_logger=hyperion_error_logger,
)
initialize_module_visibility(
sync_engine=sync_engine,
hyperion_error_logger=hyperion_error_logger,
Expand Down
4 changes: 2 additions & 2 deletions app/core/auth/endpoints_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
from app.types.exceptions import AuthHTTPException
from app.types.scopes_type import ScopeType
from app.utils.auth.providers import BaseAuthClient
from app.utils.tools import is_user_member_of_an_allowed_group
from app.utils.tools import is_user_member_of_any_group

router = APIRouter(tags=["Auth"])

Expand Down Expand Up @@ -308,7 +308,7 @@ async def authorize_validation(
# The auth_client may restrict the usage of the client to specific Hyperion groups.
# For example, only ECLAIR members may be allowed to access the wiki
if auth_client.allowed_groups is not None:
if not is_user_member_of_an_allowed_group(
if not is_user_member_of_any_group(
user=user,
allowed_groups=auth_client.allowed_groups,
):
Expand Down
4 changes: 3 additions & 1 deletion app/core/groups/groups_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ class AccountType(str, Enum):
staff = "staff"
association = "association"
external = "external"
other_school_student = "other_school_student"
demo = "demo"

def __str__(self):
Expand All @@ -57,11 +58,12 @@ def get_ecl_account_types() -> list[AccountType]:
]


def get_account_types_except_external() -> list[AccountType]:
def get_account_types_except_externals() -> list[AccountType]:
Rotheem marked this conversation as resolved.
Show resolved Hide resolved
return [
AccountType.student,
AccountType.former_student,
AccountType.staff,
AccountType.association,
AccountType.demo,
AccountType.other_school_student,
]
15 changes: 15 additions & 0 deletions app/core/models_core.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Common model files for all core in order to avoid circular import due to bidirectional relationship"""

from datetime import date, datetime
from uuid import UUID

from sqlalchemy import ForeignKey, String
from sqlalchemy.orm import Mapped, mapped_column, relationship
Expand Down Expand Up @@ -29,6 +30,7 @@ class CoreUser(Base):
index=True,
) # Use UUID later
email: Mapped[str] = mapped_column(unique=True, index=True)
school_id: Mapped[UUID] = mapped_column(ForeignKey("core_school.id"))
password_hash: Mapped[str]
# Depending on the account type, the user may have different rights and access to different features
# External users may exist for:
Expand All @@ -54,6 +56,11 @@ class CoreUser(Base):
lazy="selectin",
default_factory=list,
)
school: Mapped["CoreSchool"] = relationship(
"CoreSchool",
lazy="selectin",
init=False,
)


class CoreUserUnconfirmed(Base):
Expand Down Expand Up @@ -118,6 +125,14 @@ class CoreGroup(Base):
)


class CoreSchool(Base):
__tablename__ = "core_school"

id: Mapped[PrimaryKey]
name: Mapped[str] = mapped_column(unique=True)
email_regex: Mapped[str]


class CoreAssociationMembership(Base):
__tablename__ = "core_association_membership"

Expand Down
68 changes: 44 additions & 24 deletions app/core/schemas_core.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Common schemas file for endpoint /users et /groups because it would cause circular import"""

from datetime import date, datetime
from uuid import UUID

from pydantic import BaseModel, ConfigDict, Field
from pydantic.functional_validators import field_validator
Expand All @@ -19,6 +20,44 @@ class CoreInformation(BaseModel):
minimal_titan_version_code: int


class CoreGroupBase(BaseModel):
"""Base schema for group's model"""

name: str
description: str | None = None

_normalize_name = field_validator("name")(validators.trailing_spaces_remover)


class CoreGroupSimple(CoreGroupBase):
"""Simplified schema for group's model, used when getting all groups"""

id: str
model_config = ConfigDict(from_attributes=True)
Rotheem marked this conversation as resolved.
Show resolved Hide resolved


class CoreSchoolBase(BaseModel):
"""Schema for school's model"""

name: str
email_regex: str

_normalize_name = field_validator("name")(validators.trailing_spaces_remover)


class CoreSchool(CoreSchoolBase):
id: UUID


class CoreSchoolUpdate(BaseModel):
"""Schema for school update"""

name: str | None = None
email_regex: str | None = None

_normalize_name = field_validator("name")(validators.trailing_spaces_remover)


class CoreUserBase(BaseModel):
"""Base schema for user's model"""

Expand All @@ -35,41 +74,27 @@ class CoreUserBase(BaseModel):
)


class CoreGroupBase(BaseModel):
"""Base schema for group's model"""

name: str
description: str | None = None

_normalize_name = field_validator("name")(validators.trailing_spaces_remover)


class CoreUserSimple(CoreUserBase):
"""Simplified schema for user's model, used when getting all users"""

id: str
account_type: AccountType
model_config = ConfigDict(from_attributes=True)


class CoreGroupSimple(CoreGroupBase):
"""Simplified schema for group's model, used when getting all groups"""
school_id: UUID

id: str
model_config = ConfigDict(from_attributes=True)


class CoreUser(CoreUserSimple):
"""Schema for user's model similar to core_user table in database"""

email: str
account_type: AccountType
birthday: date | None = None
promo: int | None = None
floor: FloorsType | None = None
phone: str | None = None
created_on: datetime | None = None
groups: list[CoreGroupSimple] = []
school: CoreSchool | None = None


class CoreUserUpdate(BaseModel):
Expand Down Expand Up @@ -97,6 +122,8 @@ class CoreUserFusionRequest(BaseModel):


class CoreUserUpdateAdmin(BaseModel):
email: str | None = None
school_id: UUID | None = None
account_type: AccountType | None = None
name: str | None = None
firstname: str | None = None
Expand Down Expand Up @@ -164,7 +191,7 @@ class CoreUserActivateRequest(CoreUserBase):
floor: FloorsType | None = None
promo: int | None = Field(
default=None,
description="Promotion of the student, an integer like 21",
description="Promotion of the student, an integer like 2021",
)

# Password validator
Expand All @@ -189,13 +216,6 @@ class CoreGroupCreate(CoreGroupBase):
"""Model for group creation schema"""


class CoreGroupInDB(CoreGroupBase):
"""Schema for user activation"""

id: str
model_config = ConfigDict(from_attributes=True)


class CoreGroupUpdate(BaseModel):
"""Schema for group update"""

Expand Down
Empty file added app/core/schools/__init__.py
Empty file.
86 changes: 86 additions & 0 deletions app/core/schools/cruds_schools.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
"""File defining the functions called by the endpoints, making queries to the table using the models"""
Daihecyy marked this conversation as resolved.
Show resolved Hide resolved

from collections.abc import Sequence
from uuid import UUID

from sqlalchemy import delete, select, update
from sqlalchemy.ext.asyncio import AsyncSession

from app.core import models_core, schemas_core


async def get_schools(db: AsyncSession) -> Sequence[models_core.CoreSchool]:
"""Return all schools from database"""

result = await db.execute(select(models_core.CoreSchool))
return result.scalars().all()


async def get_school_by_id(
Rotheem marked this conversation as resolved.
Show resolved Hide resolved
db: AsyncSession,
school_id: UUID,
) -> schemas_core.CoreSchool | None:
"""Return school with id from database"""
result = (
(
await db.execute(
select(models_core.CoreSchool).where(
models_core.CoreSchool.id == school_id,
),
)
)
.scalars()
.first()
)
return (
schemas_core.CoreSchool(
name=result.name,
email_regex=result.email_regex,
id=result.id,
)
if result
else None
)


async def get_school_by_name(
db: AsyncSession,
school_name: str,
) -> models_core.CoreSchool | None:
"""Return school with name from database"""
result = await db.execute(
select(models_core.CoreSchool).where(
models_core.CoreSchool.name == school_name,
),
)
return result.scalars().first()


async def create_school(
school: models_core.CoreSchool,
db: AsyncSession,
) -> None:
"""Create a new school in database and return it"""

db.add(school)


async def delete_school(db: AsyncSession, school_id: UUID):
"""Delete a school from database by id"""

await db.execute(
delete(models_core.CoreSchool).where(models_core.CoreSchool.id == school_id),
)


async def update_school(
db: AsyncSession,
school_id: UUID,
school_update: schemas_core.CoreSchoolUpdate,
):
await db.execute(
update(models_core.CoreSchool)
.where(models_core.CoreSchool.id == school_id)
.values(**school_update.model_dump(exclude_none=True)),
)
await db.commit()
Loading
Loading