Skip to content

Commit

Permalink
(PC-33575)[API] feat: add addressId param in GET Events and GET Pro…
Browse files Browse the repository at this point in the history
…ducts API endpoints

drop required constraint on `venueId`
  • Loading branch information
tcoudray-pass committed Dec 16, 2024
1 parent 9866ee0 commit 1a44dee
Show file tree
Hide file tree
Showing 8 changed files with 191 additions and 77 deletions.
7 changes: 7 additions & 0 deletions api/documentation/src/pages/change-logs.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,17 @@ title: Pass Culture API change logs

## December 2024

### Individual offers endpoints

- You can fetch a Venue by SIRET using the [**Get Venue endpoint**](/rest-api#tag/Venues/operation/GetVenueBySiret)
- You can now filter events ([**Get Event Offers endpoint**](/rest-api#tag/Event-Offers/operation/GetEvents)) and products ([**Get Product Offers endpoint**](/rest-api#tag/Product-Offers/operation/GetProducts)) using the `addressId` parameter.

### Collective offers endpoints

- The `subcategoryId` field has been removed from collective offers. The attribute is not returned anymore in the response of the [**Get Collective Offer endpoint**](/rest-api#tag/Collective-Offers/operation/GetCollectiveOffer) and the [**Get Collective Offers endpoint**](/rest-api#tag/Collective-Offers/operation/GetCollectiveOffers)
- You must now only use the `formats` field (and not `subcategoryId`) to specify the educational format of your collective offer in the [**Create Collective Offer endpoint**](/rest-api#tag/Collective-Offers/operation/PostCollectiveOffer) and the [**Update Collective Offer endpoint**](/rest-api#tag/Collective-Offers/operation/PatchCollectiveOffer). The `formats` field is required when creating a collective offer.


## November 2024

- You can edit the name of an event using the [**Update Event Offer endpoint**](/rest-api#tag/Event-Offers/operation/EditEvent)
Expand Down
51 changes: 42 additions & 9 deletions api/documentation/static/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -4031,6 +4031,13 @@
},
"GetOffersQueryParams": {
"properties": {
"addressId": {
"description": "Address id in the pass Culture DB",
"example": 1,
"nullable": true,
"title": "Addressid",
"type": "integer"
},
"firstIndex": {
"default": 1,
"description": "The page of results will be fetched starting from `firstIndex` (which is a resource id).**To learn more about cursor-based pagination [see this page](/docs/understanding-our-api/resources/cursor-pagination)**.",
Expand All @@ -4056,13 +4063,11 @@
"venueId": {
"description": "Venue Id. The venues list is available on [**this endpoint (`Get offerer venues`)**](#tag/Venues/operation/GetOffererVenues)",
"example": 535,
"nullable": true,
"title": "Venueid",
"type": "integer"
}
},
"required": [
"venueId"
],
"title": "GetOffersQueryParams",
"type": "object"
},
Expand Down Expand Up @@ -9730,7 +9735,7 @@
},
"/public/offers/v1/events": {
"get": {
"description": "Return all the events linked to given venue. Results are paginated (by default there are `50` events per page).",
"description": "Return filtered events.\n\nResults are paginated (by default there are `50` events per page).",
"operationId": "GetEvents",
"parameters": [
{
Expand Down Expand Up @@ -9764,10 +9769,11 @@
"description": "Venue Id. The venues list is available on [**this endpoint (`Get offerer venues`)**](#tag/Venues/operation/GetOffererVenues)",
"in": "query",
"name": "venueId",
"required": true,
"required": false,
"schema": {
"description": "Venue Id. The venues list is available on [**this endpoint (`Get offerer venues`)**](#tag/Venues/operation/GetOffererVenues)",
"example": 535,
"nullable": true,
"title": "Venueid",
"type": "integer"
}
Expand All @@ -9784,6 +9790,19 @@
"title": "Idsatprovider",
"type": "string"
}
},
{
"description": "Address id in the pass Culture DB",
"in": "query",
"name": "addressId",
"required": false,
"schema": {
"description": "Address id in the pass Culture DB",
"example": 1,
"nullable": true,
"title": "Addressid",
"type": "integer"
}
}
],
"responses": {
Expand Down Expand Up @@ -9825,7 +9844,7 @@
"ApiKeyAuth": []
}
],
"summary": "Get Venue Event Offers",
"summary": "Get Event Offers",
"tags": [
"Event Offers"
]
Expand Down Expand Up @@ -10811,7 +10830,7 @@
},
"/public/offers/v1/products": {
"get": {
"description": "Return all products linked to a venue. Results are paginated (by default `50` products by page).",
"description": "Return fitered products.\n\nResults are paginated (by default `50` products by page).",
"operationId": "GetProducts",
"parameters": [
{
Expand Down Expand Up @@ -10845,10 +10864,11 @@
"description": "Venue Id. The venues list is available on [**this endpoint (`Get offerer venues`)**](#tag/Venues/operation/GetOffererVenues)",
"in": "query",
"name": "venueId",
"required": true,
"required": false,
"schema": {
"description": "Venue Id. The venues list is available on [**this endpoint (`Get offerer venues`)**](#tag/Venues/operation/GetOffererVenues)",
"example": 535,
"nullable": true,
"title": "Venueid",
"type": "integer"
}
Expand All @@ -10865,6 +10885,19 @@
"title": "Idsatprovider",
"type": "string"
}
},
{
"description": "Address id in the pass Culture DB",
"in": "query",
"name": "addressId",
"required": false,
"schema": {
"description": "Address id in the pass Culture DB",
"example": 1,
"nullable": true,
"title": "Addressid",
"type": "integer"
}
}
],
"responses": {
Expand Down Expand Up @@ -10903,7 +10936,7 @@
"ApiKeyAuth": []
}
],
"summary": "Get Venue Products",
"summary": "Get Product Offers",
"tags": [
"Product Offers"
]
Expand Down
17 changes: 7 additions & 10 deletions api/src/pcapi/routes/public/individual_offers/v1/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,19 +193,16 @@ def get_event(event_id: int) -> serialization.EventOfferResponse:
)
def get_events(query: serialization.GetOffersQueryParams) -> serialization.EventOffersResponse:
"""
Get Venue Event Offers
Get Event Offers
Return filtered events.
Return all the events linked to given venue.
Results are paginated (by default there are `50` events per page).
"""
authorization.get_venue_provider_or_raise_404(query.venue_id)

total_offers_query = utils.retrieve_offers(
is_event=True,
firstIndex=query.firstIndex,
filtered_venue_id=query.venue_id,
ids_at_provider=query.ids_at_provider, # type: ignore[arg-type]
).limit(query.limit)
if query.venue_id:
authorization.get_venue_provider_or_raise_404(query.venue_id)

total_offers_query = utils.get_filtered_offers_linked_to_provider(query, is_event=True)

return serialization.EventOffersResponse(
events=[serialization.EventOfferResponse.build_event_offer(offer) for offer in total_offers_query],
Expand Down
17 changes: 8 additions & 9 deletions api/src/pcapi/routes/public/individual_offers/v1/products.py
Original file line number Diff line number Diff line change
Expand Up @@ -767,17 +767,16 @@ def get_products(
query: serialization.GetOffersQueryParams,
) -> serialization.ProductOffersResponse:
"""
Get Venue Products
Get Product Offers
Return all products linked to a venue. Results are paginated (by default `50` products by page).
Return fitered products.
Results are paginated (by default `50` products by page).
"""
utils.check_venue_id_is_tied_to_api_key(query.venue_id)
total_offers_query = utils.retrieve_offers(
is_event=False,
firstIndex=query.firstIndex,
filtered_venue_id=query.venue_id,
ids_at_provider=query.ids_at_provider, # type: ignore[arg-type]
).limit(query.limit)
if query.venue_id:
authorization.get_venue_provider_or_raise_404(query.venue_id)

total_offers_query = utils.get_filtered_offers_linked_to_provider(query, is_event=False)

return serialization.ProductOffersResponse(
products=[serialization.ProductOfferResponse.build_product_offer(offer) for offer in total_offers_query],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -828,8 +828,9 @@ def build_event_offer(cls, offer: offers_models.Offer) -> "EventOfferResponse":


class GetOffersQueryParams(IndexPaginationQueryParams):
venue_id: int = fields.VENUE_ID
venue_id: int | None = fields.VENUE_ID
ids_at_provider: str | None = fields.IDS_AT_PROVIDER_FILTER
address_id: int | None = fields.ADDRESS_ID

@pydantic_v1.validator("ids_at_provider")
def validate_ids_at_provider(cls, ids_at_provider: str) -> list[str] | None:
Expand Down
25 changes: 18 additions & 7 deletions api/src/pcapi/routes/public/individual_offers/v1/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,26 +98,37 @@ def _retrieve_offer_tied_to_user_query() -> sqla_orm.Query:
)


def retrieve_offers(
is_event: bool, firstIndex: int, filtered_venue_id: int, ids_at_provider: list[str] | None
def get_filtered_offers_linked_to_provider(
query_filters: serialization.GetOffersQueryParams,
is_event: bool,
) -> sqla_orm.Query:
offers_query = (
offers_models.Offer.query.outerjoin(offers_models.Offer.futureOffer)
.join(offerers_models.Venue)
.join(providers_models.VenueProvider)
.filter(providers_models.VenueProvider.provider == current_api_key.provider)
.filter(offers_models.Offer.venueId == filtered_venue_id)
.filter(offers_models.Offer.isEvent == is_event)
.filter(offers_models.Offer.id >= firstIndex)
.filter(offers_models.Offer.id >= query_filters.firstIndex)
.order_by(offers_models.Offer.id)
.options(sqla.orm.contains_eager(offers_models.Offer.futureOffer))
.options(sqla_orm.joinedload(offers_models.Offer.venue))
)

if ids_at_provider:
offers_query = offers_query.filter(offers_models.Offer.idAtProvider.in_(ids_at_provider))
if query_filters.venue_id:
offers_query = offers_query.filter(offers_models.Offer.venueId == query_filters.venue_id)

return retrieve_offer_relations_query(offers_query)
if query_filters.ids_at_provider:
offers_query = offers_query.filter(offers_models.Offer.idAtProvider.in_(query_filters.ids_at_provider))

if query_filters.address_id:
offers_query = offers_query.join(
offerers_models.OffererAddress,
offerers_models.OffererAddress.id == offers_models.Offer.offererAddressId,
).filter(offerers_models.OffererAddress.addressId == query_filters.address_id)

offers_query = retrieve_offer_relations_query(offers_query).limit(query_filters.limit)

return offers_query


def save_image(image_body: serialization.ImageBody, offer: offers_models.Offer) -> None:
Expand Down
35 changes: 27 additions & 8 deletions api/tests/routes/public/individual_offers/v1/get_events_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from pcapi.core import testing
from pcapi.core.categories import subcategories_v2 as subcategories
from pcapi.core.offerers import factories as offerers_factories
from pcapi.core.offers import factories as offers_factories
from pcapi.core.offers import models as offers_models

Expand Down Expand Up @@ -46,8 +47,8 @@ def test_get_first_page_old_behavior_when_permission_system_not_enforced(self, c
response = client.with_explicit_token(plain_api_key).get(
self.endpoint_url, params={"venueId": venue_id, "limit": 5}
)
assert response.status_code == 200

assert response.status_code == 200
assert [event["id"] for event in response.json["events"]] == [offer.id for offer in offers[0:5]]

def test_get_first_page(self, client):
Expand All @@ -60,9 +61,9 @@ def test_get_first_page(self, client):
response = client.with_explicit_token(plain_api_key).get(
self.endpoint_url, params={"venueId": venue_id, "limit": 5}
)
assert response.status_code == 200

assert [event["id"] for event in response.json["events"]] == [offer.id for offer in offers[0:5]]
assert response.status_code == 200
assert [event["id"] for event in response.json["events"]] == [offer.id for offer in offers[0:5]]

# This test should be removed when our database has consistant data
def test_get_offers_with_missing_fields(self, client):
Expand All @@ -88,9 +89,9 @@ def test_get_offers_with_missing_fields(self, client):
response = client.with_explicit_token(plain_api_key).get(
self.endpoint_url, params={"venueId": venue_id, "limit": 5}
)
assert response.status_code == 200

assert len(response.json["events"]) == 2
assert response.status_code == 200
assert len(response.json["events"]) == 2

def test_get_events_without_sub_types(self, client):
plain_api_key, venue_provider = self.setup_active_venue_provider()
Expand All @@ -110,9 +111,9 @@ def test_get_events_without_sub_types(self, client):
response = client.with_explicit_token(plain_api_key).get(
self.endpoint_url, params={"venueId": venue_id, "limit": 5}
)
assert response.status_code == 200

assert len(response.json["events"]) == 2
assert response.status_code == 200
assert len(response.json["events"]) == 2

def test_get_events_using_ids_at_provider(self, client):
id_at_provider_1 = "unBelId"
Expand Down Expand Up @@ -143,6 +144,24 @@ def test_get_events_using_ids_at_provider(self, client):
"idsAtProvider": f"{id_at_provider_1},{id_at_provider_2}",
},
)

assert response.status_code == 200
assert [event["id"] for event in response.json["events"]] == [event_1.id, event_2.id]

def test_should_return_offers_linked_to_address_id(self, client):
plain_api_key, venue_provider = self.setup_active_venue_provider()
offerer_address_1 = offerers_factories.OffererAddressFactory(offerer=venue_provider.venue.managingOfferer)
offerer_address_2 = offerers_factories.OffererAddressFactory(offerer=venue_provider.venue.managingOfferer)
offerer_address_3 = offerers_factories.OffererAddressFactory(address=offerer_address_1.address)
offer1 = offers_factories.EventOfferFactory(venue=venue_provider.venue, offererAddress=offerer_address_1)
offers_factories.EventOfferFactory(venue=venue_provider.venue, offererAddress=offerer_address_2)
offers_factories.EventOfferFactory(offererAddress=offerer_address_3)

with testing.assert_num_queries(self.num_queries):
response = client.with_explicit_token(plain_api_key).get(
self.endpoint_url, {"addressId": offerer_address_1.addressId}
)

assert [event["id"] for event in response.json["events"]] == [event_1.id, event_2.id]
assert response.status_code == 200
assert len(response.json["events"]) == 1
assert response.json["events"][0]["id"] == offer1.id
Loading

0 comments on commit 1a44dee

Please sign in to comment.