Skip to content

Commit

Permalink
(PC-32336)[API] feat: public api: filter events by beginning date
Browse files Browse the repository at this point in the history
Add a new query parameter option: beginning to filter events by dates
(from beginning to today).
  • Loading branch information
jbaudet-pass committed Dec 10, 2024
1 parent 5a8cda4 commit 1715847
Show file tree
Hide file tree
Showing 5 changed files with 110 additions and 4 deletions.
36 changes: 36 additions & 0 deletions api/documentation/static/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -4031,6 +4031,14 @@
},
"GetOffersQueryParams": {
"properties": {
"beginning": {
"description": "Period beginning date. The expected format is **[ISO 8601](https://fr.wikipedia.org/wiki/ISO_8601)** (standard format for timezone aware datetime).",
"example": "2024-03-03T13:00:00+02:00",
"format": "date-time",
"nullable": true,
"title": "Beginning",
"type": "string"
},
"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 Down Expand Up @@ -9811,6 +9819,20 @@
"title": "Idsatprovider",
"type": "string"
}
},
{
"description": "Period beginning date. The expected format is **[ISO 8601](https://fr.wikipedia.org/wiki/ISO_8601)** (standard format for timezone aware datetime).",
"in": "query",
"name": "beginning",
"required": false,
"schema": {
"description": "Period beginning date. The expected format is **[ISO 8601](https://fr.wikipedia.org/wiki/ISO_8601)** (standard format for timezone aware datetime).",
"example": "2024-03-03T13:00:00+02:00",
"format": "date-time",
"nullable": true,
"title": "Beginning",
"type": "string"
}
}
],
"responses": {
Expand Down Expand Up @@ -10892,6 +10914,20 @@
"title": "Idsatprovider",
"type": "string"
}
},
{
"description": "Period beginning date. The expected format is **[ISO 8601](https://fr.wikipedia.org/wiki/ISO_8601)** (standard format for timezone aware datetime).",
"in": "query",
"name": "beginning",
"required": false,
"schema": {
"description": "Period beginning date. The expected format is **[ISO 8601](https://fr.wikipedia.org/wiki/ISO_8601)** (standard format for timezone aware datetime).",
"example": "2024-03-03T13:00:00+02:00",
"format": "date-time",
"nullable": true,
"title": "Beginning",
"type": "string"
}
}
],
"responses": {
Expand Down
1 change: 1 addition & 0 deletions api/src/pcapi/routes/public/individual_offers/v1/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ def get_events(query: serialization.GetOffersQueryParams) -> serialization.Event
firstIndex=query.firstIndex,
filtered_venue_id=query.venue_id,
ids_at_provider=query.ids_at_provider, # type: ignore[arg-type]
beginning=query.beginning,
).limit(query.limit)

return serialization.EventOffersResponse(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -830,6 +830,7 @@ def build_event_offer(cls, offer: offers_models.Offer) -> "EventOfferResponse":
class GetOffersQueryParams(IndexPaginationQueryParams):
venue_id: int = fields.VENUE_ID
ids_at_provider: str | None = fields.IDS_AT_PROVIDER_FILTER
beginning: datetime.datetime | None = fields.PERIOD_BEGINNING_DATE

@pydantic_v1.validator("ids_at_provider")
def validate_ids_at_provider(cls, ids_at_provider: str) -> list[str] | None:
Expand Down
33 changes: 29 additions & 4 deletions api/src/pcapi/routes/public/individual_offers/v1/utils.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
from datetime import datetime

from flask_sqlalchemy import BaseQuery
import sqlalchemy as sqla
from sqlalchemy import orm as sqla_orm

Expand Down Expand Up @@ -99,24 +102,46 @@ 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
is_event: bool,
firstIndex: int,
filtered_venue_id: int,
ids_at_provider: list[str] | None,
beginning: datetime | None = None,
) -> sqla_orm.Query:
def apply_base_offer_filters(query: BaseQuery) -> BaseQuery:
return (
query.filter(offers_models.Offer.venueId == filtered_venue_id)
.filter(offers_models.Offer.isEvent == is_event)
.filter(offers_models.Offer.id >= firstIndex)
)

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)
.order_by(offers_models.Offer.id)
.options(sqla.orm.contains_eager(offers_models.Offer.futureOffer))
.options(sqla_orm.joinedload(offers_models.Offer.venue))
)

offers_query = apply_base_offer_filters(offers_query)

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

if beginning:
# fetch offer ids from stock (filtered using beginningDatetime)
# inside another query to avoid a massive join
offer_ids_query = offers_models.Stock.query.filter(
offers_models.Stock.beginningDatetime >= beginning
).with_entities(offers_models.Stock.offerId)

offer_ids_query = apply_base_offer_filters(offer_ids_query.join(offers_models.Offer))
filtered_offer_ids = {row[0] for row in offer_ids_query}

offers_query = offers_query.filter(offers_models.Offer.id.in_(filtered_offer_ids))

return retrieve_offer_relations_query(offers_query)


Expand Down
43 changes: 43 additions & 0 deletions api/tests/routes/public/individual_offers/v1/get_events_test.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
from datetime import datetime
from datetime import timedelta
from datetime import timezone as tz
import decimal

import pytest
Expand Down Expand Up @@ -146,3 +149,43 @@ def test_get_events_using_ids_at_provider(self, client):
assert response.status_code == 200

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

def test_filter_events_by_beginning_date(self, client):
plain_api_key, venue_provider = self.setup_active_venue_provider()

days_ago = [6, 4, 2, 0]
offer_ids_and_days_count = {
n_days_ago: build_offer_id(venue_provider.venue, n_days_ago) for n_days_ago in days_ago
}

base_query_params = {"venueId": venue_provider.venueId, "limit": 15}

num_queries = self.num_queries
num_queries += 1 # filter offers' stocks by beginning date

for n_days_ago in days_ago:
with testing.assert_num_queries(num_queries):
query_params = base_query_params | {"beginning": formatted_day_builder(n_days_ago + 1)}
response = client.with_explicit_token(plain_api_key).get(self.endpoint_url, params=query_params)

assert response.status_code == 200

expected_offer_ids = expected_filtered_offer_ids(offer_ids_and_days_count, n_days_ago)
found_offer_ids = {event["id"] for event in response.json["events"]}
assert (
found_offer_ids == expected_offer_ids
), f"[{n_days_ago} days ago] {found_offer_ids} != {expected_offer_ids}"


def build_offer_id(venue, n_days_ago) -> offers_models.Stock:
beginning = datetime.now(tz.utc) - timedelta(days=n_days_ago)
stock = offers_factories.EventStockFactory(offer__venue=venue, beginningDatetime=beginning)
return stock.offerId


def expected_filtered_offer_ids(offer_ids_and_days_count, from_n_days_ago):
return {offer_id for days_ago, offer_id in offer_ids_and_days_count.items() if days_ago <= from_n_days_ago}


def formatted_day_builder(n_days_ago):
return (datetime.now(tz.utc) - timedelta(days=n_days_ago)).isoformat()

0 comments on commit 1715847

Please sign in to comment.