From d38e1eeebbef5e801c3d7126b61d5d3db98dc569 Mon Sep 17 00:00:00 2001 From: Warix <39554785+warix8@users.noreply.github.com> Date: Sat, 15 Nov 2025 15:53:38 +0100 Subject: [PATCH 01/10] Init pmf module --- app/modules/pmf/__init__.py | 0 app/modules/pmf/endpoints_pmf.py | 9 +++++++++ 2 files changed, 9 insertions(+) create mode 100644 app/modules/pmf/__init__.py create mode 100644 app/modules/pmf/endpoints_pmf.py diff --git a/app/modules/pmf/__init__.py b/app/modules/pmf/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/app/modules/pmf/endpoints_pmf.py b/app/modules/pmf/endpoints_pmf.py new file mode 100644 index 0000000000..cddb86e9f0 --- /dev/null +++ b/app/modules/pmf/endpoints_pmf.py @@ -0,0 +1,9 @@ +from app.core.groups.groups_type import AccountType +from app.types.module import Module + +module = Module( + root="pmf", + tag="Pmf", + default_allowed_account_types=[AccountType.student, AccountType.staff], + factory=None, +) From 603aa5cc0b555b631eb374f6b4135a5d560dbe9c Mon Sep 17 00:00:00 2001 From: Warix <39554785+warix8@users.noreply.github.com> Date: Sat, 15 Nov 2025 18:54:43 +0100 Subject: [PATCH 02/10] MVP WIP --- app/modules/pmf/cruds_pmf.py | 160 ++++++++++++++++++++ app/modules/pmf/endpoints_pmf.py | 248 ++++++++++++++++++++++++++++++- app/modules/pmf/models_pmf.py | 70 +++++++++ app/modules/pmf/schemas_pmf.py | 50 +++++++ app/modules/pmf/types_pmf.py | 15 ++ 5 files changed, 541 insertions(+), 2 deletions(-) create mode 100644 app/modules/pmf/cruds_pmf.py create mode 100644 app/modules/pmf/models_pmf.py create mode 100644 app/modules/pmf/schemas_pmf.py create mode 100644 app/modules/pmf/types_pmf.py diff --git a/app/modules/pmf/cruds_pmf.py b/app/modules/pmf/cruds_pmf.py new file mode 100644 index 0000000000..b28e807b4b --- /dev/null +++ b/app/modules/pmf/cruds_pmf.py @@ -0,0 +1,160 @@ +from uuid import UUID + +from sqlalchemy import delete, select, true, update +from sqlalchemy.ext.asyncio import AsyncSession + +from app.core.users import schemas_users +from app.modules.pmf import models_pmf, schemas_pmf, types_pmf + + +async def create_offer(offer: schemas_pmf.OfferSimple, db: AsyncSession) -> None: + """Create a new PMF offer with associated tags.""" + db.add( + models_pmf.PmfOffer( + id=offer.id, + author_id=offer.author_id, + company_name=offer.company_name, + title=offer.title, + description=offer.description, + offer_type=offer.offer_type, + location=offer.location, + location_type=offer.location_type, + start_date=offer.start_date, + end_date=offer.end_date, + duration=offer.duration, + tags=[], + ), + ) + + +async def update_offer( + offer_id: UUID, + structure_update: schemas_pmf.OfferUpdate, + db: AsyncSession, +) -> None: + await db.execute( + update(models_pmf.PmfOffer) + .where(models_pmf.PmfOffer.id == offer_id) + .values(**structure_update.model_dump(exclude_unset=True)), + ) + + +async def delete_offer(offer_id: UUID, db: AsyncSession) -> None: + await db.execute( + delete(models_pmf.PmfOffer).where(models_pmf.PmfOffer.id == offer_id), + ) + + +async def get_offer_by_id( + offer_id: UUID, + db: AsyncSession, +) -> models_pmf.PmfOffer | None: + result = await db.execute( + select(models_pmf.PmfOffer).where(models_pmf.PmfOffer.id == offer_id), + ) + return result.scalars().first() + + +async def get_offers( + db: AsyncSession, + included_offer_types: list[types_pmf.OfferType] | None = None, + included_tags: list[str] | None = None, + included_location_types: list[types_pmf.LocationType] | None = None, +) -> list[schemas_pmf.OfferComplete]: + where_clause = ( + ( + models_pmf.PmfOffer.offer_type.in_(included_offer_types) + if included_offer_types + else true() + ) + & ( + models_pmf.PmfOffer.tags.any(models_pmf.Tags.tag.in_(included_tags)) + if included_tags + else true() + ) + & ( + models_pmf.PmfOffer.location_type.in_(included_location_types) + if included_location_types + else true() + ) + ) + + offers = await db.execute( + select(models_pmf.PmfOffer).where(where_clause), + ) + return [ + schemas_pmf.OfferComplete( + id=offer.id, + author_id=offer.author_id, + company_name=offer.company_name, + title=offer.title, + description=offer.description, + offer_type=offer.offer_type, + location=offer.location, + location_type=offer.location_type, + start_date=offer.start_date, + end_date=offer.end_date, + duration=offer.duration, + author=schemas_users.CoreUserSimple.model_validate(offer.author), + tags=[schemas_pmf.TagComplete.model_validate(tag) for tag in offer.tags], + ) + for offer in offers.scalars().all() + ] + +async def get_all_tags(db: AsyncSession) -> list[schemas_pmf.TagComplete]: + tags = await db.execute( + select(models_pmf.Tags).distinct(models_pmf.Tags.tag), + ) + return [ + schemas_pmf.TagComplete.model_validate(tag) + for tag in tags.scalars().all() + ] + +async def get_tag_by_name( + tag_name: str, + db: AsyncSession, +) -> schemas_pmf.TagComplete | None: + result = await db.execute( + select(models_pmf.Tags).where(models_pmf.Tags.tag == tag_name), + ) + return schemas_pmf.TagComplete.model_validate(result.scalars().first()) + +async def get_tag_by_id( + tag_id: UUID, + db: AsyncSession, +) -> schemas_pmf.TagComplete | None: + result = await db.execute( + select(models_pmf.Tags).where(models_pmf.Tags.id == tag_id), + ) + return schemas_pmf.TagComplete.model_validate(result.scalars().first()) + +async def create_tag( + tag: schemas_pmf.TagComplete, + db: AsyncSession, +) -> None: + tag_db = models_pmf.Tags( + id=tag.id, + tag=tag.tag, + ) + db.add(tag_db) + +async def update_tag( + tag_id: UUID, + tag_update: schemas_pmf.TagBase, + db: AsyncSession, +) -> None: + await db.execute( + update(models_pmf.Tags) + .where(models_pmf.Tags.id == tag_id) + .values(**tag_update.model_dump(exclude_unset=True)), + ) + +async def delete_tag( + tag_id: UUID, + db: AsyncSession, +) -> None: + + await db.execute( + delete(models_pmf.Tags).where(models_pmf.Tags.id == tag_id), + ) + diff --git a/app/modules/pmf/endpoints_pmf.py b/app/modules/pmf/endpoints_pmf.py index cddb86e9f0..a6a6ad1942 100644 --- a/app/modules/pmf/endpoints_pmf.py +++ b/app/modules/pmf/endpoints_pmf.py @@ -1,9 +1,253 @@ -from app.core.groups.groups_type import AccountType +import uuid +from uuid import UUID + +from fastapi import APIRouter, Depends, HTTPException, Query +from sqlalchemy.ext.asyncio import AsyncSession + +from app.core.groups.groups_type import AccountType, GroupType +from app.core.users.models_users import CoreUser +from app.dependencies import is_user, is_user_in +from app.modules.pmf import cruds_pmf, schemas_pmf, types_pmf from app.types.module import Module +from app.utils.tools import is_user_member_of_any_group + +router = APIRouter(tags=["pmf"]) module = Module( root="pmf", tag="Pmf", - default_allowed_account_types=[AccountType.student, AccountType.staff], + default_allowed_account_types=[ + AccountType.student, + AccountType.staff, + AccountType.former_student, + ], factory=None, ) + + +@router.get( + "/pmf/offers/{offer_id}", + response_model=schemas_pmf.OfferComplete, + status_code=200, +) +async def get_offer( + offer_id: UUID, + db: AsyncSession, + # Allow only former students to access this endpoint + # user: CoreUser = Depends(is_user(included_account_types=[AccountType.former_student])), +): + offer = await cruds_pmf.get_offer_by_id( + offer_id=offer_id, + db=db, + ) + + if not offer: + raise HTTPException(status_code=404, detail="Offer not found") + + return offer + + +@router.get( + "/pmf/offers/", + response_model=list[schemas_pmf.OfferSimple], + status_code=200, +) +async def get_offers( + db: AsyncSession, + includedOfferTypes: list[types_pmf.OfferType] = Query(default=[]), + includedTags: list[str] = Query(default=[]), + includedLocationTypes: list[types_pmf.LocationType] = Query(default=[]), + # Allow only former students to access this endpoint + # user: CoreUser = Depends(is_user(included_account_types=[AccountType.former_student])), +): + return await cruds_pmf.get_offers( + db=db, + included_offer_types=includedOfferTypes, + included_tags=includedTags, + included_location_types=includedLocationTypes, + ) + + +@router.post( + "/pmf/offer/", + response_model=list[schemas_pmf.OfferComplete], + status_code=200, +) +async def create_offer( + db: AsyncSession, + offer: schemas_pmf.OfferBase, + # Allow only former students to create offer + user: CoreUser = Depends( + is_user(included_account_types=[AccountType.former_student]), + ), +): + # Only admin can post offers on behalf of others + if offer.author_id != user.id and not is_user_member_of_any_group( + user, + [ + GroupType.admin, + ], + ): + raise HTTPException(status_code=403, detail="Forbidden, you are not the author of this offer") + + offer_db = schemas_pmf.OfferSimple( + **offer.model_dump(), + id=uuid.uuid4(), + author_id=user.id, + ) + return await cruds_pmf.create_offer(db=db, offer=offer_db) + +@router.put( + "/pmf/offer/{offer_id}", + response_model=None, + status_code=204, +) +async def update_offer( + offer_id: UUID, + db: AsyncSession, + offer_update: schemas_pmf.OfferUpdate, + # Allow only former students to update offer + user: CoreUser = Depends( + is_user(included_account_types=[AccountType.former_student]), + ), +): + offer_db = await cruds_pmf.get_offer_by_id(offer_id=offer_id, db=db) + if not offer_db: + raise HTTPException(status_code=404, detail="Offer not found") + + # Only the author or admin can update the offer + if ( + offer_db.author_id != user.id + and not is_user_member_of_any_group( + user, + [ + GroupType.admin, + ], + ) + ): + raise HTTPException(status_code=403, detail="Forbidden, you are not the author of this offer") + + await cruds_pmf.update_offer( + offer_id=offer_id, + structure_update=offer_update, + db=db, + ) + +@router.delete( + "/pmf/offer/{offer_id}", + response_model=None, + status_code=204, +) +async def delete_offer( + offer_id: UUID, + db: AsyncSession, + # Allow only former students to delete offer + user: CoreUser = Depends( + is_user(included_account_types=[AccountType.former_student]), + ), +): + offer_db = await cruds_pmf.get_offer_by_id(offer_id=offer_id, db=db) + if not offer_db: + raise HTTPException(status_code=404, detail="Offer not found") + + # Only the author or admin can delete the offer + if ( + offer_db.author_id != user.id + and not is_user_member_of_any_group( + user, + [ + GroupType.admin, + ], + ) + ): + raise HTTPException(status_code=403, detail="Forbidden, you are not the author of this offer") + + await cruds_pmf.delete_offer(offer_id=offer_id, db=db) + +@router.get( + "/pmf/tags/", + response_model=list[schemas_pmf.TagComplete], + status_code=200, +) +async def get_all_tags( + db: AsyncSession, +) -> list[schemas_pmf.TagComplete]: + return await cruds_pmf.get_all_tags(db=db) + +@router.get( + "/pmf/tag/{tag_id}", + response_model=schemas_pmf.TagComplete | None, + status_code=200, +) +async def get_tag( + tag_id: UUID, + db: AsyncSession, +) -> schemas_pmf.TagComplete | None: + tags = await cruds_pmf.get_all_tags(db=db) + for tag in tags: + if tag.id == tag_id: + return tag + return None + +@router.post( + "/pmf/tag/", + response_model=schemas_pmf.TagComplete, + status_code=201, +) +async def create_tag( + tag: schemas_pmf.TagBase, + db: AsyncSession, + # Allow only admin to create tags + user: CoreUser = Depends( + is_user_in(group_id=GroupType.admin), + ), +): + existing_tag = await cruds_pmf.get_tag_by_name(tag_name=tag.tag, db=db) + if existing_tag: + raise HTTPException(status_code=400, detail="Tag already exists") + + tag_db = schemas_pmf.TagComplete( + **tag.model_dump(), + id=uuid.uuid4(), + ) + await cruds_pmf.create_tag(tag=tag_db, db=db) + return tag_db + +@router.put( + "/pmf/tag/{tag_id}", + response_model=None, + status_code=204, +) +async def update_tag( + tag_id: UUID, + tag_update: schemas_pmf.TagBase, + db: AsyncSession, + # Allow only admin to update tags + user: CoreUser = Depends( + is_user_in(group_id=GroupType.admin), + ), +): + existing_tag = await cruds_pmf.get_tag_by_id(tag_id=tag_id, db=db) + if not existing_tag: + raise HTTPException(status_code=404, detail="Tag not found") + + await cruds_pmf.update_tag(tag_id=tag_id, tag_update=tag_update, db=db) + +@router.delete( + "/pmf/tag/{tag_id}", + response_model=None, + status_code=204, +) +async def delete_tag( + tag_id: UUID, + db: AsyncSession, + # Allow only admin to delete tags + user: CoreUser = Depends( + is_user_in(group_id=GroupType.admin), + ), +): + existing_tag = await cruds_pmf.get_tag_by_id(tag_id=tag_id, db=db) + if not existing_tag: + raise HTTPException(status_code=404, detail="Tag not found") + + await cruds_pmf.delete_tag(tag_id=tag_id, db=db) diff --git a/app/modules/pmf/models_pmf.py b/app/modules/pmf/models_pmf.py new file mode 100644 index 0000000000..ae86c396b7 --- /dev/null +++ b/app/modules/pmf/models_pmf.py @@ -0,0 +1,70 @@ +from datetime import date +from typing import TYPE_CHECKING +from uuid import UUID + +from sqlalchemy import ForeignKey +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from app.modules.pmf.types_pmf import LocationType, OfferType +from app.types.sqlalchemy import Base, PrimaryKey + +if TYPE_CHECKING: + from app.core.users.models_users import CoreUser + + +class OfferTags(Base): + __tablename__ = "pmf_offer_tags" + + offer_id: Mapped[UUID] = mapped_column( + ForeignKey("pmf_offers.id"), primary_key=True, + ) + tag_id: Mapped[UUID] = mapped_column(ForeignKey("pmf_tags.id"), primary_key=True) + + +class PmfOffer(Base): + __tablename__ = "pmf_offers" + + id: Mapped[PrimaryKey] + + # TODO: Decide if the offer can remain if the author is deleted + author_id: Mapped[str] = mapped_column(ForeignKey("core_user.id")) + author: Mapped["CoreUser"] = relationship( + init=False, + lazy="joined", + innerjoin=True, # INNER JOIN since author_id is NOT NULL + ) + + company_name: Mapped[str] + title: Mapped[str] + description: Mapped[str] + offer_type: Mapped[OfferType] + location: Mapped[str] + location_type: Mapped[LocationType] # Enum (On_site, Hybrid, Remote) + + start_date: Mapped[date] + end_date: Mapped[date] + duration: Mapped[int] # days + + tags: Mapped[list["OfferTags"]] = relationship( + "Tags", + back_populates="offers", + lazy="selectin", # Small collection + secondary="pmf_offer_tags", + default_factory=list, + ) + + +class Tags(Base): + __tablename__ = "pmf_tags" + + id: Mapped[PrimaryKey] + tag: Mapped[str] + + created_at: Mapped[date] = mapped_column(default=date.today) + + offers: Mapped[list["OfferTags"]] = relationship( + "PmfOffer", + back_populates="tags", + secondary="pmf_offer_tags", + default_factory=list, + ) diff --git a/app/modules/pmf/schemas_pmf.py b/app/modules/pmf/schemas_pmf.py new file mode 100644 index 0000000000..5dce778ee5 --- /dev/null +++ b/app/modules/pmf/schemas_pmf.py @@ -0,0 +1,50 @@ +from datetime import datetime +from uuid import UUID + +from pydantic import BaseModel + +from app.core.users import schemas_users +from app.modules.pmf.types_pmf import LocationType, OfferType + + +class TagBase(BaseModel): + tag: str + +class TagComplete(TagBase): + id: UUID + + +class OfferBase(BaseModel): + author_id: str + + company_name: str + title: str + description: str + offer_type: OfferType + location: str + location_type: LocationType + + start_date: datetime + end_date: datetime + duration: int # days + +class OfferSimple(OfferBase): + id: UUID + +class OfferUpdate(BaseModel): + author_id: str | None = None + company_name: str | None = None + title: str | None = None + description: str | None = None + offer_type: OfferType | None = None + location: str | None = None + location_type: LocationType | None = None + start_date: datetime | None = None + end_date: datetime | None = None + duration: int | None = None # days + + tags: list[TagBase] | None = None + +class OfferComplete(OfferSimple): + author: schemas_users.CoreUserSimple + tags: list[TagComplete] diff --git a/app/modules/pmf/types_pmf.py b/app/modules/pmf/types_pmf.py new file mode 100644 index 0000000000..cd9e8cc57f --- /dev/null +++ b/app/modules/pmf/types_pmf.py @@ -0,0 +1,15 @@ +from enum import Enum + + +class OfferType(str, Enum): # for the T-shirt and the bike + TFE = "TFE" + S_APP = "Stage_Application" + EXE = "EXE" + CDI = "CDI" + CDD = "CDD" + + +class LocationType(str, Enum): + On_site = "On_site" + Hybrid = "Hybrid" + Remote = "Remote" From fa5a8ac1b4d956f128aca15ddf34981116ec3072 Mon Sep 17 00:00:00 2001 From: Warix <39554785+warix8@users.noreply.github.com> Date: Sat, 22 Nov 2025 17:29:53 +0100 Subject: [PATCH 03/10] Lint --- app/modules/pmf/cruds_pmf.py | 13 ++++----- app/modules/pmf/endpoints_pmf.py | 45 ++++++++++++++++++-------------- app/modules/pmf/models_pmf.py | 3 ++- app/modules/pmf/schemas_pmf.py | 4 +++ 4 files changed, 39 insertions(+), 26 deletions(-) diff --git a/app/modules/pmf/cruds_pmf.py b/app/modules/pmf/cruds_pmf.py index b28e807b4b..84a9c9689d 100644 --- a/app/modules/pmf/cruds_pmf.py +++ b/app/modules/pmf/cruds_pmf.py @@ -101,14 +101,13 @@ async def get_offers( for offer in offers.scalars().all() ] + async def get_all_tags(db: AsyncSession) -> list[schemas_pmf.TagComplete]: tags = await db.execute( select(models_pmf.Tags).distinct(models_pmf.Tags.tag), ) - return [ - schemas_pmf.TagComplete.model_validate(tag) - for tag in tags.scalars().all() - ] + return [schemas_pmf.TagComplete.model_validate(tag) for tag in tags.scalars().all()] + async def get_tag_by_name( tag_name: str, @@ -119,6 +118,7 @@ async def get_tag_by_name( ) return schemas_pmf.TagComplete.model_validate(result.scalars().first()) + async def get_tag_by_id( tag_id: UUID, db: AsyncSession, @@ -128,6 +128,7 @@ async def get_tag_by_id( ) return schemas_pmf.TagComplete.model_validate(result.scalars().first()) + async def create_tag( tag: schemas_pmf.TagComplete, db: AsyncSession, @@ -138,6 +139,7 @@ async def create_tag( ) db.add(tag_db) + async def update_tag( tag_id: UUID, tag_update: schemas_pmf.TagBase, @@ -149,12 +151,11 @@ async def update_tag( .values(**tag_update.model_dump(exclude_unset=True)), ) + async def delete_tag( tag_id: UUID, db: AsyncSession, ) -> None: - await db.execute( delete(models_pmf.Tags).where(models_pmf.Tags.id == tag_id), ) - diff --git a/app/modules/pmf/endpoints_pmf.py b/app/modules/pmf/endpoints_pmf.py index a6a6ad1942..d0011c1c5f 100644 --- a/app/modules/pmf/endpoints_pmf.py +++ b/app/modules/pmf/endpoints_pmf.py @@ -88,7 +88,9 @@ async def create_offer( GroupType.admin, ], ): - raise HTTPException(status_code=403, detail="Forbidden, you are not the author of this offer") + raise HTTPException( + status_code=403, detail="Forbidden, you are not the author of this offer" + ) offer_db = schemas_pmf.OfferSimple( **offer.model_dump(), @@ -97,6 +99,7 @@ async def create_offer( ) return await cruds_pmf.create_offer(db=db, offer=offer_db) + @router.put( "/pmf/offer/{offer_id}", response_model=None, @@ -116,16 +119,15 @@ async def update_offer( raise HTTPException(status_code=404, detail="Offer not found") # Only the author or admin can update the offer - if ( - offer_db.author_id != user.id - and not is_user_member_of_any_group( - user, - [ - GroupType.admin, - ], - ) + if offer_db.author_id != user.id and not is_user_member_of_any_group( + user, + [ + GroupType.admin, + ], ): - raise HTTPException(status_code=403, detail="Forbidden, you are not the author of this offer") + raise HTTPException( + status_code=403, detail="Forbidden, you are not the author of this offer" + ) await cruds_pmf.update_offer( offer_id=offer_id, @@ -133,6 +135,7 @@ async def update_offer( db=db, ) + @router.delete( "/pmf/offer/{offer_id}", response_model=None, @@ -151,19 +154,19 @@ async def delete_offer( raise HTTPException(status_code=404, detail="Offer not found") # Only the author or admin can delete the offer - if ( - offer_db.author_id != user.id - and not is_user_member_of_any_group( - user, - [ - GroupType.admin, - ], - ) + if offer_db.author_id != user.id and not is_user_member_of_any_group( + user, + [ + GroupType.admin, + ], ): - raise HTTPException(status_code=403, detail="Forbidden, you are not the author of this offer") + raise HTTPException( + status_code=403, detail="Forbidden, you are not the author of this offer" + ) await cruds_pmf.delete_offer(offer_id=offer_id, db=db) + @router.get( "/pmf/tags/", response_model=list[schemas_pmf.TagComplete], @@ -174,6 +177,7 @@ async def get_all_tags( ) -> list[schemas_pmf.TagComplete]: return await cruds_pmf.get_all_tags(db=db) + @router.get( "/pmf/tag/{tag_id}", response_model=schemas_pmf.TagComplete | None, @@ -189,6 +193,7 @@ async def get_tag( return tag return None + @router.post( "/pmf/tag/", response_model=schemas_pmf.TagComplete, @@ -213,6 +218,7 @@ async def create_tag( await cruds_pmf.create_tag(tag=tag_db, db=db) return tag_db + @router.put( "/pmf/tag/{tag_id}", response_model=None, @@ -233,6 +239,7 @@ async def update_tag( await cruds_pmf.update_tag(tag_id=tag_id, tag_update=tag_update, db=db) + @router.delete( "/pmf/tag/{tag_id}", response_model=None, diff --git a/app/modules/pmf/models_pmf.py b/app/modules/pmf/models_pmf.py index ae86c396b7..fc1bcad43a 100644 --- a/app/modules/pmf/models_pmf.py +++ b/app/modules/pmf/models_pmf.py @@ -16,7 +16,8 @@ class OfferTags(Base): __tablename__ = "pmf_offer_tags" offer_id: Mapped[UUID] = mapped_column( - ForeignKey("pmf_offers.id"), primary_key=True, + ForeignKey("pmf_offers.id"), + primary_key=True, ) tag_id: Mapped[UUID] = mapped_column(ForeignKey("pmf_tags.id"), primary_key=True) diff --git a/app/modules/pmf/schemas_pmf.py b/app/modules/pmf/schemas_pmf.py index 5dce778ee5..338e372f6d 100644 --- a/app/modules/pmf/schemas_pmf.py +++ b/app/modules/pmf/schemas_pmf.py @@ -10,6 +10,7 @@ class TagBase(BaseModel): tag: str + class TagComplete(TagBase): id: UUID @@ -28,9 +29,11 @@ class OfferBase(BaseModel): end_date: datetime duration: int # days + class OfferSimple(OfferBase): id: UUID + class OfferUpdate(BaseModel): author_id: str | None = None company_name: str | None = None @@ -45,6 +48,7 @@ class OfferUpdate(BaseModel): tags: list[TagBase] | None = None + class OfferComplete(OfferSimple): author: schemas_users.CoreUserSimple tags: list[TagComplete] From e10f76c32ff11dfe32ebcd9b256ccba0d9ca88eb Mon Sep 17 00:00:00 2001 From: Warix <39554785+warix8@users.noreply.github.com> Date: Sat, 22 Nov 2025 17:32:16 +0100 Subject: [PATCH 04/10] Pagination --- app/modules/pmf/cruds_pmf.py | 4 +++- app/modules/pmf/endpoints_pmf.py | 11 ++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/app/modules/pmf/cruds_pmf.py b/app/modules/pmf/cruds_pmf.py index 84a9c9689d..c6435aed43 100644 --- a/app/modules/pmf/cruds_pmf.py +++ b/app/modules/pmf/cruds_pmf.py @@ -60,6 +60,8 @@ async def get_offers( included_offer_types: list[types_pmf.OfferType] | None = None, included_tags: list[str] | None = None, included_location_types: list[types_pmf.LocationType] | None = None, + limit: int | None = None, + offset: int | None = None, ) -> list[schemas_pmf.OfferComplete]: where_clause = ( ( @@ -80,7 +82,7 @@ async def get_offers( ) offers = await db.execute( - select(models_pmf.PmfOffer).where(where_clause), + select(models_pmf.PmfOffer).where(where_clause).limit(limit).offset(offset), ) return [ schemas_pmf.OfferComplete( diff --git a/app/modules/pmf/endpoints_pmf.py b/app/modules/pmf/endpoints_pmf.py index d0011c1c5f..829aae623b 100644 --- a/app/modules/pmf/endpoints_pmf.py +++ b/app/modules/pmf/endpoints_pmf.py @@ -57,6 +57,8 @@ async def get_offers( includedOfferTypes: list[types_pmf.OfferType] = Query(default=[]), includedTags: list[str] = Query(default=[]), includedLocationTypes: list[types_pmf.LocationType] = Query(default=[]), + limit: int | None = Query(default=50, gt=0, le=50), + offset: int | None = Query(default=0, ge=0), # Allow only former students to access this endpoint # user: CoreUser = Depends(is_user(included_account_types=[AccountType.former_student])), ): @@ -89,7 +91,8 @@ async def create_offer( ], ): raise HTTPException( - status_code=403, detail="Forbidden, you are not the author of this offer" + status_code=403, + detail="Forbidden, you are not the author of this offer", ) offer_db = schemas_pmf.OfferSimple( @@ -126,7 +129,8 @@ async def update_offer( ], ): raise HTTPException( - status_code=403, detail="Forbidden, you are not the author of this offer" + status_code=403, + detail="Forbidden, you are not the author of this offer", ) await cruds_pmf.update_offer( @@ -161,7 +165,8 @@ async def delete_offer( ], ): raise HTTPException( - status_code=403, detail="Forbidden, you are not the author of this offer" + status_code=403, + detail="Forbidden, you are not the author of this offer", ) await cruds_pmf.delete_offer(offer_id=offer_id, db=db) From 6ef1b5ceed13959601256a865ba8a26a08bc4866 Mon Sep 17 00:00:00 2001 From: Warix <39554785+warix8@users.noreply.github.com> Date: Sat, 22 Nov 2025 18:49:03 +0100 Subject: [PATCH 05/10] Added get_tests --- app/modules/pmf/endpoints_pmf.py | 24 ++--- app/modules/pmf/models_pmf.py | 4 +- tests/modules/test_pmf.py | 163 +++++++++++++++++++++++++++++++ 3 files changed, 179 insertions(+), 12 deletions(-) create mode 100644 tests/modules/test_pmf.py diff --git a/app/modules/pmf/endpoints_pmf.py b/app/modules/pmf/endpoints_pmf.py index 829aae623b..db1c439c3e 100644 --- a/app/modules/pmf/endpoints_pmf.py +++ b/app/modules/pmf/endpoints_pmf.py @@ -6,7 +6,7 @@ from app.core.groups.groups_type import AccountType, GroupType from app.core.users.models_users import CoreUser -from app.dependencies import is_user, is_user_in +from app.dependencies import get_db, is_user, is_user_in from app.modules.pmf import cruds_pmf, schemas_pmf, types_pmf from app.types.module import Module from app.utils.tools import is_user_member_of_any_group @@ -32,7 +32,7 @@ ) async def get_offer( offer_id: UUID, - db: AsyncSession, + db: AsyncSession = Depends(get_db), # Allow only former students to access this endpoint # user: CoreUser = Depends(is_user(included_account_types=[AccountType.former_student])), ): @@ -53,7 +53,7 @@ async def get_offer( status_code=200, ) async def get_offers( - db: AsyncSession, + db: AsyncSession = Depends(get_db), includedOfferTypes: list[types_pmf.OfferType] = Query(default=[]), includedTags: list[str] = Query(default=[]), includedLocationTypes: list[types_pmf.LocationType] = Query(default=[]), @@ -67,6 +67,8 @@ async def get_offers( included_offer_types=includedOfferTypes, included_tags=includedTags, included_location_types=includedLocationTypes, + limit=limit, + offset=offset, ) @@ -76,8 +78,8 @@ async def get_offers( status_code=200, ) async def create_offer( - db: AsyncSession, offer: schemas_pmf.OfferBase, + db: AsyncSession = Depends(get_db), # Allow only former students to create offer user: CoreUser = Depends( is_user(included_account_types=[AccountType.former_student]), @@ -110,8 +112,8 @@ async def create_offer( ) async def update_offer( offer_id: UUID, - db: AsyncSession, offer_update: schemas_pmf.OfferUpdate, + db: AsyncSession = Depends(get_db), # Allow only former students to update offer user: CoreUser = Depends( is_user(included_account_types=[AccountType.former_student]), @@ -147,7 +149,7 @@ async def update_offer( ) async def delete_offer( offer_id: UUID, - db: AsyncSession, + db: AsyncSession = Depends(get_db), # Allow only former students to delete offer user: CoreUser = Depends( is_user(included_account_types=[AccountType.former_student]), @@ -178,7 +180,7 @@ async def delete_offer( status_code=200, ) async def get_all_tags( - db: AsyncSession, + db: AsyncSession = Depends(get_db), ) -> list[schemas_pmf.TagComplete]: return await cruds_pmf.get_all_tags(db=db) @@ -190,7 +192,7 @@ async def get_all_tags( ) async def get_tag( tag_id: UUID, - db: AsyncSession, + db: AsyncSession = Depends(get_db), ) -> schemas_pmf.TagComplete | None: tags = await cruds_pmf.get_all_tags(db=db) for tag in tags: @@ -206,7 +208,7 @@ async def get_tag( ) async def create_tag( tag: schemas_pmf.TagBase, - db: AsyncSession, + db: AsyncSession = Depends(get_db), # Allow only admin to create tags user: CoreUser = Depends( is_user_in(group_id=GroupType.admin), @@ -232,7 +234,7 @@ async def create_tag( async def update_tag( tag_id: UUID, tag_update: schemas_pmf.TagBase, - db: AsyncSession, + db: AsyncSession = Depends(get_db), # Allow only admin to update tags user: CoreUser = Depends( is_user_in(group_id=GroupType.admin), @@ -252,7 +254,7 @@ async def update_tag( ) async def delete_tag( tag_id: UUID, - db: AsyncSession, + db: AsyncSession = Depends(get_db), # Allow only admin to delete tags user: CoreUser = Depends( is_user_in(group_id=GroupType.admin), diff --git a/app/modules/pmf/models_pmf.py b/app/modules/pmf/models_pmf.py index fc1bcad43a..7a27e48efd 100644 --- a/app/modules/pmf/models_pmf.py +++ b/app/modules/pmf/models_pmf.py @@ -46,6 +46,8 @@ class PmfOffer(Base): end_date: Mapped[date] duration: Mapped[int] # days + created_at: Mapped[date] = mapped_column(insert_default=date.today) + tags: Mapped[list["OfferTags"]] = relationship( "Tags", back_populates="offers", @@ -61,7 +63,7 @@ class Tags(Base): id: Mapped[PrimaryKey] tag: Mapped[str] - created_at: Mapped[date] = mapped_column(default=date.today) + created_at: Mapped[date] = mapped_column(insert_default=date.today) offers: Mapped[list["OfferTags"]] = relationship( "PmfOffer", diff --git a/tests/modules/test_pmf.py b/tests/modules/test_pmf.py new file mode 100644 index 0000000000..dffba3a5fc --- /dev/null +++ b/tests/modules/test_pmf.py @@ -0,0 +1,163 @@ +import uuid +from datetime import date + +import pytest +import pytest_asyncio +from fastapi.testclient import TestClient + +from app.core.groups.groups_type import AccountType, GroupType +from app.core.users import models_users +from app.modules.pmf import models_pmf, schemas_pmf + +# We need to import event_loop for pytest-asyncio routine defined bellow +from app.modules.pmf.types_pmf import LocationType, OfferType +from tests.commons import ( + add_object_to_db, + create_api_access_token, + create_user_with_groups, +) + +not_alumni_user: models_users.CoreUser +student_user: models_users.CoreUser +alumni_user: models_users.CoreUser + +tag1_id = uuid.UUID("0b7dc7bf-0ab4-421a-bbe7-7ec064fcec8d") +tag2_id = uuid.UUID("1c8dc7bf-1ab4-421a-bbe7-7ec064fcec8d") +tag_fake_id = uuid.UUID("5e8ec7bf-4ab4-421a-bbe7-7ec064fcec8d") + +offer1_id = uuid.UUID("2b7dc7bf-2ab4-421a-bbe7-7ec064fcec8d") +offer2_id = uuid.UUID("3c8dc7bf-3ab4-421a-bbe7-7ec064fcec8d") +offer3_id = uuid.UUID("4d9ec7bf-4ab4-421a-bbe7-7ec064fcec8d") +offer_fake_id = uuid.UUID("5e9ec7bf-0ab4-421a-bbe7-7ec064fcec8d") + + +@pytest_asyncio.fixture(scope="module", autouse=True) +async def init_objects(): + global not_alumni_user, student_user, alumni_user + + # We create an user in the test database + not_alumni_user = await create_user_with_groups( + groups=[], + account_type=AccountType.external, + ) + student_user = await create_user_with_groups( + groups=[], + account_type=AccountType.student, + ) + alumni_user = await create_user_with_groups( + groups=[], + account_type=AccountType.former_student, + ) + + tag_aero = models_pmf.Tags( + tag="Aeronautics", + id=tag1_id, + created_at=date(2023, 5, 1), + ) + tag_ai = models_pmf.Tags( + tag="Artificial Intelligence", + id=tag2_id, + created_at=date(2023, 5, 1), + ) + await add_object_to_db(tag_aero) + await add_object_to_db(tag_ai) + + # Creat 5 offers + offer_1 = models_pmf.PmfOffer( + id=offer1_id, + author_id=alumni_user.id, + company_name="AeroCorp", + title="Aerospace Engineer Internship", + description="Join our team to work on cutting-edge aerospace projects.", + offer_type=OfferType.TFE, + location="Toulouse, France", + location_type=LocationType.On_site, + start_date=date(2023, 6, 1), + end_date=date(2023, 8, 31), + created_at=date(2023, 5, 1), + duration=92, + ) + await add_object_to_db(offer_1) + offer_tag = models_pmf.OfferTags(offer_id=offer_1.id, tag_id=tag_aero.id) + await add_object_to_db(offer_tag) + + offer_2 = models_pmf.PmfOffer( + id=offer2_id, + author_id=alumni_user.id, + company_name="TechAI", + title="AI Research Internship", + description="Work on innovative AI research projects with our expert team.", + offer_type=OfferType.S_APP, + location="Remote", + location_type=LocationType.Remote, + start_date=date(2023, 7, 1), + end_date=date(2023, 9, 30), + created_at=date(2023, 6, 1), + duration=92, + ) + await add_object_to_db(offer_2) + offer_tag = models_pmf.OfferTags(offer_id=offer_2.id, tag_id=tag_ai.id) + await add_object_to_db(offer_tag) + + # A 3rd offer with the two tags + offer_3 = models_pmf.PmfOffer( + id=offer3_id, + author_id=alumni_user.id, + company_name="RoboAero", + title="Robotics and Aerospace Internship", + description="Combine robotics and aerospace in this exciting internship.", + offer_type=OfferType.TFE, + location="Hybrid - Paris, France / Remote", + location_type=LocationType.Hybrid, + start_date=date(2023, 8, 1), + end_date=date(2023, 10, 31), + created_at=date(2023, 7, 1), + duration=92, + ) + await add_object_to_db(offer_3) + offer_tag1 = models_pmf.OfferTags(offer_id=offer_3.id, tag_id=tag_aero.id) + offer_tag2 = models_pmf.OfferTags(offer_id=offer_3.id, tag_id=tag_ai.id) + await add_object_to_db(offer_tag1) + await add_object_to_db(offer_tag2) + + +@pytest.mark.parametrize( + ("offer_id", "expected_code"), + [ + (offer1_id, 200), + (offer2_id, 200), + (offer3_id, 200), + (offer_fake_id, 404), + ], +) +def test_get_offer(offer_id: uuid.UUID, expected_code: int, client: TestClient): + response = client.get( + f"/offers/{offer_id}", + ) + assert response.status_code == expected_code + + +@pytest.mark.parametrize( + ("query", "expected_code", "expected_length"), + [ + ("", 200, 3), + (f"?tag={tag1_id}", 200, 2), + (f"?tag={tag2_id}", 200, 2), + (f"?tag={tag1_id}&tag={tag2_id}", 200, 1), + (f"?tag={tag_fake_id}", 200, 0), + (f"?offer_type={OfferType.TFE}", 200, 2), + (f"?offer_type={OfferType.S_APP}", 200, 1), + (f"?location_type={LocationType.On_site}", 200, 1), + (f"?location_type={LocationType.Remote}", 200, 1), + ("?location_type=Fake", 200, 1), + (f"?tag={tag1_id}&offer_type={OfferType.TFE}", 200, 2), + ], +) +def test_get_offers( + query: uuid.UUID, expected_code: int, expected_length: int, client: TestClient +): + response = client.get( + f"/offers{query}", + ) + assert response.status_code == expected_code + assert len(response.json()) == expected_length From 075fd91a4d0886bdfab3f992b09ba5196a68de12 Mon Sep 17 00:00:00 2001 From: Warix <39554785+warix8@users.noreply.github.com> Date: Sat, 22 Nov 2025 19:54:08 +0100 Subject: [PATCH 06/10] added migrations --- migrations/versions/45-pmf.py | 88 +++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 migrations/versions/45-pmf.py diff --git a/migrations/versions/45-pmf.py b/migrations/versions/45-pmf.py new file mode 100644 index 0000000000..f54fac3d5c --- /dev/null +++ b/migrations/versions/45-pmf.py @@ -0,0 +1,88 @@ +"""PMF + +Create Date: 2025-11-22 19:49:38.136247 +""" + +from collections.abc import Sequence +from typing import TYPE_CHECKING, Union + +if TYPE_CHECKING: + from pytest_alembic import MigrationContext + +import sqlalchemy as sa +from alembic import op + +from app.types.sqlalchemy import TZDateTime + +# revision identifiers, used by Alembic. +revision: str = "ca5a9c9c64e5" +down_revision: str | None = "91fadc90f892" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "pmf_tags", + sa.Column("id", sa.Uuid(), nullable=False), + sa.Column("tag", sa.String(), nullable=False), + sa.Column("created_at", sa.Date(), nullable=False), + sa.PrimaryKeyConstraint("id"), + ) + op.create_table( + "pmf_offers", + sa.Column("id", sa.Uuid(), nullable=False), + sa.Column("author_id", sa.String(), nullable=False), + sa.Column("company_name", sa.String(), nullable=False), + sa.Column("title", sa.String(), nullable=False), + sa.Column("description", sa.String(), nullable=False), + sa.Column( + "offer_type", + sa.Enum("TFE", "S_APP", "EXE", "CDI", "CDD", name="offertype"), + nullable=False, + ), + sa.Column("location", sa.String(), nullable=False), + sa.Column( + "location_type", + sa.Enum("On_site", "Hybrid", "Remote", name="locationtype"), + nullable=False, + ), + sa.Column("start_date", sa.Date(), nullable=False), + sa.Column("end_date", sa.Date(), nullable=False), + sa.Column("duration", sa.Integer(), nullable=False), + sa.Column("created_at", sa.Date(), nullable=False), + sa.ForeignKeyConstraint(["author_id"], ["core_user.id"]), + sa.PrimaryKeyConstraint("id"), + ) + op.create_table( + "pmf_offer_tags", + sa.Column("offer_id", sa.Uuid(), nullable=False), + sa.Column("tag_id", sa.Uuid(), nullable=False), + sa.ForeignKeyConstraint(["offer_id"], ["pmf_offers.id"]), + sa.ForeignKeyConstraint(["tag_id"], ["pmf_tags.id"]), + sa.PrimaryKeyConstraint("offer_id", "tag_id"), + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table("pmf_offer_tags") + op.drop_table("pmf_offers") + op.drop_table("pmf_tags") + # ### end Alembic commands ### + + +def pre_test_upgrade( + alembic_runner: "MigrationContext", + alembic_connection: sa.Connection, +) -> None: + pass + + +def test_upgrade( + alembic_runner: "MigrationContext", + alembic_connection: sa.Connection, +) -> None: + pass From 0380fdd63942fc9c1ce34e7fddf632afe4f0cccf Mon Sep 17 00:00:00 2001 From: Warix <39554785+warix8@users.noreply.github.com> Date: Sat, 22 Nov 2025 21:23:51 +0100 Subject: [PATCH 07/10] Fixed get tests --- app/modules/pmf/cruds_pmf.py | 12 ++++++- app/modules/pmf/endpoints_pmf.py | 5 ++- app/modules/pmf/models_pmf.py | 4 +-- app/modules/pmf/schemas_pmf.py | 11 +++--- tests/modules/test_pmf.py | 58 ++++++++++++++++++++------------ 5 files changed, 60 insertions(+), 30 deletions(-) diff --git a/app/modules/pmf/cruds_pmf.py b/app/modules/pmf/cruds_pmf.py index c6435aed43..7600bb8b5d 100644 --- a/app/modules/pmf/cruds_pmf.py +++ b/app/modules/pmf/cruds_pmf.py @@ -1,3 +1,4 @@ +from datetime import date from uuid import UUID from sqlalchemy import delete, select, true, update @@ -22,6 +23,7 @@ async def create_offer(offer: schemas_pmf.OfferSimple, db: AsyncSession) -> None start_date=offer.start_date, end_date=offer.end_date, duration=offer.duration, + created_at=date.today(), tags=[], ), ) @@ -98,7 +100,14 @@ async def get_offers( end_date=offer.end_date, duration=offer.duration, author=schemas_users.CoreUserSimple.model_validate(offer.author), - tags=[schemas_pmf.TagComplete.model_validate(tag) for tag in offer.tags], + tags=[ + schemas_pmf.TagComplete( + id=tag.id, + tag=tag.tag, + created_at=tag.created_at, + ) + for tag in offer.tags + ], ) for offer in offers.scalars().all() ] @@ -138,6 +147,7 @@ async def create_tag( tag_db = models_pmf.Tags( id=tag.id, tag=tag.tag, + created_at=tag.created_at, ) db.add(tag_db) diff --git a/app/modules/pmf/endpoints_pmf.py b/app/modules/pmf/endpoints_pmf.py index db1c439c3e..cfb69f2714 100644 --- a/app/modules/pmf/endpoints_pmf.py +++ b/app/modules/pmf/endpoints_pmf.py @@ -1,4 +1,5 @@ import uuid +from datetime import date from uuid import UUID from fastapi import APIRouter, Depends, HTTPException, Query @@ -16,6 +17,7 @@ module = Module( root="pmf", tag="Pmf", + router=router, default_allowed_account_types=[ AccountType.student, AccountType.staff, @@ -41,7 +43,7 @@ async def get_offer( db=db, ) - if not offer: + if offer is None: raise HTTPException(status_code=404, detail="Offer not found") return offer @@ -221,6 +223,7 @@ async def create_tag( tag_db = schemas_pmf.TagComplete( **tag.model_dump(), id=uuid.uuid4(), + created_at=date.today(), ) await cruds_pmf.create_tag(tag=tag_db, db=db) return tag_db diff --git a/app/modules/pmf/models_pmf.py b/app/modules/pmf/models_pmf.py index 7a27e48efd..0861fef665 100644 --- a/app/modules/pmf/models_pmf.py +++ b/app/modules/pmf/models_pmf.py @@ -48,7 +48,7 @@ class PmfOffer(Base): created_at: Mapped[date] = mapped_column(insert_default=date.today) - tags: Mapped[list["OfferTags"]] = relationship( + tags: Mapped[list["Tags"]] = relationship( "Tags", back_populates="offers", lazy="selectin", # Small collection @@ -65,7 +65,7 @@ class Tags(Base): created_at: Mapped[date] = mapped_column(insert_default=date.today) - offers: Mapped[list["OfferTags"]] = relationship( + offers: Mapped[list["PmfOffer"]] = relationship( "PmfOffer", back_populates="tags", secondary="pmf_offer_tags", diff --git a/app/modules/pmf/schemas_pmf.py b/app/modules/pmf/schemas_pmf.py index 338e372f6d..8a242cb4ba 100644 --- a/app/modules/pmf/schemas_pmf.py +++ b/app/modules/pmf/schemas_pmf.py @@ -1,4 +1,4 @@ -from datetime import datetime +from datetime import date from uuid import UUID from pydantic import BaseModel @@ -13,6 +13,7 @@ class TagBase(BaseModel): class TagComplete(TagBase): id: UUID + created_at: date class OfferBase(BaseModel): @@ -25,8 +26,8 @@ class OfferBase(BaseModel): location: str location_type: LocationType - start_date: datetime - end_date: datetime + start_date: date + end_date: date duration: int # days @@ -42,8 +43,8 @@ class OfferUpdate(BaseModel): offer_type: OfferType | None = None location: str | None = None location_type: LocationType | None = None - start_date: datetime | None = None - end_date: datetime | None = None + start_date: date | None = None + end_date: date | None = None duration: int | None = None # days tags: list[TagBase] | None = None diff --git a/tests/modules/test_pmf.py b/tests/modules/test_pmf.py index dffba3a5fc..97f61c21a4 100644 --- a/tests/modules/test_pmf.py +++ b/tests/modules/test_pmf.py @@ -22,7 +22,9 @@ alumni_user: models_users.CoreUser tag1_id = uuid.UUID("0b7dc7bf-0ab4-421a-bbe7-7ec064fcec8d") +tag1: models_pmf.Tags tag2_id = uuid.UUID("1c8dc7bf-1ab4-421a-bbe7-7ec064fcec8d") +tag2: models_pmf.Tags tag_fake_id = uuid.UUID("5e8ec7bf-4ab4-421a-bbe7-7ec064fcec8d") offer1_id = uuid.UUID("2b7dc7bf-2ab4-421a-bbe7-7ec064fcec8d") @@ -30,6 +32,9 @@ offer3_id = uuid.UUID("4d9ec7bf-4ab4-421a-bbe7-7ec064fcec8d") offer_fake_id = uuid.UUID("5e9ec7bf-0ab4-421a-bbe7-7ec064fcec8d") +not_alumni_token: str +student_token: str +alumni_token: str @pytest_asyncio.fixture(scope="module", autouse=True) async def init_objects(): @@ -49,18 +54,24 @@ async def init_objects(): account_type=AccountType.former_student, ) - tag_aero = models_pmf.Tags( + global not_alumni_token, student_token, alumni_token + not_alumni_token = create_api_access_token(not_alumni_user) + student_token = create_api_access_token(student_user) + alumni_token = create_api_access_token(alumni_user) + + global tag1, tag2 + tag1 = models_pmf.Tags( tag="Aeronautics", id=tag1_id, created_at=date(2023, 5, 1), ) - tag_ai = models_pmf.Tags( + tag2 = models_pmf.Tags( tag="Artificial Intelligence", id=tag2_id, created_at=date(2023, 5, 1), ) - await add_object_to_db(tag_aero) - await add_object_to_db(tag_ai) + await add_object_to_db(tag1) + await add_object_to_db(tag2) # Creat 5 offers offer_1 = models_pmf.PmfOffer( @@ -78,7 +89,7 @@ async def init_objects(): duration=92, ) await add_object_to_db(offer_1) - offer_tag = models_pmf.OfferTags(offer_id=offer_1.id, tag_id=tag_aero.id) + offer_tag = models_pmf.OfferTags(offer_id=offer_1.id, tag_id=tag1.id) await add_object_to_db(offer_tag) offer_2 = models_pmf.PmfOffer( @@ -96,7 +107,7 @@ async def init_objects(): duration=92, ) await add_object_to_db(offer_2) - offer_tag = models_pmf.OfferTags(offer_id=offer_2.id, tag_id=tag_ai.id) + offer_tag = models_pmf.OfferTags(offer_id=offer_2.id, tag_id=tag2.id) await add_object_to_db(offer_tag) # A 3rd offer with the two tags @@ -115,8 +126,8 @@ async def init_objects(): duration=92, ) await add_object_to_db(offer_3) - offer_tag1 = models_pmf.OfferTags(offer_id=offer_3.id, tag_id=tag_aero.id) - offer_tag2 = models_pmf.OfferTags(offer_id=offer_3.id, tag_id=tag_ai.id) + offer_tag1 = models_pmf.OfferTags(offer_id=offer_3.id, tag_id=tag1.id) + offer_tag2 = models_pmf.OfferTags(offer_id=offer_3.id, tag_id=tag2.id) await add_object_to_db(offer_tag1) await add_object_to_db(offer_tag2) @@ -132,7 +143,8 @@ async def init_objects(): ) def test_get_offer(offer_id: uuid.UUID, expected_code: int, client: TestClient): response = client.get( - f"/offers/{offer_id}", + f"/pmf/offers/{offer_id}", + headers={"Authorization": f"Bearer {student_token}"}, ) assert response.status_code == expected_code @@ -141,23 +153,27 @@ def test_get_offer(offer_id: uuid.UUID, expected_code: int, client: TestClient): ("query", "expected_code", "expected_length"), [ ("", 200, 3), - (f"?tag={tag1_id}", 200, 2), - (f"?tag={tag2_id}", 200, 2), - (f"?tag={tag1_id}&tag={tag2_id}", 200, 1), - (f"?tag={tag_fake_id}", 200, 0), - (f"?offer_type={OfferType.TFE}", 200, 2), - (f"?offer_type={OfferType.S_APP}", 200, 1), - (f"?location_type={LocationType.On_site}", 200, 1), - (f"?location_type={LocationType.Remote}", 200, 1), - ("?location_type=Fake", 200, 1), - (f"?tag={tag1_id}&offer_type={OfferType.TFE}", 200, 2), + ("?includedTags=Aeronautics", 200, 2), + ("?includedTags=Artificial+Intelligence", 200, 2), + ("?includedTags=Aeronautics&includedTags=Artificial+Intelligence", 200, 3), + ("?includedTags=Fake", 200, 0), + (f"?includedOfferTypes={OfferType.TFE.value}", 200, 2), + (f"?includedOfferTypes={OfferType.S_APP.value}", 200, 1), + (f"?includedLocationTypes={LocationType.On_site.value}", 200, 1), + (f"?includedLocationTypes={LocationType.Remote.value}", 200, 1), + ("?includedLocationTypes=FakeLocation", 422, 0), + (f"?includedTags=Aeronautics&includedOfferTypes={OfferType.TFE.value}", 200, 2), + ("?limit=2", 200, 2), + ("?offset=1", 200, 2), + ("?limit=2&offset=2", 200, 1), ], ) def test_get_offers( query: uuid.UUID, expected_code: int, expected_length: int, client: TestClient ): response = client.get( - f"/offers{query}", + f"/pmf/offers/{query}", ) assert response.status_code == expected_code - assert len(response.json()) == expected_length + if expected_code == 200: + assert len(response.json()) == expected_length \ No newline at end of file From 5b53042748269c4f68438bf47ec86949e870d642 Mon Sep 17 00:00:00 2001 From: Warix <39554785+warix8@users.noreply.github.com> Date: Sat, 22 Nov 2025 23:58:51 +0100 Subject: [PATCH 08/10] Working tests --- app/modules/pmf/cruds_pmf.py | 25 ++- app/modules/pmf/endpoints_pmf.py | 22 +- tests/modules/test_pmf.py | 341 ++++++++++++++++++++++++++++++- 3 files changed, 370 insertions(+), 18 deletions(-) diff --git a/app/modules/pmf/cruds_pmf.py b/app/modules/pmf/cruds_pmf.py index 7600bb8b5d..8741edeeb1 100644 --- a/app/modules/pmf/cruds_pmf.py +++ b/app/modules/pmf/cruds_pmf.py @@ -42,6 +42,10 @@ async def update_offer( async def delete_offer(offer_id: UUID, db: AsyncSession) -> None: + # First, delete associations in pmf_offer_tags + await db.execute( + delete(models_pmf.OfferTags).where(models_pmf.OfferTags.offer_id == offer_id), + ) await db.execute( delete(models_pmf.PmfOffer).where(models_pmf.PmfOffer.id == offer_id), ) @@ -117,27 +121,34 @@ async def get_all_tags(db: AsyncSession) -> list[schemas_pmf.TagComplete]: tags = await db.execute( select(models_pmf.Tags).distinct(models_pmf.Tags.tag), ) - return [schemas_pmf.TagComplete.model_validate(tag) for tag in tags.scalars().all()] + return [ + schemas_pmf.TagComplete( + id=tag.id, + tag=tag.tag, + created_at=tag.created_at, + ) + for tag in tags.scalars().all() + ] async def get_tag_by_name( tag_name: str, db: AsyncSession, -) -> schemas_pmf.TagComplete | None: +) -> models_pmf.Tags | None: result = await db.execute( select(models_pmf.Tags).where(models_pmf.Tags.tag == tag_name), ) - return schemas_pmf.TagComplete.model_validate(result.scalars().first()) + return result.scalars().first() async def get_tag_by_id( tag_id: UUID, db: AsyncSession, -) -> schemas_pmf.TagComplete | None: +) -> models_pmf.Tags | None: result = await db.execute( select(models_pmf.Tags).where(models_pmf.Tags.id == tag_id), ) - return schemas_pmf.TagComplete.model_validate(result.scalars().first()) + return result.scalars().first() async def create_tag( @@ -168,6 +179,10 @@ async def delete_tag( tag_id: UUID, db: AsyncSession, ) -> None: + # First, delete associations in pmf_offer_tags + await db.execute( + delete(models_pmf.OfferTags).where(models_pmf.OfferTags.tag_id == tag_id), + ) await db.execute( delete(models_pmf.Tags).where(models_pmf.Tags.id == tag_id), ) diff --git a/app/modules/pmf/endpoints_pmf.py b/app/modules/pmf/endpoints_pmf.py index cfb69f2714..c8c8dc30f5 100644 --- a/app/modules/pmf/endpoints_pmf.py +++ b/app/modules/pmf/endpoints_pmf.py @@ -76,7 +76,7 @@ async def get_offers( @router.post( "/pmf/offer/", - response_model=list[schemas_pmf.OfferComplete], + response_model=schemas_pmf.OfferComplete, status_code=200, ) async def create_offer( @@ -102,9 +102,10 @@ async def create_offer( offer_db = schemas_pmf.OfferSimple( **offer.model_dump(), id=uuid.uuid4(), - author_id=user.id, ) - return await cruds_pmf.create_offer(db=db, offer=offer_db) + await cruds_pmf.create_offer(db=db, offer=offer_db) + await db.flush() + return await cruds_pmf.get_offer_by_id(offer_id=offer_db.id, db=db) @router.put( @@ -195,12 +196,15 @@ async def get_all_tags( async def get_tag( tag_id: UUID, db: AsyncSession = Depends(get_db), -) -> schemas_pmf.TagComplete | None: - tags = await cruds_pmf.get_all_tags(db=db) - for tag in tags: - if tag.id == tag_id: - return tag - return None +) -> schemas_pmf.TagComplete: + tag = await cruds_pmf.get_tag_by_id(tag_id=tag_id, db=db) + if not tag: + raise HTTPException(status_code=404, detail="Tag not found") + return schemas_pmf.TagComplete( + tag=tag.tag, + id=tag.id, + created_at=tag.created_at, + ) @router.post( diff --git a/tests/modules/test_pmf.py b/tests/modules/test_pmf.py index 97f61c21a4..2b1bf2b580 100644 --- a/tests/modules/test_pmf.py +++ b/tests/modules/test_pmf.py @@ -20,6 +20,7 @@ not_alumni_user: models_users.CoreUser student_user: models_users.CoreUser alumni_user: models_users.CoreUser +admin_user: models_users.CoreUser tag1_id = uuid.UUID("0b7dc7bf-0ab4-421a-bbe7-7ec064fcec8d") tag1: models_pmf.Tags @@ -35,10 +36,12 @@ not_alumni_token: str student_token: str alumni_token: str +admin_token: str + @pytest_asyncio.fixture(scope="module", autouse=True) async def init_objects(): - global not_alumni_user, student_user, alumni_user + global not_alumni_user, student_user, alumni_user, admin_user # We create an user in the test database not_alumni_user = await create_user_with_groups( @@ -53,11 +56,16 @@ async def init_objects(): groups=[], account_type=AccountType.former_student, ) + admin_user = await create_user_with_groups( + groups=[GroupType.admin], + account_type=AccountType.former_student, + ) - global not_alumni_token, student_token, alumni_token + global not_alumni_token, student_token, alumni_token, admin_token not_alumni_token = create_api_access_token(not_alumni_user) student_token = create_api_access_token(student_user) alumni_token = create_api_access_token(alumni_user) + admin_token = create_api_access_token(admin_user) global tag1, tag2 tag1 = models_pmf.Tags( @@ -169,11 +177,336 @@ def test_get_offer(offer_id: uuid.UUID, expected_code: int, client: TestClient): ], ) def test_get_offers( - query: uuid.UUID, expected_code: int, expected_length: int, client: TestClient + query: uuid.UUID, + expected_code: int, + expected_length: int, + client: TestClient, ): response = client.get( f"/pmf/offers/{query}", ) assert response.status_code == expected_code if expected_code == 200: - assert len(response.json()) == expected_length \ No newline at end of file + assert len(response.json()) == expected_length + + +# Tests for POST, PUT, DELETE offers +@pytest.mark.parametrize( + ("token", "author_id", "expected_code"), + [ + ("alumni_token", "alumni_user", 200), # Alumni can create offer for themselves + ("admin_token", "alumni_user", 200), # Admin can create offer for others + ("student_token", "student_user", 403), # Student cannot create offers + ( + "not_alumni_token", + "not_alumni_user", + 403, + ), # External user cannot create offers + ( + "alumni_token", + "admin_user", + 403, + ), # Alumni cannot create offer for others (non-admin) + ], +) +def test_create_offer( + token: str, + author_id: str, + expected_code: int, + client: TestClient, +): + # Get the actual token and user id + actual_token = globals()[token] + actual_author_id = globals()[author_id].id + + offer_data = { + "author_id": actual_author_id, + "company_name": "Test Company", + "title": "Test Position", + "description": "This is a test offer description", + "offer_type": OfferType.TFE.value, + "location": "Test City", + "location_type": LocationType.On_site.value, + "start_date": "2024-01-01", + "end_date": "2024-06-30", + "duration": 181, + } + + response = client.post( + "/pmf/offer/", + json=offer_data, + headers={"Authorization": f"Bearer {actual_token}"}, + ) + assert response.status_code == expected_code + + +def test_update_offer_success(client: TestClient): + """Test successful offer update by the author""" + offer_update = { + "title": "Updated Title", + "description": "Updated description", + "company_name": "Updated Company", + } + + response = client.put( + f"/pmf/offer/{offer1_id}", + json=offer_update, + headers={"Authorization": f"Bearer {alumni_token}"}, + ) + assert response.status_code == 204 + + +def test_update_offer_by_admin(client: TestClient): + """Test successful offer update by admin""" + offer_update = { + "title": "Admin Updated Title", + } + + response = client.put( + f"/pmf/offer/{offer2_id}", + json=offer_update, + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert response.status_code == 204 + + +def test_update_offer_forbidden(client: TestClient): + """Test forbidden offer update by non-author""" + offer_update = { + "title": "Unauthorized Update", + } + + response = client.put( + f"/pmf/offer/{offer1_id}", + json=offer_update, + headers={"Authorization": f"Bearer {student_token}"}, + ) + assert response.status_code == 403 + + +def test_update_nonexistent_offer(client: TestClient): + """Test update of non-existent offer""" + offer_update = { + "title": "Updated Title", + } + + response = client.put( + f"/pmf/offer/{offer_fake_id}", + json=offer_update, + headers={"Authorization": f"Bearer {alumni_token}"}, + ) + assert response.status_code == 404 + + +def test_delete_offer_success(client: TestClient): + """Test successful offer deletion by the author""" + response = client.delete( + f"/pmf/offer/{offer3_id}", + headers={"Authorization": f"Bearer {alumni_token}"}, + ) + assert response.status_code == 204 + + +def test_delete_offer_by_admin(client: TestClient): + """Test successful offer deletion by admin""" + # First create a new offer to delete + offer_data = { + "author_id": alumni_user.id, + "company_name": "Delete Test Company", + "title": "Delete Test Position", + "description": "This offer will be deleted by admin", + "offer_type": OfferType.S_APP.value, + "location": "Delete Test City", + "location_type": LocationType.Remote.value, + "start_date": "2024-01-01", + "end_date": "2024-06-30", + "duration": 181, + } + + create_response = client.post( + "/pmf/offer/", + json=offer_data, + headers={"Authorization": f"Bearer {alumni_token}"}, + ) + assert create_response.status_code == 200 + created_offer_id = create_response.json()["id"] + + # Now delete it as admin + response = client.delete( + f"/pmf/offer/{created_offer_id}", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert response.status_code == 204 + + +def test_delete_offer_forbidden(client: TestClient): + """Test forbidden offer deletion by non-author""" + response = client.delete( + f"/pmf/offer/{offer2_id}", + headers={"Authorization": f"Bearer {student_token}"}, + ) + assert response.status_code == 403 + + +def test_delete_nonexistent_offer(client: TestClient): + """Test deletion of non-existent offer""" + response = client.delete( + f"/pmf/offer/{offer_fake_id}", + headers={"Authorization": f"Bearer {alumni_token}"}, + ) + assert response.status_code == 404 + + +# Tests for tags endpoints +def test_get_all_tags(client: TestClient): + """Test getting all tags""" + response = client.get("/pmf/tags/") + assert response.status_code == 200 + tags = response.json() + assert len(tags) >= 2 # We have at least the 2 created tags + assert any(tag["tag"] == "Aeronautics" for tag in tags) + assert any(tag["tag"] == "Artificial Intelligence" for tag in tags) + + +def test_get_tag_by_id(client: TestClient): + """Test getting a specific tag by ID""" + response = client.get(f"/pmf/tag/{tag1_id}") + assert response.status_code == 200 + tag = response.json() + assert tag["id"] == str(tag1_id) + assert tag["tag"] == "Aeronautics" + + +def test_get_nonexistent_tag(client: TestClient): + """Test getting a non-existent tag""" + response = client.get(f"/pmf/tag/{tag_fake_id}") + assert response.status_code == 404 + + +def test_create_tag_success(client: TestClient): + """Test successful tag creation by admin""" + tag_data = { + "tag": "Machine Learning", + } + + response = client.post( + "/pmf/tag/", + json=tag_data, + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert response.status_code == 201 + created_tag = response.json() + assert created_tag["tag"] == "Machine Learning" + assert "id" in created_tag + assert "created_at" in created_tag + + +def test_create_tag_forbidden(client: TestClient): + """Test tag creation by non-admin user""" + tag_data = { + "tag": "Unauthorized Tag", + } + + response = client.post( + "/pmf/tag/", + json=tag_data, + headers={"Authorization": f"Bearer {alumni_token}"}, + ) + assert response.status_code == 403 + + +def test_create_duplicate_tag(client: TestClient): + """Test creating a duplicate tag""" + tag_data = { + "tag": "Aeronautics", # This tag already exists + } + + response = client.post( + "/pmf/tag/", + json=tag_data, + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert response.status_code == 400 + + +def test_update_tag_success(client: TestClient): + """Test successful tag update by admin""" + tag_update = { + "tag": "Updated Aeronautics", + } + + response = client.put( + f"/pmf/tag/{tag2_id}", + json=tag_update, + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert response.status_code == 204 + + +def test_update_tag_forbidden(client: TestClient): + """Test tag update by non-admin user""" + tag_update = { + "tag": "Unauthorized Update", + } + + response = client.put( + f"/pmf/tag/{tag1_id}", + json=tag_update, + headers={"Authorization": f"Bearer {alumni_token}"}, + ) + assert response.status_code == 403 + + +def test_update_nonexistent_tag(client: TestClient): + """Test update of non-existent tag""" + tag_update = { + "tag": "Updated Non-existent", + } + + response = client.put( + f"/pmf/tag/{tag_fake_id}", + json=tag_update, + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert response.status_code == 404 + + +def test_delete_tag_success(client: TestClient): + """Test successful tag deletion by admin""" + # First create a tag to delete + tag_data = { + "tag": "Tag to Delete", + } + + create_response = client.post( + "/pmf/tag/", + json=tag_data, + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert create_response.status_code == 201 + created_tag_id = create_response.json()["id"] + + # Now delete it + response = client.delete( + f"/pmf/tag/{created_tag_id}", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert response.status_code == 204 + + +def test_delete_tag_forbidden(client: TestClient): + """Test tag deletion by non-admin user""" + response = client.delete( + f"/pmf/tag/{tag1_id}", + headers={"Authorization": f"Bearer {alumni_token}"}, + ) + assert response.status_code == 403 + + +def test_delete_nonexistent_tag(client: TestClient): + """Test deletion of non-existent tag""" + response = client.delete( + f"/pmf/tag/{tag_fake_id}", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert response.status_code == 404 From f04598ac5c95cf29a40779ad87caa24397927a20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=BChmos?= Date: Thu, 18 Dec 2025 16:14:39 +0100 Subject: [PATCH 09/10] added factories and a basic provider --- app/modules/pmf/endpoints_pmf.py | 4 ++-- app/modules/pmf/factory_pmf.py | 40 ++++++++++++++++++++++++++++++++ app/modules/pmf/types_pmf.py | 2 +- app/utils/auth/providers.py | 6 +++++ migrations/versions/45-pmf.py | 2 +- tests/modules/test_pmf.py | 6 ++--- 6 files changed, 53 insertions(+), 7 deletions(-) create mode 100644 app/modules/pmf/factory_pmf.py diff --git a/app/modules/pmf/endpoints_pmf.py b/app/modules/pmf/endpoints_pmf.py index c8c8dc30f5..920397ac30 100644 --- a/app/modules/pmf/endpoints_pmf.py +++ b/app/modules/pmf/endpoints_pmf.py @@ -8,7 +8,7 @@ from app.core.groups.groups_type import AccountType, GroupType from app.core.users.models_users import CoreUser from app.dependencies import get_db, is_user, is_user_in -from app.modules.pmf import cruds_pmf, schemas_pmf, types_pmf +from app.modules.pmf import cruds_pmf, schemas_pmf, types_pmf, factory_pmf from app.types.module import Module from app.utils.tools import is_user_member_of_any_group @@ -23,7 +23,7 @@ AccountType.staff, AccountType.former_student, ], - factory=None, + factory=factory_pmf.PmfFactory, ) diff --git a/app/modules/pmf/factory_pmf.py b/app/modules/pmf/factory_pmf.py new file mode 100644 index 0000000000..65d0aabd9b --- /dev/null +++ b/app/modules/pmf/factory_pmf.py @@ -0,0 +1,40 @@ +from app.types.factory import Factory +from app.core.users.factory_users import CoreUsersFactory +from app.core.utils.config import Settings +from sqlalchemy.ext.asyncio import AsyncSession +from app.modules.pmf import cruds_pmf,models_pmf,types_pmf +import uuid +from datetime import date + +class PmfFactory(Factory): + depends_on = [CoreUsersFactory] + + @classmethod + async def create_offers(cls, db: AsyncSession): + await cruds_pmf.create_offer( + offer=models_pmf.PmfOffer( + id=uuid.uuid4(), + author_id=CoreUsersFactory.demo_users_id[0], + company_name="Centrale Innovation", + start_date=date(2025,12,1), + end_date=date(2026,4,17), + duration=0, + title="Stage", + description="Stageant", + location="Montcuq", + location_type=types_pmf.LocationType.On_site, + offer_type=types_pmf.OfferType.TFE, + created_at=date.today + ), + db=db, + ) + + + @classmethod + async def run(cls, db: AsyncSession, settings: Settings) -> None: + await cls.create_offers(db) + + @classmethod + async def should_run(cls, db: AsyncSession): + assos = await cruds_pmf.get_offers(db=db) + return len(assos) == 0 diff --git a/app/modules/pmf/types_pmf.py b/app/modules/pmf/types_pmf.py index cd9e8cc57f..816245b5d1 100644 --- a/app/modules/pmf/types_pmf.py +++ b/app/modules/pmf/types_pmf.py @@ -3,7 +3,7 @@ class OfferType(str, Enum): # for the T-shirt and the bike TFE = "TFE" - S_APP = "Stage_Application" + APP = "APP" EXE = "EXE" CDI = "CDI" CDD = "CDD" diff --git a/app/utils/auth/providers.py b/app/utils/auth/providers.py index 12c9ceb2f2..d93d4c94f1 100644 --- a/app/utils/auth/providers.py +++ b/app/utils/auth/providers.py @@ -354,6 +354,12 @@ class SiarnaqAuthClient(BaseAuthClient): allowed_account_types: list[AccountType] | None = None +class PMFAuthClient(BaseAuthClient): + allowed_scopes: set[ScopeType | str] = {ScopeType.API} + + allowed_account_types: list[AccountType] | None = None + + class OverleafAuthClient(BaseAuthClient): allowed_account_types: list[AccountType] | None = get_ecl_account_types() diff --git a/migrations/versions/45-pmf.py b/migrations/versions/45-pmf.py index f54fac3d5c..8064254135 100644 --- a/migrations/versions/45-pmf.py +++ b/migrations/versions/45-pmf.py @@ -39,7 +39,7 @@ def upgrade() -> None: sa.Column("description", sa.String(), nullable=False), sa.Column( "offer_type", - sa.Enum("TFE", "S_APP", "EXE", "CDI", "CDD", name="offertype"), + sa.Enum("TFE", "APP", "EXE", "CDI", "CDD", name="offertype"), nullable=False, ), sa.Column("location", sa.String(), nullable=False), diff --git a/tests/modules/test_pmf.py b/tests/modules/test_pmf.py index 2b1bf2b580..56ad020b0b 100644 --- a/tests/modules/test_pmf.py +++ b/tests/modules/test_pmf.py @@ -106,7 +106,7 @@ async def init_objects(): company_name="TechAI", title="AI Research Internship", description="Work on innovative AI research projects with our expert team.", - offer_type=OfferType.S_APP, + offer_type=OfferType.APP, location="Remote", location_type=LocationType.Remote, start_date=date(2023, 7, 1), @@ -166,7 +166,7 @@ def test_get_offer(offer_id: uuid.UUID, expected_code: int, client: TestClient): ("?includedTags=Aeronautics&includedTags=Artificial+Intelligence", 200, 3), ("?includedTags=Fake", 200, 0), (f"?includedOfferTypes={OfferType.TFE.value}", 200, 2), - (f"?includedOfferTypes={OfferType.S_APP.value}", 200, 1), + (f"?includedOfferTypes={OfferType.APP.value}", 200, 1), (f"?includedLocationTypes={LocationType.On_site.value}", 200, 1), (f"?includedLocationTypes={LocationType.Remote.value}", 200, 1), ("?includedLocationTypes=FakeLocation", 422, 0), @@ -315,7 +315,7 @@ def test_delete_offer_by_admin(client: TestClient): "company_name": "Delete Test Company", "title": "Delete Test Position", "description": "This offer will be deleted by admin", - "offer_type": OfferType.S_APP.value, + "offer_type": OfferType.APP.value, "location": "Delete Test City", "location_type": LocationType.Remote.value, "start_date": "2024-01-01", From 76f67396c7af987409207fe45f6ddde0b36707cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=BChmos?= Date: Tue, 6 Jan 2026 15:36:29 +0100 Subject: [PATCH 10/10] added more factories --- app/modules/pmf/factory_pmf.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/app/modules/pmf/factory_pmf.py b/app/modules/pmf/factory_pmf.py index 65d0aabd9b..a1654f560f 100644 --- a/app/modules/pmf/factory_pmf.py +++ b/app/modules/pmf/factory_pmf.py @@ -28,6 +28,23 @@ async def create_offers(cls, db: AsyncSession): ), db=db, ) + await cruds_pmf.create_offer( + offer=models_pmf.PmfOffer( + id=uuid.uuid4(), + author_id=CoreUsersFactory.demo_users_id[1], + company_name="EDF", + start_date=date(2025,12,12), + end_date=date(2026,6,5), + duration=0, + title="Ingeneirue", + description="elec", + location="Ecully", + location_type=types_pmf.LocationType.On_site, + offer_type=types_pmf.OfferType.CDI, + created_at=date.today + ), + db=db, + ) @classmethod