diff --git a/mealie/repos/repository_meals.py b/mealie/repos/repository_meals.py index da26bf6cfdbf..34187e35fb45 100644 --- a/mealie/repos/repository_meals.py +++ b/mealie/repos/repository_meals.py @@ -19,3 +19,15 @@ class RepositoryMeals(HouseholdRepositoryGeneric[ReadPlanEntry, GroupMealPlan]): ) plans = self.session.execute(stmt).scalars().all() return [self.schema.model_validate(x) for x in plans] + + def get_meals_by_date_range(self, start_date: datetime, end_date: datetime) -> list[ReadPlanEntry]: + if not self.household_id: + raise Exception("household_id not set") + + stmt = select(GroupMealPlan).filter( + GroupMealPlan.date >= start_date.date(), + GroupMealPlan.date <= end_date.date(), + GroupMealPlan.household_id == self.household_id, + ) + plans = self.session.execute(stmt).scalars().all() + return [self.schema.model_validate(x) for x in plans] diff --git a/mealie/services/event_bus_service/event_bus_listeners.py b/mealie/services/event_bus_service/event_bus_listeners.py index 6443dbe956e5..dd19992d9e31 100644 --- a/mealie/services/event_bus_service/event_bus_listeners.py +++ b/mealie/services/event_bus_service/event_bus_listeners.py @@ -16,7 +16,6 @@ from mealie.db.models.household.webhooks import GroupWebhooksModel from mealie.repos.repository_factory import AllRepositories from mealie.schema.household.group_events import GroupEventNotifierPrivate from mealie.schema.household.webhook import ReadWebhook -from mealie.schema.response.pagination import PaginationQuery from .event_types import Event, EventDocumentType, EventTypes, EventWebhookData from .publisher import ApprisePublisher, PublisherLike, WebhookPublisher @@ -123,7 +122,14 @@ class AppriseEventListener(EventListenerBase): @staticmethod def is_custom_url(url: str): - return url.split(":", 1)[0].lower() in ["form", "forms", "json", "jsons", "xml", "xmls"] + return url.split(":", 1)[0].lower() in [ + "form", + "forms", + "json", + "jsons", + "xml", + "xmls", + ] class WebhookEventListener(EventListenerBase): @@ -143,12 +149,12 @@ class WebhookEventListener(EventListenerBase): def publish_to_subscribers(self, event: Event, subscribers: list[ReadWebhook]) -> None: with self.ensure_repos(self.group_id, self.household_id) as repos: if event.document_data.document_type == EventDocumentType.mealplan: - # TODO: limit mealplan data to a date range instead of returning all mealplans + webhook_data = cast(EventWebhookData, event.document_data) meal_repo = repos.meals - meal_pagination_data = meal_repo.page_all(pagination=PaginationQuery(page=1, per_page=-1)) - meal_data = meal_pagination_data.items + meal_data = meal_repo.get_meals_by_date_range( + webhook_data.webhook_start_dt, webhook_data.webhook_end_dt + ) if meal_data: - webhook_data = cast(EventWebhookData, event.document_data) webhook_data.webhook_body = meal_data self.publisher.publish(event, [webhook.url for webhook in subscribers]) diff --git a/tests/unit_tests/services_tests/scheduler/tasks/test_post_webhook.py b/tests/unit_tests/services_tests/scheduler/tasks/test_post_webhook.py index 9a6bd1eacd56..6a218d322027 100644 --- a/tests/unit_tests/services_tests/scheduler/tasks/test_post_webhook.py +++ b/tests/unit_tests/services_tests/scheduler/tasks/test_post_webhook.py @@ -5,6 +5,13 @@ from pydantic import UUID4 from mealie.schema.household.webhook import SaveWebhook, WebhookType from mealie.services.event_bus_service.event_bus_listeners import WebhookEventListener +from mealie.services.event_bus_service.event_types import ( + Event, + EventBusMessage, + EventDocumentType, + EventTypes, + EventWebhookData, +) from tests.utils import random_string from tests.utils.factories import random_bool from tests.utils.fixture_schemas import TestUser @@ -69,3 +76,222 @@ def test_get_scheduled_webhooks_filter_query(unique_user: TestUser): if result.name == expected_item.name: # Names are uniquely generated so we can use this to compare assert result.enabled == expected_item.enabled break + + +def test_event_listener_get_meals_by_date_range(unique_user: TestUser): + """ + Test that WebhookEventListener correctly uses the get_meals_by_date_range method + to retrieve meals and publish the webhook event. + """ + meal_repo = unique_user.repos.meals + + start_date = datetime.now(timezone.utc) - timedelta(days=7) + end_date = datetime.now(timezone.utc) + + meal_1 = meal_repo.create( + { + "date": start_date + timedelta(days=1), + "entry_type": "lunch", + "title": "Meal 1", + "text": "Test meal 1", + "group_id": unique_user.group_id, + "household_id": unique_user.household_id, + "user_id": unique_user.user_id, + } + ) + meal_2 = meal_repo.create( + { + "date": start_date + timedelta(days=3), + "entry_type": "dinner", + "title": "Meal 2", + "text": "Test meal 2", + "group_id": unique_user.group_id, + "household_id": unique_user.household_id, + "user_id": unique_user.user_id, + } + ) + + webhook_data = EventWebhookData( + webhook_start_dt=start_date, + webhook_end_dt=end_date, + document_type=EventDocumentType.mealplan, + operation="create", + ) + event = Event( + event_type=EventTypes.webhook_task, + document_data=webhook_data, + message=EventBusMessage(title="Test event message"), + integration_id="00000000-0000-0000-0000-000000000000", + ) + + event_bus_listener = WebhookEventListener(UUID(unique_user.group_id), UUID(unique_user.household_id)) + subscribers = event_bus_listener.get_scheduled_webhooks(start_date, end_date) + + event_bus_listener.publish_to_subscribers(event, subscribers) + + assert event.document_data.webhook_body is not None + meals = event.document_data.webhook_body + assert len(meals) == 2 + + assert any(meal.title == "Meal 1" for meal in meals) + assert any(meal.title == "Meal 2" for meal in meals) + + try: + assert event.document_data.webhook_body is not None + meals = event.document_data.webhook_body + assert len(meals) == 2 + + assert any(meal.title == "Meal 1" for meal in meals) + assert any(meal.title == "Meal 2" for meal in meals) + + finally: + meal_repo.delete(meal_1.id) + meal_repo.delete(meal_2.id) + + +def test_get_meals_by_date_range(unique_user: TestUser): + meal_repo = unique_user.repos.meals + + start_date = datetime.now(timezone.utc) - timedelta(days=7) + end_date = datetime.now(timezone.utc) + + meal_1 = meal_repo.create( + { + "date": start_date + timedelta(days=1), + "entry_type": "breakfast", + "title": "Meal 1", + "text": "Test meal 1", + "group_id": unique_user.group_id, + "household_id": unique_user.household_id, + "user_id": unique_user.user_id, + } + ) + meal_2 = meal_repo.create( + { + "date": start_date + timedelta(days=3), + "entry_type": "lunch", + "title": "Meal 2", + "text": "Test meal 2", + "group_id": unique_user.group_id, + "household_id": unique_user.household_id, + "user_id": unique_user.user_id, + } + ) + meal_3 = meal_repo.create( + { + "date": start_date - timedelta(days=10), + "entry_type": "dinner", + "title": "Meal 3", + "text": "Test meal 3", + "group_id": unique_user.group_id, + "household_id": unique_user.household_id, + "user_id": unique_user.user_id, + } + ) + + try: + meals_in_range = meal_repo.get_meals_by_date_range(start_date, end_date) + + assert len(meals_in_range) == 2 + assert any(meal.title == "Meal 1" for meal in meals_in_range) + assert any(meal.title == "Meal 2" for meal in meals_in_range) + + assert all(meal.title != "Meal 3" for meal in meals_in_range) + + finally: + meal_repo.delete(meal_1.id) + meal_repo.delete(meal_2.id) + meal_repo.delete(meal_3.id) + + +def test_get_meals_by_date_range_no_meals(unique_user: TestUser): + """ + Test that get_meals_by_date_range returns an empty list when there are no meals in the given date range. + """ + meal_repo = unique_user.repos.meals + + start_date = datetime.now(timezone.utc) - timedelta(days=7) + end_date = datetime.now(timezone.utc) + + meals_in_range = meal_repo.get_meals_by_date_range(start_date, end_date) + + assert len(meals_in_range) == 0 + + +def test_get_meals_by_date_range_single_day(unique_user: TestUser): + """ + Test that get_meals_by_date_range returns meals correctly when start_date and end_date are the same. + """ + meal_repo = unique_user.repos.meals + + single_day = datetime.now(timezone.utc) + + meal_1 = meal_repo.create( + { + "date": single_day, + "entry_type": "breakfast", + "title": "Single Day Meal", + "text": "Test meal for a single day", + "group_id": unique_user.group_id, + "household_id": unique_user.household_id, + "user_id": unique_user.user_id, + } + ) + + try: + meals_in_range = meal_repo.get_meals_by_date_range(single_day, single_day) + + assert len(meals_in_range) == 1 + assert meals_in_range[0].title == "Single Day Meal" + assert meals_in_range[0].date == single_day.date() + + finally: + meal_repo.delete(meal_1.id) + + +def test_get_meals_by_date_range_no_overlap(unique_user: TestUser): + """ + Test that get_meals_by_date_range returns an empty list when there are no meals that overlap with the date range. + """ + meal_repo = unique_user.repos.meals + + start_date = datetime.now(timezone.utc) + timedelta(days=1) + end_date = datetime.now(timezone.utc) + timedelta(days=10) + + meal_1 = meal_repo.create( + { + "date": datetime.now(timezone.utc) - timedelta(days=5), + "entry_type": "dinner", + "title": "Meal Outside Range", + "text": "This meal is outside the tested date range", + "group_id": unique_user.group_id, + "household_id": unique_user.household_id, + "user_id": unique_user.user_id, + } + ) + + meals_in_range = meal_repo.get_meals_by_date_range(start_date, end_date) + + assert len(meals_in_range) == 0 + + try: + meals_in_range = meal_repo.get_meals_by_date_range(start_date, end_date) + + assert len(meals_in_range) == 0 + + finally: + meal_repo.delete(meal_1.id) + + +def test_get_meals_by_date_range_invalid_date_range(unique_user: TestUser): + """ + Test that get_meals_by_date_range raises an exception or returns empty when start_date is greater than end_date. + """ + meal_repo = unique_user.repos.meals + + start_date = datetime.now(timezone.utc) + end_date = start_date - timedelta(days=1) + + meals_in_range = meal_repo.get_meals_by_date_range(start_date, end_date) + + assert len(meals_in_range) == 0