diff --git a/api/src/pcapi/core/educational/factories.py b/api/src/pcapi/core/educational/factories.py index b90a27bf74a..c3b610614e1 100644 --- a/api/src/pcapi/core/educational/factories.py +++ b/api/src/pcapi/core/educational/factories.py @@ -446,3 +446,34 @@ def create_collective_offer_by_status( return CollectiveOfferFactory(**kwargs) raise NotImplementedStatus(f"Factory for {status}") + + +def create_collective_offer_template_by_status( + status: CollectiveOfferDisplayedStatus, + **kwargs: typing.Any, +) -> models.CollectiveOfferTemplate: + match status.value: + case CollectiveOfferDisplayedStatus.ARCHIVED.value: + kwargs["dateArchived"] = datetime.datetime.utcnow() + return CollectiveOfferTemplateFactory(**kwargs) + + case CollectiveOfferDisplayedStatus.REJECTED.value: + kwargs["validation"] = OfferValidationStatus.REJECTED + return CollectiveOfferTemplateFactory(**kwargs) + + case CollectiveOfferDisplayedStatus.PENDING.value: + kwargs["validation"] = OfferValidationStatus.PENDING + return CollectiveOfferTemplateFactory(**kwargs) + + case CollectiveOfferDisplayedStatus.DRAFT.value: + kwargs["validation"] = OfferValidationStatus.DRAFT + return CollectiveOfferTemplateFactory(**kwargs) + + case CollectiveOfferDisplayedStatus.INACTIVE.value: + kwargs["isActive"] = False + return CollectiveOfferTemplateFactory(**kwargs) + + case CollectiveOfferDisplayedStatus.ACTIVE.value: + return CollectiveOfferTemplateFactory(**kwargs) + + raise NotImplementedStatus(f"Factory for {status}") diff --git a/api/src/pcapi/core/educational/models.py b/api/src/pcapi/core/educational/models.py index 6d98962bb03..82e6b9319ba 100644 --- a/api/src/pcapi/core/educational/models.py +++ b/api/src/pcapi/core/educational/models.py @@ -141,6 +141,15 @@ class CollectiveOfferAllowedAction(enum.Enum): CAN_ARCHIVE = "CAN_ARCHIVE" +class CollectiveOfferTemplateAllowedAction(enum.Enum): + CAN_EDIT_DETAILS = "CAN_EDIT_DETAILS" + CAN_DUPLICATE = "CAN_DUPLICATE" + CAN_ARCHIVE = "CAN_ARCHIVE" + CAN_CREATE_BOOKABLE_OFFER = "CAN_CREATE_BOOKABLE_OFFER" + CAN_PUBLISH = "CAN_PUBLISH" + CAN_HIDE = "CAN_HIDE" + + ALLOWED_ACTIONS_BY_DISPLAYED_STATUS: dict[CollectiveOfferDisplayedStatus, tuple[CollectiveOfferAllowedAction, ...]] = { CollectiveOfferDisplayedStatus.DRAFT: ( CollectiveOfferAllowedAction.CAN_EDIT_DETAILS, @@ -194,6 +203,38 @@ class CollectiveOfferAllowedAction(enum.Enum): CollectiveOfferDisplayedStatus.INACTIVE: (), } +TEMPLATE_ALLOWED_ACTIONS_BY_DISPLAYED_STATUS: dict[ + CollectiveOfferDisplayedStatus, tuple[CollectiveOfferTemplateAllowedAction, ...] +] = { + CollectiveOfferDisplayedStatus.DRAFT: ( + CollectiveOfferTemplateAllowedAction.CAN_EDIT_DETAILS, + CollectiveOfferTemplateAllowedAction.CAN_ARCHIVE, + ), + CollectiveOfferDisplayedStatus.PENDING: (CollectiveOfferTemplateAllowedAction.CAN_DUPLICATE,), + CollectiveOfferDisplayedStatus.ACTIVE: ( + CollectiveOfferTemplateAllowedAction.CAN_EDIT_DETAILS, + CollectiveOfferTemplateAllowedAction.CAN_DUPLICATE, + CollectiveOfferTemplateAllowedAction.CAN_ARCHIVE, + CollectiveOfferTemplateAllowedAction.CAN_CREATE_BOOKABLE_OFFER, + CollectiveOfferTemplateAllowedAction.CAN_HIDE, + ), + CollectiveOfferDisplayedStatus.REJECTED: ( + CollectiveOfferTemplateAllowedAction.CAN_DUPLICATE, + CollectiveOfferTemplateAllowedAction.CAN_ARCHIVE, + ), + CollectiveOfferDisplayedStatus.ARCHIVED: ( + CollectiveOfferTemplateAllowedAction.CAN_DUPLICATE, + CollectiveOfferTemplateAllowedAction.CAN_CREATE_BOOKABLE_OFFER, + ), + CollectiveOfferDisplayedStatus.INACTIVE: ( + CollectiveOfferTemplateAllowedAction.CAN_EDIT_DETAILS, + CollectiveOfferTemplateAllowedAction.CAN_DUPLICATE, + CollectiveOfferTemplateAllowedAction.CAN_ARCHIVE, + CollectiveOfferTemplateAllowedAction.CAN_CREATE_BOOKABLE_OFFER, + CollectiveOfferTemplateAllowedAction.CAN_PUBLISH, + ), +} + class EducationalBookingStatus(enum.Enum): REFUSED = "REFUSED" @@ -773,7 +814,7 @@ def displayedStatus(self) -> CollectiveOfferDisplayedStatus: return CollectiveOfferDisplayedStatus.ACTIVE @property - def allowed_actions(self) -> list[CollectiveOfferAllowedAction]: + def allowedActions(self) -> list[CollectiveOfferAllowedAction]: displayed_status = self.displayedStatus allowed_actions = ALLOWED_ACTIONS_BY_DISPLAYED_STATUS[displayed_status] @@ -1029,9 +1070,8 @@ def displayedStatus(self) -> CollectiveOfferDisplayedStatus: return CollectiveOfferDisplayedStatus.ACTIVE @property - def allowed_actions(self) -> None: - # TODO: this will be implemented once the actions are defined for an OfferTemplate - return None + def allowedActions(self) -> list[CollectiveOfferTemplateAllowedAction]: + return list(TEMPLATE_ALLOWED_ACTIONS_BY_DISPLAYED_STATUS[self.displayedStatus]) @property def start(self) -> datetime | None: diff --git a/api/src/pcapi/routes/serialization/collective_offers_serialize.py b/api/src/pcapi/routes/serialization/collective_offers_serialize.py index db7155734cc..add35ef5fa4 100644 --- a/api/src/pcapi/routes/serialization/collective_offers_serialize.py +++ b/api/src/pcapi/routes/serialization/collective_offers_serialize.py @@ -149,7 +149,10 @@ class CollectiveOfferResponseModel(BaseModel): venue: base_serializers.ListOffersVenueResponseModel status: CollectiveOfferStatus displayedStatus: educational_models.CollectiveOfferDisplayedStatus - allowedActions: list[educational_models.CollectiveOfferAllowedAction] | None + allowedActions: ( + list[educational_models.CollectiveOfferAllowedAction] + | list[educational_models.CollectiveOfferTemplateAllowedAction] + ) educationalInstitution: EducationalInstitutionResponseModel | None interventionArea: list[str] templateId: str | None @@ -206,7 +209,7 @@ def _serialize_offer_paginated( venue=_serialize_venue(offer.venue), # type: ignore[arg-type] status=offer.status.name, displayedStatus=offer.displayedStatus, - allowedActions=offer.allowed_actions, + allowedActions=offer.allowedActions, isShowcase=is_offer_template, educationalInstitution=EducationalInstitutionResponseModel.from_orm(institution) if institution else None, interventionArea=offer.interventionArea, @@ -380,6 +383,7 @@ class GetCollectiveOfferTemplateResponseModel(GetCollectiveOfferBaseResponseMode contactPhone: str | None contactUrl: str | None contactForm: educational_models.OfferContactFormEnum | None + allowedActions: list[educational_models.CollectiveOfferTemplateAllowedAction] class Config: orm_mode = True @@ -435,13 +439,12 @@ class GetCollectiveOfferResponseModel(GetCollectiveOfferBaseResponseModel): formats: typing.Sequence[subcategories.EacFormat] | None isTemplate: bool = False dates: TemplateDatesModel | None - allowedActions: list[educational_models.CollectiveOfferAllowedAction] | None + allowedActions: list[educational_models.CollectiveOfferAllowedAction] @classmethod def from_orm(cls, offer: educational_models.CollectiveOffer) -> "GetCollectiveOfferResponseModel": result = super().from_orm(offer) result.formats = offer.get_formats() - result.allowedActions = offer.allowed_actions if result.status == CollectiveOfferStatus.INACTIVE.name: result.isActive = False diff --git a/api/tests/core/educational/test_models.py b/api/tests/core/educational/test_models.py index 7c0b8d8feca..bb3d0713478 100644 --- a/api/tests/core/educational/test_models.py +++ b/api/tests/core/educational/test_models.py @@ -7,7 +7,6 @@ from pcapi.core.educational import exceptions from pcapi.core.educational import factories -from pcapi.core.educational.factories import create_collective_offer_by_status from pcapi.core.educational.models import ALLOWED_ACTIONS_BY_DISPLAYED_STATUS from pcapi.core.educational.models import CollectiveBookingStatus from pcapi.core.educational.models import CollectiveOffer @@ -16,6 +15,7 @@ from pcapi.core.educational.models import CollectiveStock from pcapi.core.educational.models import EducationalDeposit from pcapi.core.educational.models import HasImageMixin +from pcapi.core.educational.models import TEMPLATE_ALLOWED_ACTIONS_BY_DISPLAYED_STATUS import pcapi.core.offerers.factories as offerers_factories import pcapi.core.providers.factories as providers_factories from pcapi.models import db @@ -28,6 +28,15 @@ pytestmark = pytest.mark.usefixtures("db_session") +COLLECTIVE_OFFER_TEMPLATE_STATUS_LIST = [ + CollectiveOfferDisplayedStatus.ARCHIVED, + CollectiveOfferDisplayedStatus.REJECTED, + CollectiveOfferDisplayedStatus.PENDING, + CollectiveOfferDisplayedStatus.DRAFT, + CollectiveOfferDisplayedStatus.INACTIVE, + CollectiveOfferDisplayedStatus.ACTIVE, +] + class EducationalDepositTest: def test_should_raise_insufficient_fund(self) -> None: @@ -609,7 +618,7 @@ def test_unique_program_for_an_educational_institution(self): class CollectiveOfferDisplayedStatusTest: @pytest.mark.parametrize("status", CollectiveOfferDisplayedStatus) def test_get_offer_displayed_status(self, status): - offer = create_collective_offer_by_status(status) + offer = factories.create_collective_offer_by_status(status) assert offer.displayedStatus == status @@ -630,29 +639,24 @@ def test_get_displayed_status_for_inactive_offer_due_to_booking_date_passed(self class CollectiveOfferAllowedActionsTest: @pytest.mark.parametrize("status", CollectiveOfferDisplayedStatus) def test_get_offer_allowed_actions(self, status): - offer = create_collective_offer_by_status(status) + offer = factories.create_collective_offer_by_status(status) - assert offer.allowed_actions == list(ALLOWED_ACTIONS_BY_DISPLAYED_STATUS[status]) + assert offer.allowedActions == list(ALLOWED_ACTIONS_BY_DISPLAYED_STATUS[status]) def test_get_ended_offer_allowed_actions(self): - offer = create_collective_offer_by_status(CollectiveOfferDisplayedStatus.ENDED) + offer = factories.create_collective_offer_by_status(CollectiveOfferDisplayedStatus.ENDED) - assert offer.allowed_actions == [ + assert offer.allowedActions == [ CollectiveOfferAllowedAction.CAN_EDIT_DISCOUNT, CollectiveOfferAllowedAction.CAN_DUPLICATE, CollectiveOfferAllowedAction.CAN_CANCEL, ] offer.collectiveStock.endDatetime = datetime.datetime.utcnow() - datetime.timedelta(days=3) - assert offer.allowed_actions == [ + assert offer.allowedActions == [ CollectiveOfferAllowedAction.CAN_DUPLICATE, ] - def test_get_offer_template_allowed_actions(self): - offer = factories.CollectiveOfferTemplateFactory() - - assert offer.allowed_actions == None - def test_is_two_days_past_end(self): offer = factories.CollectiveOfferFactory() factories.CollectiveStockFactory(collectiveOffer=offer) @@ -665,3 +669,9 @@ def test_is_two_days_past_end(self): offer.collectiveStock.endDatetime = datetime.datetime.utcnow() - datetime.timedelta(days=3) assert offer.is_two_days_past_end + + @pytest.mark.parametrize("status", COLLECTIVE_OFFER_TEMPLATE_STATUS_LIST) + def test_get_offer_template_allowed_actions(self, status): + offer = factories.create_collective_offer_template_by_status(status) + + assert offer.allowedActions == list(TEMPLATE_ALLOWED_ACTIONS_BY_DISPLAYED_STATUS[status]) diff --git a/api/tests/routes/pro/get_collective_offer_template_test.py b/api/tests/routes/pro/get_collective_offer_template_test.py index 71633e575f7..6edda6859f9 100644 --- a/api/tests/routes/pro/get_collective_offer_template_test.py +++ b/api/tests/routes/pro/get_collective_offer_template_test.py @@ -53,6 +53,13 @@ def test_get_collective_offer_template(self, client): } assert response.json["formats"] == offer.formats assert response.json["displayedStatus"] == "ACTIVE" + assert response.json["allowedActions"] == [ + "CAN_EDIT_DETAILS", + "CAN_DUPLICATE", + "CAN_ARCHIVE", + "CAN_CREATE_BOOKABLE_OFFER", + "CAN_HIDE", + ] def test_performance(self, client): # Given diff --git a/pro/src/apiClient/v1/index.ts b/pro/src/apiClient/v1/index.ts index c4d472d5462..6de34cf2e26 100644 --- a/pro/src/apiClient/v1/index.ts +++ b/pro/src/apiClient/v1/index.ts @@ -53,6 +53,7 @@ export type { CollectiveOfferResponseModel } from './models/CollectiveOfferRespo export type { CollectiveOffersBookingResponseModel } from './models/CollectiveOffersBookingResponseModel'; export type { CollectiveOffersStockResponseModel } from './models/CollectiveOffersStockResponseModel'; export { CollectiveOfferStatus } from './models/CollectiveOfferStatus'; +export { CollectiveOfferTemplateAllowedAction } from './models/CollectiveOfferTemplateAllowedAction'; export type { CollectiveOfferTemplateBodyModel } from './models/CollectiveOfferTemplateBodyModel'; export type { CollectiveOfferTemplateResponseIdModel } from './models/CollectiveOfferTemplateResponseIdModel'; export { CollectiveOfferType } from './models/CollectiveOfferType'; diff --git a/pro/src/apiClient/v1/models/CollectiveOfferResponseModel.ts b/pro/src/apiClient/v1/models/CollectiveOfferResponseModel.ts index dba98381519..884930f356d 100644 --- a/pro/src/apiClient/v1/models/CollectiveOfferResponseModel.ts +++ b/pro/src/apiClient/v1/models/CollectiveOfferResponseModel.ts @@ -7,6 +7,7 @@ import type { CollectiveOfferDisplayedStatus } from './CollectiveOfferDisplayedS import type { CollectiveOffersBookingResponseModel } from './CollectiveOffersBookingResponseModel'; import type { CollectiveOffersStockResponseModel } from './CollectiveOffersStockResponseModel'; import type { CollectiveOfferStatus } from './CollectiveOfferStatus'; +import type { CollectiveOfferTemplateAllowedAction } from './CollectiveOfferTemplateAllowedAction'; import type { EacFormat } from './EacFormat'; import type { EducationalInstitutionResponseModel } from './EducationalInstitutionResponseModel'; import type { ListOffersVenueResponseModel } from './ListOffersVenueResponseModel'; @@ -14,7 +15,7 @@ import type { NationalProgramModel } from './NationalProgramModel'; import type { SubcategoryIdEnum } from './SubcategoryIdEnum'; import type { TemplateDatesModel } from './TemplateDatesModel'; export type CollectiveOfferResponseModel = { - allowedActions?: Array | null; + allowedActions: (Array | Array); booking?: CollectiveOffersBookingResponseModel | null; dates?: TemplateDatesModel | null; displayedStatus: CollectiveOfferDisplayedStatus; diff --git a/pro/src/apiClient/v1/models/CollectiveOfferTemplateAllowedAction.ts b/pro/src/apiClient/v1/models/CollectiveOfferTemplateAllowedAction.ts new file mode 100644 index 00000000000..d2a55e78ef8 --- /dev/null +++ b/pro/src/apiClient/v1/models/CollectiveOfferTemplateAllowedAction.ts @@ -0,0 +1,15 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +/** + * An enumeration. + */ +export enum CollectiveOfferTemplateAllowedAction { + CAN_EDIT_DETAILS = 'CAN_EDIT_DETAILS', + CAN_DUPLICATE = 'CAN_DUPLICATE', + CAN_ARCHIVE = 'CAN_ARCHIVE', + CAN_CREATE_BOOKABLE_OFFER = 'CAN_CREATE_BOOKABLE_OFFER', + CAN_PUBLISH = 'CAN_PUBLISH', + CAN_HIDE = 'CAN_HIDE', +} diff --git a/pro/src/apiClient/v1/models/GetCollectiveOfferResponseModel.ts b/pro/src/apiClient/v1/models/GetCollectiveOfferResponseModel.ts index 1776f092a8c..e9cd112236e 100644 --- a/pro/src/apiClient/v1/models/GetCollectiveOfferResponseModel.ts +++ b/pro/src/apiClient/v1/models/GetCollectiveOfferResponseModel.ts @@ -19,7 +19,7 @@ import type { StudentLevels } from './StudentLevels'; import type { SubcategoryIdEnum } from './SubcategoryIdEnum'; import type { TemplateDatesModel } from './TemplateDatesModel'; export type GetCollectiveOfferResponseModel = { - allowedActions?: Array | null; + allowedActions: Array; audioDisabilityCompliant?: boolean | null; bookingEmails: Array; collectiveStock?: GetCollectiveOfferCollectiveStockResponseModel | null; diff --git a/pro/src/apiClient/v1/models/GetCollectiveOfferTemplateResponseModel.ts b/pro/src/apiClient/v1/models/GetCollectiveOfferTemplateResponseModel.ts index ba0d4cbc29e..ad58230e4c3 100644 --- a/pro/src/apiClient/v1/models/GetCollectiveOfferTemplateResponseModel.ts +++ b/pro/src/apiClient/v1/models/GetCollectiveOfferTemplateResponseModel.ts @@ -5,6 +5,7 @@ import type { CollectiveOfferDisplayedStatus } from './CollectiveOfferDisplayedStatus'; import type { CollectiveOfferOfferVenueResponseModel } from './CollectiveOfferOfferVenueResponseModel'; import type { CollectiveOfferStatus } from './CollectiveOfferStatus'; +import type { CollectiveOfferTemplateAllowedAction } from './CollectiveOfferTemplateAllowedAction'; import type { EacFormat } from './EacFormat'; import type { GetCollectiveOfferVenueResponseModel } from './GetCollectiveOfferVenueResponseModel'; import type { NationalProgramModel } from './NationalProgramModel'; @@ -14,6 +15,7 @@ import type { StudentLevels } from './StudentLevels'; import type { SubcategoryIdEnum } from './SubcategoryIdEnum'; import type { TemplateDatesModel } from './TemplateDatesModel'; export type GetCollectiveOfferTemplateResponseModel = { + allowedActions: Array; audioDisabilityCompliant?: boolean | null; bookingEmails: Array; contactEmail?: string | null; diff --git a/pro/src/components/CollectiveOfferSummary/CollectiveOfferSummary.tsx b/pro/src/components/CollectiveOfferSummary/CollectiveOfferSummary.tsx index 30aa9e66b73..aabec3d7f34 100644 --- a/pro/src/components/CollectiveOfferSummary/CollectiveOfferSummary.tsx +++ b/pro/src/components/CollectiveOfferSummary/CollectiveOfferSummary.tsx @@ -67,9 +67,13 @@ export const CollectiveOfferSummary = ({ - {offer.isTemplate && } + {isCollectiveOfferTemplate(offer) && ( + + )} - {offer.isTemplate && } + {isCollectiveOfferTemplate(offer) && ( + + )} diff --git a/pro/src/screens/OfferEducational/useCollectiveOfferFromParams.tsx b/pro/src/screens/OfferEducational/useCollectiveOfferFromParams.tsx index d1d277dac3a..9f2643f86fb 100644 --- a/pro/src/screens/OfferEducational/useCollectiveOfferFromParams.tsx +++ b/pro/src/screens/OfferEducational/useCollectiveOfferFromParams.tsx @@ -42,7 +42,11 @@ export const useCollectiveOfferFromParams = ( const isTemplate = isTemplateId || pathNameIncludesTemplate - const { data: offer } = useSWR( + const { data: offer } = useSWR< + GetCollectiveOfferResponseModel | GetCollectiveOfferTemplateResponseModel, + any, + [string, number] | null + >( offerId !== undefined ? [ isTemplate diff --git a/pro/src/utils/collectiveApiFactories.ts b/pro/src/utils/collectiveApiFactories.ts index 0532dccb279..219a2bdb456 100644 --- a/pro/src/utils/collectiveApiFactories.ts +++ b/pro/src/utils/collectiveApiFactories.ts @@ -56,6 +56,7 @@ export const collectiveOfferFactory = ( isPublicApi: false, interventionArea: [], isShowcase: false, + allowedActions: [], ...customCollectiveOffer, } } @@ -116,6 +117,7 @@ export const getCollectiveOfferFactory = ( isVisibilityEditable: true, isTemplate: false, collectiveStock: getCollectiveOfferCollectiveStockFactory(), + allowedActions: [], ...customCollectiveOffer, } } @@ -148,6 +150,7 @@ export const getCollectiveOfferTemplateFactory = ( start: new Date().toISOString(), end: addDays(new Date(), 1).toISOString(), }, + allowedActions: [], ...customCollectiveOfferTemplate, })