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

(PC-31838)[API] feat: add allowed actions for collective offer templates #14242

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
31 changes: 31 additions & 0 deletions api/src/pcapi/core/educational/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}")
48 changes: 44 additions & 4 deletions api/src/pcapi/core/educational/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -194,6 +203,38 @@ class CollectiveOfferAllowedAction(enum.Enum):
CollectiveOfferDisplayedStatus.INACTIVE: (),
}

TEMPLATE_ALLOWED_ACTIONS_BY_DISPLAYED_STATUS: dict[
CollectiveOfferDisplayedStatus, tuple[CollectiveOfferTemplateAllowedAction, ...]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Les CollectiveOfferTemplateAllowedAction étant de taille variable une liste ou un set me semble plus approprié.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Un tuple a l'avantage de ne pas être mutable, comme ce sont des "constantes" ça me paraissait adapté

Dans ma 1è PR j'étais même tombé sur le piège : c'était une liste, je le récupère dans une méthode, je modifie la liste.. et boum c'est modifié pour tout le monde

] = {
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"
Expand Down Expand Up @@ -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]

Expand Down Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
34 changes: 22 additions & 12 deletions api/tests/core/educational/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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

Expand All @@ -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)
Expand All @@ -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):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Je ne vois pas vraiement l'intéret de ce test -> il me semble trop proche de l'implementation, le jour ou l'implementation change, ce test devra changer.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ça a le mérite d'appeler la méthode directement

Et comme ça boucle sur tous les statuts ça vérifie qu'on en a pas oublié un dans le dico

offer = factories.create_collective_offer_template_by_status(status)

assert offer.allowedActions == list(TEMPLATE_ALLOWED_ACTIONS_BY_DISPLAYED_STATUS[status])
7 changes: 7 additions & 0 deletions api/tests/routes/pro/get_collective_offer_template_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions pro/src/apiClient/v1/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
3 changes: 2 additions & 1 deletion pro/src/apiClient/v1/models/CollectiveOfferResponseModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@ 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';
import type { NationalProgramModel } from './NationalProgramModel';
import type { SubcategoryIdEnum } from './SubcategoryIdEnum';
import type { TemplateDatesModel } from './TemplateDatesModel';
export type CollectiveOfferResponseModel = {
allowedActions?: Array<CollectiveOfferAllowedAction> | null;
allowedActions: (Array<CollectiveOfferAllowedAction> | Array<CollectiveOfferTemplateAllowedAction>);
booking?: CollectiveOffersBookingResponseModel | null;
dates?: TemplateDatesModel | null;
displayedStatus: CollectiveOfferDisplayedStatus;
Expand Down
Original file line number Diff line number Diff line change
@@ -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',
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import type { StudentLevels } from './StudentLevels';
import type { SubcategoryIdEnum } from './SubcategoryIdEnum';
import type { TemplateDatesModel } from './TemplateDatesModel';
export type GetCollectiveOfferResponseModel = {
allowedActions?: Array<CollectiveOfferAllowedAction> | null;
allowedActions: Array<CollectiveOfferAllowedAction>;
audioDisabilityCompliant?: boolean | null;
bookingEmails: Array<string>;
collectiveStock?: GetCollectiveOfferCollectiveStockResponseModel | null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -14,6 +15,7 @@ import type { StudentLevels } from './StudentLevels';
import type { SubcategoryIdEnum } from './SubcategoryIdEnum';
import type { TemplateDatesModel } from './TemplateDatesModel';
export type GetCollectiveOfferTemplateResponseModel = {
allowedActions: Array<CollectiveOfferTemplateAllowedAction>;
audioDisabilityCompliant?: boolean | null;
bookingEmails: Array<string>;
contactEmail?: string | null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,13 @@ export const CollectiveOfferSummary = ({
<CollectiveOfferVenueSection venue={offer.venue} />
<CollectiveOfferTypeSection offer={offer} />
<CollectiveOfferImagePreview offer={offer} />
{offer.isTemplate && <CollectiveOfferDateSection offer={offer} />}
{isCollectiveOfferTemplate(offer) && (
<CollectiveOfferDateSection offer={offer} />
)}
<CollectiveOfferLocationSection offer={offer} />
{offer.isTemplate && <CollectiveOfferPriceSection offer={offer} />}
{isCollectiveOfferTemplate(offer) && (
<CollectiveOfferPriceSection offer={offer} />
)}
<CollectiveOfferParticipantSection students={offer.students} />
<AccessibilitySummarySection
accessibleItem={offer}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { screen, waitForElementToBeRemoved } from '@testing-library/react'

import { api } from 'apiClient/api'
import {
GetCollectiveOfferResponseModel,
GetCollectiveOfferTemplateResponseModel,
OfferAddressType,
} from 'apiClient/v1'
Expand All @@ -17,7 +18,9 @@ import { renderWithProviders } from 'utils/renderWithProviders'
import { AdagePreviewLayout } from '../AdagePreviewLayout'

function renderAdagePreviewLayout(
offer: GetCollectiveOfferTemplateResponseModel = getCollectiveOfferTemplateFactory()
offer:
| GetCollectiveOfferTemplateResponseModel
| GetCollectiveOfferResponseModel = getCollectiveOfferTemplateFactory()
) {
renderWithProviders(
<AdageUserContextProvider adageUser={defaultAdageUser}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions pro/src/utils/collectiveApiFactories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export const collectiveOfferFactory = (
isPublicApi: false,
interventionArea: [],
isShowcase: false,
allowedActions: [],
...customCollectiveOffer,
}
}
Expand Down Expand Up @@ -116,6 +117,7 @@ export const getCollectiveOfferFactory = (
isVisibilityEditable: true,
isTemplate: false,
collectiveStock: getCollectiveOfferCollectiveStockFactory(),
allowedActions: [],
...customCollectiveOffer,
}
}
Expand Down Expand Up @@ -148,6 +150,7 @@ export const getCollectiveOfferTemplateFactory = (
start: new Date().toISOString(),
end: addDays(new Date(), 1).toISOString(),
},
allowedActions: [],
...customCollectiveOfferTemplate,
})

Expand Down
Loading