1
0
Fork 0
mirror of https://github.com/mealie-recipes/mealie.git synced 2025-08-04 21:15:22 +02:00

fix: mealplan pagination (#1464)

* added pagination to get_slice route

* updated mealplan tests

* renamed vars to match pagination query
This commit is contained in:
Michael Genson 2022-07-02 12:44:01 -05:00 committed by GitHub
parent 2f7ff6d178
commit 2809cef3b1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 102 additions and 29 deletions

View file

@ -26,11 +26,11 @@ export const useMealplans = function (range: Ref<DateRange>) {
loading.value = true; loading.value = true;
const units = useAsync(async () => { const units = useAsync(async () => {
const query = { const query = {
start: format(range.value.start, "yyyy-MM-dd"), start_date: format(range.value.start, "yyyy-MM-dd"),
limit: format(range.value.end, "yyyy-MM-dd"), end_date: format(range.value.end, "yyyy-MM-dd"),
}; };
// @ts-ignore TODO Modify typing to allow for string start+limit for mealplans // @ts-ignore TODO Modify typing to allow for string start+limit for mealplans
const { data } = await api.mealplans.getAll(1, -1, { start: query.start, limit: query.limit }); const { data } = await api.mealplans.getAll(1, -1, { start_date: query.start_date, end_date: query.end_date });
if (data) { if (data) {
return data.items; return data.items;
@ -45,11 +45,11 @@ export const useMealplans = function (range: Ref<DateRange>) {
async refreshAll(this: void) { async refreshAll(this: void) {
loading.value = true; loading.value = true;
const query = { const query = {
start: format(range.value.start, "yyyy-MM-dd"), start_date: format(range.value.start, "yyyy-MM-dd"),
limit: format(range.value.end, "yyyy-MM-dd"), end_date: format(range.value.end, "yyyy-MM-dd"),
}; };
// @ts-ignore TODO Modify typing to allow for string start+limit for mealplans // @ts-ignore TODO Modify typing to allow for string start+limit for mealplans
const { data } = await api.mealplans.getAll(1, -1, { start: query.start, limit: query.limit }); const { data } = await api.mealplans.getAll(1, -1, { start_date: query.start_date, end_date: query.end_date });
if (data && data.items) { if (data && data.items) {
mealplans.value = data.items; mealplans.value = data.items;

View file

@ -1,8 +1,13 @@
from datetime import date from datetime import date
from math import ceil
from uuid import UUID from uuid import UUID
from sqlalchemy import func
from sqlalchemy.sql import sqltypes
from mealie.db.models.group import GroupMealPlan from mealie.db.models.group import GroupMealPlan
from mealie.schema.meal_plan.new_meal import ReadPlanEntry from mealie.schema.meal_plan.new_meal import PlanEntryPagination, ReadPlanEntry
from mealie.schema.response.pagination import OrderDirection, PaginationQuery
from .repository_generic import RepositoryGeneric from .repository_generic import RepositoryGeneric
@ -11,15 +16,67 @@ class RepositoryMeals(RepositoryGeneric[ReadPlanEntry, GroupMealPlan]):
def by_group(self, group_id: UUID) -> "RepositoryMeals": def by_group(self, group_id: UUID) -> "RepositoryMeals":
return super().by_group(group_id) # type: ignore return super().by_group(group_id) # type: ignore
def get_slice(self, start: date, end: date, group_id: UUID) -> list[ReadPlanEntry]: def get_slice(
start_str = start.strftime("%Y-%m-%d") self, pagination: PaginationQuery, start_date: date, end_date: date, group_id: UUID
end_str = end.strftime("%Y-%m-%d") ) -> PlanEntryPagination:
qry = self.session.query(GroupMealPlan).filter( start_str = start_date.strftime("%Y-%m-%d")
end_str = end_date.strftime("%Y-%m-%d")
# get the total number of documents
q = self.session.query(GroupMealPlan).filter(
GroupMealPlan.date.between(start_str, end_str), GroupMealPlan.date.between(start_str, end_str),
GroupMealPlan.group_id == group_id, GroupMealPlan.group_id == group_id,
) )
return [self.schema.from_orm(x) for x in qry.all()] count = q.count()
# interpret -1 as "get_all"
if pagination.per_page == -1:
pagination.per_page = count
try:
total_pages = ceil(count / pagination.per_page)
except ZeroDivisionError:
total_pages = 0
# interpret -1 as "last page"
if pagination.page == -1:
pagination.page = total_pages
# failsafe for user input error
if pagination.page < 1:
pagination.page = 1
if pagination.order_by:
if order_attr := getattr(self.model, pagination.order_by, None):
# queries handle uppercase and lowercase differently, which is undesirable
if isinstance(order_attr.type, sqltypes.String):
order_attr = func.lower(order_attr)
if pagination.order_direction == OrderDirection.asc:
order_attr = order_attr.asc()
elif pagination.order_direction == OrderDirection.desc:
order_attr = order_attr.desc()
q = q.order_by(order_attr)
q = q.limit(pagination.per_page).offset((pagination.page - 1) * pagination.per_page)
try:
data = [self.schema.from_orm(x) for x in q.all()]
except Exception as e:
self._log_exception(e)
self.session.rollback()
raise e
return PlanEntryPagination(
page=pagination.page,
per_page=pagination.per_page,
total=count,
total_pages=total_pages,
items=data,
)
def get_today(self, group_id: UUID) -> list[ReadPlanEntry]: def get_today(self, group_id: UUID) -> list[ReadPlanEntry]:
today = date.today() today = date.today()

View file

@ -1,7 +1,8 @@
from datetime import date, timedelta from datetime import date, timedelta
from functools import cached_property from functools import cached_property
from typing import Optional
from fastapi import APIRouter, HTTPException from fastapi import APIRouter, Depends, HTTPException
from mealie.core.exceptions import mealie_registered_exceptions from mealie.core.exceptions import mealie_registered_exceptions
from mealie.repos.repository_meals import RepositoryMeals from mealie.repos.repository_meals import RepositoryMeals
@ -9,9 +10,10 @@ from mealie.routes._base import BaseUserController, controller
from mealie.routes._base.mixins import HttpRepo from mealie.routes._base.mixins import HttpRepo
from mealie.schema import mapper from mealie.schema import mapper
from mealie.schema.meal_plan import CreatePlanEntry, ReadPlanEntry, SavePlanEntry, UpdatePlanEntry from mealie.schema.meal_plan import CreatePlanEntry, ReadPlanEntry, SavePlanEntry, UpdatePlanEntry
from mealie.schema.meal_plan.new_meal import CreateRandomEntry from mealie.schema.meal_plan.new_meal import CreateRandomEntry, PlanEntryPagination
from mealie.schema.meal_plan.plan_rules import PlanRulesDay from mealie.schema.meal_plan.plan_rules import PlanRulesDay
from mealie.schema.recipe.recipe import Recipe from mealie.schema.recipe.recipe import Recipe
from mealie.schema.response.pagination import PaginationQuery
from mealie.schema.response.responses import ErrorResponse from mealie.schema.response.responses import ErrorResponse
router = APIRouter(prefix="/groups/mealplans", tags=["Groups: Mealplans"]) router = APIRouter(prefix="/groups/mealplans", tags=["Groups: Mealplans"])
@ -85,11 +87,17 @@ class GroupMealplanController(BaseUserController):
except IndexError: except IndexError:
raise HTTPException(status_code=404, detail=ErrorResponse.respond(message="No recipes match your rules")) raise HTTPException(status_code=404, detail=ErrorResponse.respond(message="No recipes match your rules"))
@router.get("", response_model=list[ReadPlanEntry]) @router.get("", response_model=PlanEntryPagination)
def get_all(self, start: date = None, limit: date = None): def get_all(
start = start or date.today() - timedelta(days=999) self,
limit = limit or date.today() + timedelta(days=999) q: PaginationQuery = Depends(PaginationQuery),
return self.repo.get_slice(start, limit, group_id=self.group.id) start_date: Optional[date] = None,
end_date: Optional[date] = None,
):
start_date = start_date or date.today() - timedelta(days=999)
end_date = end_date or date.today() + timedelta(days=999)
return self.repo.get_slice(pagination=q, start_date=start_date, end_date=end_date, group_id=self.group.id)
@router.post("", response_model=ReadPlanEntry, status_code=201) @router.post("", response_model=ReadPlanEntry, status_code=201)
def create_one(self, data: CreatePlanEntry): def create_one(self, data: CreatePlanEntry):

View file

@ -7,6 +7,7 @@ from pydantic import validator
from mealie.schema._mealie import MealieModel from mealie.schema._mealie import MealieModel
from mealie.schema.recipe.recipe import RecipeSummary from mealie.schema.recipe.recipe import RecipeSummary
from mealie.schema.response.pagination import PaginationBase
class PlanEntryType(str, Enum): class PlanEntryType(str, Enum):
@ -54,3 +55,7 @@ class ReadPlanEntry(UpdatePlanEntry):
class Config: class Config:
orm_mode = True orm_mode = True
class PlanEntryPagination(PaginationBase):
items: list[ReadPlanEntry]

View file

@ -12,14 +12,17 @@ class Routes:
recipe = "/api/recipes" recipe = "/api/recipes"
today = "/api/groups/mealplans/today" today = "/api/groups/mealplans/today"
def all_slice(start: str, end: str): @staticmethod
return f"{Routes.base}?start={start}&limit={end}" def all_slice(page: int, perPage: int, start_date: str, end_date: str):
return f"{Routes.base}?page={page}&perPage={perPage}&start_date={start_date}&end_date={end_date}"
@staticmethod
def item(item_id: int) -> str: def item(item_id: int) -> str:
return f"{Routes.base}/{item_id}" return f"{Routes.base}/{item_id}"
def recipe_slug(recipe_id: int) -> str: @staticmethod
return f"{Routes.recipe}/{recipe_id}" def recipe_slug(recipe_name: str) -> str:
return f"{Routes.recipe}/{recipe_name}"
def test_create_mealplan_no_recipe(api_client: TestClient, unique_user: TestUser): def test_create_mealplan_no_recipe(api_client: TestClient, unique_user: TestUser):
@ -106,10 +109,10 @@ def test_get_all_mealplans(api_client: TestClient, unique_user: TestUser):
response = api_client.post(Routes.base, json=new_plan, headers=unique_user.token) response = api_client.post(Routes.base, json=new_plan, headers=unique_user.token)
assert response.status_code == 201 assert response.status_code == 201
response = api_client.get(Routes.base, headers=unique_user.token) response = api_client.get(Routes.base, headers=unique_user.token, params={"page": 1, "perPage": -1})
assert response.status_code == 200 assert response.status_code == 200
assert len(response.json()) >= 3 assert len(response.json()["items"]) >= 3
def test_get_slice_mealplans(api_client: TestClient, unique_user: TestUser): def test_get_slice_mealplans(api_client: TestClient, unique_user: TestUser):
@ -132,15 +135,15 @@ def test_get_slice_mealplans(api_client: TestClient, unique_user: TestUser):
slices = [dates, dates[1:2], dates[2:3], dates[3:4], dates[4:5]] slices = [dates, dates[1:2], dates[2:3], dates[3:4], dates[4:5]]
for date_range in slices: for date_range in slices:
start = date_range[0].strftime("%Y-%m-%d") start_date = date_range[0].strftime("%Y-%m-%d")
end = date_range[-1].strftime("%Y-%m-%d") end_date = date_range[-1].strftime("%Y-%m-%d")
response = api_client.get(Routes.all_slice(start, end), headers=unique_user.token) response = api_client.get(Routes.all_slice(1, -1, start_date, end_date), headers=unique_user.token)
assert response.status_code == 200 assert response.status_code == 200
response_json = response.json() response_json = response.json()
for meal_plan in response_json: for meal_plan in response_json["items"]:
assert meal_plan["date"] in [date.strftime("%Y-%m-%d") for date in date_range] assert meal_plan["date"] in [date.strftime("%Y-%m-%d") for date in date_range]