mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-05-24 01:12:54 -04:00
feat: re-write get all routes to use pagination (#1424)
rewrite get_all routes to use a pagination pattern to allow for better implementations of search, filter, and sorting on the frontend or by any client without fetching all the data. Additionally we added a CI check for running the Nuxt built to confirm that no TS errors were present. Finally, I had to remove the header support for the Shopping lists as the browser caching based off last_updated header was not allowing it to read recent updates due to how we're handling the updated_at property in the database with nested fields. This will have to be looked at in the future to reimplement. I'm unsure how many other routes have a similar issue. Co-authored-by: Hayden <64056131+hay-kot@users.noreply.github.com>
This commit is contained in:
parent
c158672d12
commit
cb15db2d27
41
.github/workflows/frontend-lint.yml
vendored
41
.github/workflows/frontend-lint.yml
vendored
@ -9,7 +9,7 @@ on:
|
|||||||
- mealie-next
|
- mealie-next
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
ci:
|
lint:
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
@ -47,3 +47,42 @@ jobs:
|
|||||||
- name: Run linter 👀
|
- name: Run linter 👀
|
||||||
run: yarn lint
|
run: yarn lint
|
||||||
working-directory: "frontend"
|
working-directory: "frontend"
|
||||||
|
|
||||||
|
build:
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-latest]
|
||||||
|
node: [16]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout 🛎
|
||||||
|
uses: actions/checkout@master
|
||||||
|
|
||||||
|
- name: Setup node env 🏗
|
||||||
|
uses: actions/setup-node@v2.1.5
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node }}
|
||||||
|
check-latest: true
|
||||||
|
|
||||||
|
- name: Get yarn cache directory path 🛠
|
||||||
|
id: yarn-cache-dir-path
|
||||||
|
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||||
|
|
||||||
|
- name: Cache node_modules 📦
|
||||||
|
uses: actions/cache@v2.1.4
|
||||||
|
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
|
||||||
|
with:
|
||||||
|
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||||
|
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-yarn-
|
||||||
|
|
||||||
|
- name: Install dependencies 👨🏻💻
|
||||||
|
run: yarn
|
||||||
|
working-directory: "frontend"
|
||||||
|
|
||||||
|
- name: Run Build 🚚
|
||||||
|
run: yarn build
|
||||||
|
working-directory: "frontend"
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { ApiRequestInstance } from "~/types/api";
|
import { ApiRequestInstance, PaginationData } from "~/types/api";
|
||||||
|
|
||||||
export interface CrudAPIInterface {
|
export interface CrudAPIInterface {
|
||||||
requests: ApiRequestInstance;
|
requests: ApiRequestInstance;
|
||||||
@ -22,9 +22,9 @@ export abstract class BaseCRUDAPI<CreateType, ReadType, UpdateType=CreateType> e
|
|||||||
abstract baseRoute: string;
|
abstract baseRoute: string;
|
||||||
abstract itemRoute(itemId: string | number): string;
|
abstract itemRoute(itemId: string | number): string;
|
||||||
|
|
||||||
async getAll(start = 0, limit = 9999, params = {} as any) {
|
async getAll(page = 1, perPage = -1, params = {} as any) {
|
||||||
return await this.requests.get<ReadType[]>(this.baseRoute, {
|
return await this.requests.get<PaginationData<ReadType>>(this.baseRoute, {
|
||||||
params: { start, limit, ...params },
|
params: { page, perPage, ...params },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -261,7 +261,7 @@ export default defineComponent({
|
|||||||
async function getShoppingLists() {
|
async function getShoppingLists() {
|
||||||
const { data } = await api.shopping.lists.getAll();
|
const { data } = await api.shopping.lists.getAll();
|
||||||
if (data) {
|
if (data) {
|
||||||
shoppingLists.value = data;
|
shoppingLists.value = data.items ?? [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -131,10 +131,10 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function refreshTokens() {
|
async function refreshTokens() {
|
||||||
const { data } = await userApi.recipes.share.getAll(0, 999, { recipe_id: props.recipeId });
|
const { data } = await userApi.recipes.share.getAll(1, -1, { recipe_id: props.recipeId });
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
state.tokens = data;
|
state.tokens = data.items ?? [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,11 +30,15 @@ export function useStoreActions<T extends BoundT>(
|
|||||||
const allItems = useAsync(async () => {
|
const allItems = useAsync(async () => {
|
||||||
const { data } = await api.getAll();
|
const { data } = await api.getAll();
|
||||||
|
|
||||||
if (allRef) {
|
if (data && allRef) {
|
||||||
allRef.value = data;
|
allRef.value = data.items;
|
||||||
}
|
}
|
||||||
|
|
||||||
return data ?? [];
|
if (data) {
|
||||||
|
return data.items ?? [];
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
}, useAsyncKey());
|
}, useAsyncKey());
|
||||||
|
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
@ -45,8 +49,8 @@ export function useStoreActions<T extends BoundT>(
|
|||||||
loading.value = true;
|
loading.value = true;
|
||||||
const { data } = await api.getAll();
|
const { data } = await api.getAll();
|
||||||
|
|
||||||
if (data && allRef) {
|
if (data && data.items && allRef) {
|
||||||
allRef.value = data;
|
allRef.value = data.items;
|
||||||
}
|
}
|
||||||
|
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
|
@ -21,7 +21,12 @@ export const useTools = function (eager = true) {
|
|||||||
loading.value = true;
|
loading.value = true;
|
||||||
const units = useAsync(async () => {
|
const units = useAsync(async () => {
|
||||||
const { data } = await api.tools.getAll();
|
const { data } = await api.tools.getAll();
|
||||||
return data;
|
|
||||||
|
if (data) {
|
||||||
|
return data.items;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}, useAsyncKey());
|
}, useAsyncKey());
|
||||||
|
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
@ -33,7 +38,7 @@ export const useTools = function (eager = true) {
|
|||||||
const { data } = await api.tools.getAll();
|
const { data } = await api.tools.getAll();
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
tools.value = data;
|
tools.value = data.items;
|
||||||
}
|
}
|
||||||
|
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
|
@ -18,8 +18,8 @@ function swap(t: Array<unknown>, i: number, j: number) {
|
|||||||
export const useSorter = () => {
|
export const useSorter = () => {
|
||||||
function sortAToZ(list: Array<Recipe>) {
|
function sortAToZ(list: Array<Recipe>) {
|
||||||
list.sort((a, b) => {
|
list.sort((a, b) => {
|
||||||
const textA = a.name?.toUpperCase() ?? "";
|
const textA: string = a.name?.toUpperCase() ?? "";
|
||||||
const textB = b.name?.toUpperCase() ?? "";
|
const textB: string = b.name?.toUpperCase() ?? "";
|
||||||
return textA < textB ? -1 : textA > textB ? 1 : 0;
|
return textA < textB ? -1 : textA > textB ? 1 : 0;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -61,10 +61,10 @@ export const useLazyRecipes = function () {
|
|||||||
|
|
||||||
const recipes = ref<Recipe[]>([]);
|
const recipes = ref<Recipe[]>([]);
|
||||||
|
|
||||||
async function fetchMore(start: number, limit: number, orderBy: string | null = null, orderDescending = true) {
|
async function fetchMore(page: number, perPage: number, orderBy: string | null = null, orderDirection = "desc") {
|
||||||
const { data } = await api.recipes.getAll(start, limit, { orderBy, orderDescending });
|
const { data } = await api.recipes.getAll(page, perPage, { orderBy, orderDirection });
|
||||||
if (data) {
|
if (data) {
|
||||||
data.forEach((recipe) => {
|
data.items.forEach((recipe) => {
|
||||||
recipes.value?.push(recipe);
|
recipes.value?.push(recipe);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -80,26 +80,26 @@ export const useRecipes = (all = false, fetchRecipes = true) => {
|
|||||||
const api = useUserApi();
|
const api = useUserApi();
|
||||||
|
|
||||||
// recipes is non-reactive!!
|
// recipes is non-reactive!!
|
||||||
const { recipes, start, end } = (() => {
|
const { recipes, page, perPage } = (() => {
|
||||||
if (all) {
|
if (all) {
|
||||||
return {
|
return {
|
||||||
recipes: allRecipes,
|
recipes: allRecipes,
|
||||||
start: 0,
|
page: 1,
|
||||||
end: 9999,
|
perPage: -1,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
recipes: recentRecipes,
|
recipes: recentRecipes,
|
||||||
start: 0,
|
page: 1,
|
||||||
end: 30,
|
perPage: 30,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
async function refreshRecipes() {
|
async function refreshRecipes() {
|
||||||
const { data } = await api.recipes.getAll(start, end, { loadFood: true, orderBy: "created_at" });
|
const { data } = await api.recipes.getAll(page, perPage, { loadFood: true, orderBy: "created_at" });
|
||||||
if (data) {
|
if (data) {
|
||||||
recipes.value = data;
|
recipes.value = data.items;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,7 +31,11 @@ export const useCookbooks = function () {
|
|||||||
const units = useAsync(async () => {
|
const units = useAsync(async () => {
|
||||||
const { data } = await api.cookbooks.getAll();
|
const { data } = await api.cookbooks.getAll();
|
||||||
|
|
||||||
return data;
|
if (data) {
|
||||||
|
return data.items;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}, useAsyncKey());
|
}, useAsyncKey());
|
||||||
|
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
@ -41,8 +45,8 @@ export const useCookbooks = function () {
|
|||||||
loading.value = true;
|
loading.value = true;
|
||||||
const { data } = await api.cookbooks.getAll();
|
const { data } = await api.cookbooks.getAll();
|
||||||
|
|
||||||
if (data && cookbookStore) {
|
if (data && data.items && cookbookStore) {
|
||||||
cookbookStore.value = data;
|
cookbookStore.value = data.items;
|
||||||
}
|
}
|
||||||
|
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
|
@ -30,9 +30,13 @@ export const useMealplans = function (range: Ref<DateRange>) {
|
|||||||
limit: format(range.value.end, "yyyy-MM-dd"),
|
limit: 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(query.start, query.limit);
|
const { data } = await api.mealplans.getAll(1, -1, { start: query.start, limit: query.limit });
|
||||||
|
|
||||||
return data;
|
if (data) {
|
||||||
|
return data.items;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}, useAsyncKey());
|
}, useAsyncKey());
|
||||||
|
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
@ -45,10 +49,10 @@ export const useMealplans = function (range: Ref<DateRange>) {
|
|||||||
limit: format(range.value.end, "yyyy-MM-dd"),
|
limit: 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(query.start, query.limit);
|
const { data } = await api.mealplans.getAll(1, -1, { start: query.start, limit: query.limit });
|
||||||
|
|
||||||
if (data) {
|
if (data && data.items) {
|
||||||
mealplans.value = data;
|
mealplans.value = data.items;
|
||||||
}
|
}
|
||||||
|
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
|
@ -14,7 +14,11 @@ export const useGroupWebhooks = function () {
|
|||||||
const units = useAsync(async () => {
|
const units = useAsync(async () => {
|
||||||
const { data } = await api.groupWebhooks.getAll();
|
const { data } = await api.groupWebhooks.getAll();
|
||||||
|
|
||||||
return data;
|
if (data) {
|
||||||
|
return data.items;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}, useAsyncKey());
|
}, useAsyncKey());
|
||||||
|
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
@ -24,8 +28,8 @@ export const useGroupWebhooks = function () {
|
|||||||
loading.value = true;
|
loading.value = true;
|
||||||
const { data } = await api.groupWebhooks.getAll();
|
const { data } = await api.groupWebhooks.getAll();
|
||||||
|
|
||||||
if (data) {
|
if (data && data.items) {
|
||||||
webhooks.value = data;
|
webhooks.value = data.items;
|
||||||
}
|
}
|
||||||
|
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
|
@ -43,7 +43,12 @@ export const useGroups = function () {
|
|||||||
const asyncKey = String(Date.now());
|
const asyncKey = String(Date.now());
|
||||||
const groups = useAsync(async () => {
|
const groups = useAsync(async () => {
|
||||||
const { data } = await api.groups.getAll();
|
const { data } = await api.groups.getAll();
|
||||||
return data;
|
|
||||||
|
if (data) {
|
||||||
|
return data.items;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}, asyncKey);
|
}, asyncKey);
|
||||||
|
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
@ -53,7 +58,13 @@ export const useGroups = function () {
|
|||||||
async function refreshAllGroups() {
|
async function refreshAllGroups() {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
const { data } = await api.groups.getAll();
|
const { data } = await api.groups.getAll();
|
||||||
groups.value = data;
|
|
||||||
|
if (data) {
|
||||||
|
groups.value = data.items;
|
||||||
|
} else {
|
||||||
|
groups.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,7 +17,11 @@ export const useAllUsers = function () {
|
|||||||
const asyncKey = String(Date.now());
|
const asyncKey = String(Date.now());
|
||||||
const allUsers = useAsync(async () => {
|
const allUsers = useAsync(async () => {
|
||||||
const { data } = await api.users.getAll();
|
const { data } = await api.users.getAll();
|
||||||
return data;
|
if (data) {
|
||||||
|
return data.items;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}, asyncKey);
|
}, asyncKey);
|
||||||
|
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
@ -27,7 +31,13 @@ export const useAllUsers = function () {
|
|||||||
async function refreshAllUsers() {
|
async function refreshAllUsers() {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
const { data } = await api.users.getAll();
|
const { data } = await api.users.getAll();
|
||||||
users.value = data;
|
|
||||||
|
if (data) {
|
||||||
|
users.value = data.items;
|
||||||
|
} else {
|
||||||
|
users.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -126,7 +126,7 @@ export default defineComponent({
|
|||||||
const { data } = await api.mealplanRules.getAll();
|
const { data } = await api.mealplanRules.getAll();
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
allRules.value = data;
|
allRules.value = data.items ?? [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,21 +22,18 @@ import { useLazyRecipes } from "~/composables/recipes";
|
|||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: { RecipeCardSection },
|
components: { RecipeCardSection },
|
||||||
setup() {
|
setup() {
|
||||||
// paging and sorting params
|
const page = ref(1);
|
||||||
|
const perPage = ref(30);
|
||||||
const orderBy = "name";
|
const orderBy = "name";
|
||||||
const orderDescending = false;
|
const orderDirection = "asc";
|
||||||
const increment = ref(30);
|
|
||||||
|
|
||||||
const start = ref(0);
|
|
||||||
const offset = ref(increment.value);
|
|
||||||
const limit = ref(increment.value);
|
|
||||||
const ready = ref(false);
|
const ready = ref(false);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
|
||||||
const { recipes, fetchMore } = useLazyRecipes();
|
const { recipes, fetchMore } = useLazyRecipes();
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await fetchMore(start.value, limit.value, orderBy, orderDescending);
|
await fetchMore(page.value, perPage.value, orderBy, orderDirection);
|
||||||
ready.value = true;
|
ready.value = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -45,9 +42,8 @@ export default defineComponent({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
start.value = offset.value + 1;
|
page.value = page.value + 1;
|
||||||
offset.value = offset.value + increment.value;
|
fetchMore(page.value, perPage.value, orderBy, orderDirection);
|
||||||
fetchMore(start.value, limit.value, orderBy, orderDescending);
|
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
|
@ -193,11 +193,11 @@ import { useCopyList } from "~/composables/use-copy";
|
|||||||
import { useUserApi } from "~/composables/api";
|
import { useUserApi } from "~/composables/api";
|
||||||
import { useAsyncKey } from "~/composables/use-utils";
|
import { useAsyncKey } from "~/composables/use-utils";
|
||||||
import ShoppingListItem from "~/components/Domain/ShoppingList/ShoppingListItem.vue";
|
import ShoppingListItem from "~/components/Domain/ShoppingList/ShoppingListItem.vue";
|
||||||
import { MultiPurposeLabelOut } from "~/types/api-types/labels";
|
|
||||||
import { ShoppingListItemCreate, ShoppingListItemOut } from "~/types/api-types/group";
|
import { ShoppingListItemCreate, ShoppingListItemOut } from "~/types/api-types/group";
|
||||||
import RecipeList from "~/components/Domain/Recipe/RecipeList.vue";
|
import RecipeList from "~/components/Domain/Recipe/RecipeList.vue";
|
||||||
import ShoppingListItemEditor from "~/components/Domain/ShoppingList/ShoppingListItemEditor.vue";
|
import ShoppingListItemEditor from "~/components/Domain/ShoppingList/ShoppingListItemEditor.vue";
|
||||||
import { getDisplayText } from "~/composables/use-display-text";
|
import { getDisplayText } from "~/composables/use-display-text";
|
||||||
|
import { useFoodStore, useLabelStore, useUnitStore } from "~/composables/store";
|
||||||
|
|
||||||
type CopyTypes = "plain" | "markdown";
|
type CopyTypes = "plain" | "markdown";
|
||||||
|
|
||||||
@ -336,17 +336,9 @@ export default defineComponent({
|
|||||||
// Labels, Units, Foods
|
// Labels, Units, Foods
|
||||||
// TODO: Extract to Composable
|
// TODO: Extract to Composable
|
||||||
|
|
||||||
const allLabels = ref([] as MultiPurposeLabelOut[]);
|
const { labels: allLabels } = useLabelStore();
|
||||||
|
const { units: allUnits } = useUnitStore();
|
||||||
const allUnits = useAsync(async () => {
|
const { foods: allFoods } = useFoodStore();
|
||||||
const { data } = await userApi.units.getAll();
|
|
||||||
return data ?? [];
|
|
||||||
}, useAsyncKey());
|
|
||||||
|
|
||||||
const allFoods = useAsync(async () => {
|
|
||||||
const { data } = await userApi.foods.getAll();
|
|
||||||
return data ?? [];
|
|
||||||
}, useAsyncKey());
|
|
||||||
|
|
||||||
function sortByLabels() {
|
function sortByLabels() {
|
||||||
byLabel.value = !byLabel.value;
|
byLabel.value = !byLabel.value;
|
||||||
@ -405,7 +397,10 @@ export default defineComponent({
|
|||||||
|
|
||||||
async function refreshLabels() {
|
async function refreshLabels() {
|
||||||
const { data } = await userApi.multiPurposeLabels.getAll();
|
const { data } = await userApi.multiPurposeLabels.getAll();
|
||||||
allLabels.value = data ?? [];
|
|
||||||
|
if (data) {
|
||||||
|
allLabels.value = data.items ?? [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshLabels();
|
refreshLabels();
|
||||||
|
@ -60,7 +60,12 @@ export default defineComponent({
|
|||||||
|
|
||||||
async function fetchShoppingLists() {
|
async function fetchShoppingLists() {
|
||||||
const { data } = await userApi.shopping.lists.getAll();
|
const { data } = await userApi.shopping.lists.getAll();
|
||||||
return data;
|
|
||||||
|
if (!data) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.items;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refresh() {
|
async function refresh() {
|
||||||
|
@ -13,3 +13,11 @@ export interface ApiRequestInstance {
|
|||||||
patch<T, U = Partial<T>>(url: string, data: U): Promise<RequestResponse<T>>;
|
patch<T, U = Partial<T>>(url: string, data: U): Promise<RequestResponse<T>>;
|
||||||
delete<T>(url: string): Promise<RequestResponse<T>>;
|
delete<T>(url: string): Promise<RequestResponse<T>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PaginationData<T> {
|
||||||
|
page: number;
|
||||||
|
per_page: number;
|
||||||
|
total: number;
|
||||||
|
total_pages: number;
|
||||||
|
items: T[];
|
||||||
|
}
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
|
from math import ceil
|
||||||
from typing import Any, Generic, TypeVar, Union
|
from typing import Any, Generic, TypeVar, Union
|
||||||
|
|
||||||
from pydantic import UUID4, BaseModel
|
from pydantic import UUID4, BaseModel
|
||||||
from sqlalchemy import func
|
from sqlalchemy import func
|
||||||
from sqlalchemy.orm.session import Session
|
from sqlalchemy.orm.session import Session
|
||||||
|
from sqlalchemy.sql import sqltypes
|
||||||
|
|
||||||
from mealie.core.root_logger import get_logger
|
from mealie.core.root_logger import get_logger
|
||||||
from mealie.schema.response.pagination import OrderDirection, PaginationBase, PaginationQuery
|
from mealie.schema.response.pagination import OrderDirection, PaginationBase, PaginationQuery
|
||||||
@ -59,6 +61,8 @@ class RepositoryGeneric(Generic[Schema, Model]):
|
|||||||
def get_all(
|
def get_all(
|
||||||
self, limit: int = None, order_by: str = None, order_descending: bool = True, start=0, override=None
|
self, limit: int = None, order_by: str = None, order_descending: bool = True, start=0, override=None
|
||||||
) -> list[Schema]:
|
) -> list[Schema]:
|
||||||
|
self.logger.warning('"get_all" method is deprecated; use "page_all" instead')
|
||||||
|
|
||||||
# sourcery skip: remove-unnecessary-cast
|
# sourcery skip: remove-unnecessary-cast
|
||||||
eff_schema = override or self.schema
|
eff_schema = override or self.schema
|
||||||
|
|
||||||
@ -224,7 +228,7 @@ class RepositoryGeneric(Generic[Schema, Model]):
|
|||||||
else:
|
else:
|
||||||
return [eff_schema.from_orm(x) for x in q.all()]
|
return [eff_schema.from_orm(x) for x in q.all()]
|
||||||
|
|
||||||
def pagination(self, pagination: PaginationQuery, override=None) -> PaginationBase[Schema]:
|
def page_all(self, pagination: PaginationQuery, override=None) -> PaginationBase[Schema]:
|
||||||
"""
|
"""
|
||||||
pagination is a method to interact with the filtered database table and return a paginated result
|
pagination is a method to interact with the filtered database table and return a paginated result
|
||||||
using the PaginationBase that provides several data points that are needed to manage pagination
|
using the PaginationBase that provides several data points that are needed to manage pagination
|
||||||
@ -240,11 +244,32 @@ class RepositoryGeneric(Generic[Schema, Model]):
|
|||||||
|
|
||||||
fltr = self._filter_builder()
|
fltr = self._filter_builder()
|
||||||
q = q.filter_by(**fltr)
|
q = q.filter_by(**fltr)
|
||||||
|
|
||||||
count = q.count()
|
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 pagination.order_by:
|
||||||
if order_attr := getattr(self.model, pagination.order_by, None):
|
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:
|
if pagination.order_direction == OrderDirection.asc:
|
||||||
order_attr = order_attr.asc()
|
order_attr = order_attr.asc()
|
||||||
elif pagination.order_direction == OrderDirection.desc:
|
elif pagination.order_direction == OrderDirection.desc:
|
||||||
@ -265,6 +290,6 @@ class RepositoryGeneric(Generic[Schema, Model]):
|
|||||||
page=pagination.page,
|
page=pagination.page,
|
||||||
per_page=pagination.per_page,
|
per_page=pagination.per_page,
|
||||||
total=count,
|
total=count,
|
||||||
total_pages=int(count / pagination.per_page) + 1,
|
total_pages=total_pages,
|
||||||
data=[eff_schema.from_orm(s) for s in data],
|
items=[eff_schema.from_orm(s) for s in data],
|
||||||
)
|
)
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
from math import ceil
|
||||||
from random import randint
|
from random import randint
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
@ -7,6 +8,7 @@ from slugify import slugify
|
|||||||
from sqlalchemy import and_, func
|
from sqlalchemy import and_, func
|
||||||
from sqlalchemy.exc import IntegrityError
|
from sqlalchemy.exc import IntegrityError
|
||||||
from sqlalchemy.orm import joinedload
|
from sqlalchemy.orm import joinedload
|
||||||
|
from sqlalchemy.sql import sqltypes
|
||||||
|
|
||||||
from mealie.db.models.recipe.category import Category
|
from mealie.db.models.recipe.category import Category
|
||||||
from mealie.db.models.recipe.ingredient import RecipeIngredient
|
from mealie.db.models.recipe.ingredient import RecipeIngredient
|
||||||
@ -15,8 +17,9 @@ from mealie.db.models.recipe.settings import RecipeSettings
|
|||||||
from mealie.db.models.recipe.tag import Tag
|
from mealie.db.models.recipe.tag import Tag
|
||||||
from mealie.db.models.recipe.tool import Tool
|
from mealie.db.models.recipe.tool import Tool
|
||||||
from mealie.schema.recipe import Recipe
|
from mealie.schema.recipe import Recipe
|
||||||
from mealie.schema.recipe.recipe import RecipeCategory, RecipeSummary, RecipeTag, RecipeTool
|
from mealie.schema.recipe.recipe import RecipeCategory, RecipePagination, RecipeSummary, RecipeTag, RecipeTool
|
||||||
from mealie.schema.recipe.recipe_category import CategoryBase, TagBase
|
from mealie.schema.recipe.recipe_category import CategoryBase, TagBase
|
||||||
|
from mealie.schema.response.pagination import OrderDirection, PaginationQuery
|
||||||
|
|
||||||
from .repository_generic import RepositoryGeneric
|
from .repository_generic import RepositoryGeneric
|
||||||
|
|
||||||
@ -128,6 +131,72 @@ class RepositoryRecipes(RepositoryGeneric[Recipe, RecipeModel]):
|
|||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def page_all(self, pagination: PaginationQuery, override=None, load_food=False) -> RecipePagination:
|
||||||
|
q = self.session.query(self.model)
|
||||||
|
|
||||||
|
args = [
|
||||||
|
joinedload(RecipeModel.recipe_category),
|
||||||
|
joinedload(RecipeModel.tags),
|
||||||
|
joinedload(RecipeModel.tools),
|
||||||
|
]
|
||||||
|
|
||||||
|
if load_food:
|
||||||
|
args.append(joinedload(RecipeModel.recipe_ingredient).options(joinedload(RecipeIngredient.food)))
|
||||||
|
|
||||||
|
q = q.options(*args)
|
||||||
|
|
||||||
|
fltr = self._filter_builder()
|
||||||
|
q = q.filter_by(**fltr)
|
||||||
|
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 = q.all()
|
||||||
|
except Exception as e:
|
||||||
|
self._log_exception(e)
|
||||||
|
self.session.rollback()
|
||||||
|
raise e
|
||||||
|
|
||||||
|
return RecipePagination(
|
||||||
|
page=pagination.page,
|
||||||
|
per_page=pagination.per_page,
|
||||||
|
total=count,
|
||||||
|
total_pages=total_pages,
|
||||||
|
items=data,
|
||||||
|
)
|
||||||
|
|
||||||
def get_by_categories(self, categories: list[RecipeCategory]) -> list[RecipeSummary]:
|
def get_by_categories(self, categories: list[RecipeCategory]) -> list[RecipeSummary]:
|
||||||
"""
|
"""
|
||||||
get_by_categories returns all the Recipes that contain every category provided in the list
|
get_by_categories returns all the Recipes that contain every category provided in the list
|
||||||
|
@ -5,9 +5,9 @@ from pydantic import UUID4
|
|||||||
|
|
||||||
from mealie.schema.group.group import GroupAdminUpdate
|
from mealie.schema.group.group import GroupAdminUpdate
|
||||||
from mealie.schema.mapper import mapper
|
from mealie.schema.mapper import mapper
|
||||||
from mealie.schema.query import GetAll
|
from mealie.schema.response.pagination import PaginationQuery
|
||||||
from mealie.schema.response.responses import ErrorResponse
|
from mealie.schema.response.responses import ErrorResponse
|
||||||
from mealie.schema.user.user import GroupBase, GroupInDB
|
from mealie.schema.user.user import GroupBase, GroupInDB, GroupPagination
|
||||||
from mealie.services.group_services.group_service import GroupService
|
from mealie.services.group_services.group_service import GroupService
|
||||||
|
|
||||||
from .._base import BaseAdminController, controller
|
from .._base import BaseAdminController, controller
|
||||||
@ -39,9 +39,15 @@ class AdminUserManagementRoutes(BaseAdminController):
|
|||||||
self.registered_exceptions,
|
self.registered_exceptions,
|
||||||
)
|
)
|
||||||
|
|
||||||
@router.get("", response_model=list[GroupInDB])
|
@router.get("", response_model=GroupPagination)
|
||||||
def get_all(self, q: GetAll = Depends(GetAll)):
|
def get_all(self, q: PaginationQuery = Depends(PaginationQuery)):
|
||||||
return self.repo.get_all(start=q.start, limit=q.limit, override=GroupInDB)
|
response = self.repo.page_all(
|
||||||
|
pagination=q,
|
||||||
|
override=GroupInDB,
|
||||||
|
)
|
||||||
|
|
||||||
|
response.set_pagination_guides(router.url_path_for("get_all"), q.dict())
|
||||||
|
return response
|
||||||
|
|
||||||
@router.post("", response_model=GroupInDB, status_code=status.HTTP_201_CREATED)
|
@router.post("", response_model=GroupInDB, status_code=status.HTTP_201_CREATED)
|
||||||
def create_one(self, data: GroupBase):
|
def create_one(self, data: GroupBase):
|
||||||
|
@ -7,9 +7,9 @@ from mealie.core import security
|
|||||||
from mealie.routes._base import BaseAdminController, controller
|
from mealie.routes._base import BaseAdminController, controller
|
||||||
from mealie.routes._base.dependencies import SharedDependencies
|
from mealie.routes._base.dependencies import SharedDependencies
|
||||||
from mealie.routes._base.mixins import HttpRepo
|
from mealie.routes._base.mixins import HttpRepo
|
||||||
from mealie.schema.query import GetAll
|
from mealie.schema.response.pagination import PaginationQuery
|
||||||
from mealie.schema.response.responses import ErrorResponse
|
from mealie.schema.response.responses import ErrorResponse
|
||||||
from mealie.schema.user.user import UserIn, UserOut
|
from mealie.schema.user.user import UserIn, UserOut, UserPagination
|
||||||
|
|
||||||
router = APIRouter(prefix="/users", tags=["Admin: Users"])
|
router = APIRouter(prefix="/users", tags=["Admin: Users"])
|
||||||
|
|
||||||
@ -32,9 +32,15 @@ class AdminUserManagementRoutes(BaseAdminController):
|
|||||||
def mixins(self):
|
def mixins(self):
|
||||||
return HttpRepo[UserIn, UserOut, UserOut](self.repo, self.deps.logger, self.registered_exceptions)
|
return HttpRepo[UserIn, UserOut, UserOut](self.repo, self.deps.logger, self.registered_exceptions)
|
||||||
|
|
||||||
@router.get("", response_model=list[UserOut])
|
@router.get("", response_model=UserPagination)
|
||||||
def get_all(self, q: GetAll = Depends(GetAll)):
|
def get_all(self, q: PaginationQuery = Depends(PaginationQuery)):
|
||||||
return self.repo.get_all(start=q.start, limit=q.limit, override=UserOut)
|
response = self.repo.page_all(
|
||||||
|
pagination=q,
|
||||||
|
override=UserOut,
|
||||||
|
)
|
||||||
|
|
||||||
|
response.set_pagination_guides(router.url_path_for("get_all"), q.dict())
|
||||||
|
return response
|
||||||
|
|
||||||
@router.post("", response_model=UserOut, status_code=201)
|
@router.post("", response_model=UserOut, status_code=201)
|
||||||
def create_one(self, data: UserIn):
|
def create_one(self, data: UserIn):
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
from fastapi import BackgroundTasks
|
from fastapi import BackgroundTasks, Depends
|
||||||
|
|
||||||
from mealie.routes._base import BaseAdminController, controller
|
from mealie.routes._base import BaseAdminController, controller
|
||||||
from mealie.routes._base.routers import UserAPIRouter
|
from mealie.routes._base.routers import UserAPIRouter
|
||||||
from mealie.schema.server.tasks import ServerTask, ServerTaskNames
|
from mealie.schema.response.pagination import PaginationQuery
|
||||||
|
from mealie.schema.server.tasks import ServerTask, ServerTaskNames, ServerTaskPagination
|
||||||
from mealie.services.server_tasks import BackgroundExecutor, test_executor_func
|
from mealie.services.server_tasks import BackgroundExecutor, test_executor_func
|
||||||
|
|
||||||
router = UserAPIRouter()
|
router = UserAPIRouter()
|
||||||
@ -10,9 +11,15 @@ router = UserAPIRouter()
|
|||||||
|
|
||||||
@controller(router)
|
@controller(router)
|
||||||
class AdminServerTasksController(BaseAdminController):
|
class AdminServerTasksController(BaseAdminController):
|
||||||
@router.get("/server-tasks", response_model=list[ServerTask])
|
@router.get("/server-tasks", response_model=ServerTaskPagination)
|
||||||
def get_all(self):
|
def get_all(self, q: PaginationQuery = Depends(PaginationQuery)):
|
||||||
return self.repos.server_tasks.get_all(order_by="created_at")
|
response = self.repos.server_tasks.page_all(
|
||||||
|
pagination=q,
|
||||||
|
override=ServerTask,
|
||||||
|
)
|
||||||
|
|
||||||
|
response.set_pagination_guides(router.url_path_for("get_all"), q.dict())
|
||||||
|
return response
|
||||||
|
|
||||||
@router.post("/server-tasks", response_model=ServerTask, status_code=201)
|
@router.post("/server-tasks", response_model=ServerTask, status_code=201)
|
||||||
def create_test_tasks(self, bg_tasks: BackgroundTasks):
|
def create_test_tasks(self, bg_tasks: BackgroundTasks):
|
||||||
|
@ -8,13 +8,14 @@ from mealie.core.exceptions import mealie_registered_exceptions
|
|||||||
from mealie.routes._base.base_controllers import BaseUserController
|
from mealie.routes._base.base_controllers import BaseUserController
|
||||||
from mealie.routes._base.controller import controller
|
from mealie.routes._base.controller import controller
|
||||||
from mealie.routes._base.mixins import HttpRepo
|
from mealie.routes._base.mixins import HttpRepo
|
||||||
from mealie.schema.query import GetAll
|
|
||||||
from mealie.schema.recipe.recipe_comments import (
|
from mealie.schema.recipe.recipe_comments import (
|
||||||
RecipeCommentCreate,
|
RecipeCommentCreate,
|
||||||
RecipeCommentOut,
|
RecipeCommentOut,
|
||||||
|
RecipeCommentPagination,
|
||||||
RecipeCommentSave,
|
RecipeCommentSave,
|
||||||
RecipeCommentUpdate,
|
RecipeCommentUpdate,
|
||||||
)
|
)
|
||||||
|
from mealie.schema.response.pagination import PaginationQuery
|
||||||
from mealie.schema.response.responses import ErrorResponse, SuccessResponse
|
from mealie.schema.response.responses import ErrorResponse, SuccessResponse
|
||||||
|
|
||||||
router = APIRouter(prefix="/comments", tags=["Recipe: Comments"])
|
router = APIRouter(prefix="/comments", tags=["Recipe: Comments"])
|
||||||
@ -38,12 +39,18 @@ class RecipeCommentRoutes(BaseUserController):
|
|||||||
if comment.user_id != self.deps.acting_user.id and not self.deps.acting_user.admin:
|
if comment.user_id != self.deps.acting_user.id and not self.deps.acting_user.admin:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=403,
|
status_code=403,
|
||||||
detail=ErrorResponse.response(message="Comment does not belong to user"),
|
detail=ErrorResponse(message="Comment does not belong to user"),
|
||||||
)
|
)
|
||||||
|
|
||||||
@router.get("", response_model=list[RecipeCommentOut])
|
@router.get("", response_model=RecipeCommentPagination)
|
||||||
def get_all(self, q: GetAll = Depends(GetAll)):
|
def get_all(self, q: PaginationQuery = Depends(PaginationQuery)):
|
||||||
return self.repo.get_all(start=q.start, limit=q.limit, override=RecipeCommentOut)
|
response = self.repo.page_all(
|
||||||
|
pagination=q,
|
||||||
|
override=RecipeCommentOut,
|
||||||
|
)
|
||||||
|
|
||||||
|
response.set_pagination_guides(router.url_path_for("get_all"), q.dict())
|
||||||
|
return response
|
||||||
|
|
||||||
@router.post("", response_model=RecipeCommentOut, status_code=201)
|
@router.post("", response_model=RecipeCommentOut, status_code=201)
|
||||||
def create_one(self, data: RecipeCommentCreate):
|
def create_one(self, data: RecipeCommentCreate):
|
||||||
|
@ -9,6 +9,8 @@ from mealie.routes._base.mixins import HttpRepo
|
|||||||
from mealie.routes._base.routers import MealieCrudRoute
|
from mealie.routes._base.routers import MealieCrudRoute
|
||||||
from mealie.schema import mapper
|
from mealie.schema import mapper
|
||||||
from mealie.schema.cookbook import CreateCookBook, ReadCookBook, RecipeCookBook, SaveCookBook, UpdateCookBook
|
from mealie.schema.cookbook import CreateCookBook, ReadCookBook, RecipeCookBook, SaveCookBook, UpdateCookBook
|
||||||
|
from mealie.schema.cookbook.cookbook import CookBookPagination
|
||||||
|
from mealie.schema.response.pagination import PaginationQuery
|
||||||
from mealie.services.event_bus_service.event_bus_service import EventBusService, EventSource
|
from mealie.services.event_bus_service.event_bus_service import EventBusService, EventSource
|
||||||
from mealie.services.event_bus_service.message_types import EventTypes
|
from mealie.services.event_bus_service.message_types import EventTypes
|
||||||
|
|
||||||
@ -38,11 +40,15 @@ class GroupCookbookController(BaseUserController):
|
|||||||
self.registered_exceptions,
|
self.registered_exceptions,
|
||||||
)
|
)
|
||||||
|
|
||||||
@router.get("", response_model=list[ReadCookBook])
|
@router.get("", response_model=CookBookPagination)
|
||||||
def get_all(self):
|
def get_all(self, q: PaginationQuery = Depends(PaginationQuery)):
|
||||||
items = self.repo.get_all()
|
response = self.repo.page_all(
|
||||||
items.sort(key=lambda x: x.position)
|
pagination=q,
|
||||||
return items
|
override=ReadCookBook,
|
||||||
|
)
|
||||||
|
|
||||||
|
response.set_pagination_guides(router.url_path_for("get_all"), q.dict())
|
||||||
|
return response
|
||||||
|
|
||||||
@router.post("", response_model=ReadCookBook, status_code=201)
|
@router.post("", response_model=ReadCookBook, status_code=201)
|
||||||
def create_one(self, data: CreateCookBook):
|
def create_one(self, data: CreateCookBook):
|
||||||
|
@ -13,9 +13,10 @@ from mealie.schema.group.group_events import (
|
|||||||
GroupEventNotifierPrivate,
|
GroupEventNotifierPrivate,
|
||||||
GroupEventNotifierSave,
|
GroupEventNotifierSave,
|
||||||
GroupEventNotifierUpdate,
|
GroupEventNotifierUpdate,
|
||||||
|
GroupEventPagination,
|
||||||
)
|
)
|
||||||
from mealie.schema.mapper import cast
|
from mealie.schema.mapper import cast
|
||||||
from mealie.schema.query import GetAll
|
from mealie.schema.response.pagination import PaginationQuery
|
||||||
from mealie.services.event_bus_service.event_bus_service import EventBusService
|
from mealie.services.event_bus_service.event_bus_service import EventBusService
|
||||||
|
|
||||||
router = APIRouter(
|
router = APIRouter(
|
||||||
@ -41,9 +42,15 @@ class GroupEventsNotifierController(BaseUserController):
|
|||||||
def mixins(self) -> HttpRepo:
|
def mixins(self) -> HttpRepo:
|
||||||
return HttpRepo(self.repo, self.deps.logger, self.registered_exceptions, "An unexpected error occurred.")
|
return HttpRepo(self.repo, self.deps.logger, self.registered_exceptions, "An unexpected error occurred.")
|
||||||
|
|
||||||
@router.get("", response_model=list[GroupEventNotifierOut])
|
@router.get("", response_model=GroupEventPagination)
|
||||||
def get_all(self, q: GetAll = Depends(GetAll)):
|
def get_all(self, q: PaginationQuery = Depends(PaginationQuery)):
|
||||||
return self.repo.get_all(start=q.start, limit=q.limit)
|
response = self.repo.page_all(
|
||||||
|
pagination=q,
|
||||||
|
override=GroupEventNotifierOut,
|
||||||
|
)
|
||||||
|
|
||||||
|
response.set_pagination_guides(router.url_path_for("get_all"), q.dict())
|
||||||
|
return response
|
||||||
|
|
||||||
@router.post("", response_model=GroupEventNotifierOut, status_code=201)
|
@router.post("", response_model=GroupEventNotifierOut, status_code=201)
|
||||||
def create_one(self, data: GroupEventNotifierCreate):
|
def create_one(self, data: GroupEventNotifierCreate):
|
||||||
|
@ -14,8 +14,9 @@ from mealie.schema.labels import (
|
|||||||
MultiPurposeLabelSummary,
|
MultiPurposeLabelSummary,
|
||||||
MultiPurposeLabelUpdate,
|
MultiPurposeLabelUpdate,
|
||||||
)
|
)
|
||||||
|
from mealie.schema.labels.multi_purpose_label import MultiPurposeLabelPagination
|
||||||
from mealie.schema.mapper import cast
|
from mealie.schema.mapper import cast
|
||||||
from mealie.schema.query import GetAll
|
from mealie.schema.response.pagination import PaginationQuery
|
||||||
|
|
||||||
router = APIRouter(prefix="/groups/labels", tags=["Group: Multi Purpose Labels"], route_class=MealieCrudRoute)
|
router = APIRouter(prefix="/groups/labels", tags=["Group: Multi Purpose Labels"], route_class=MealieCrudRoute)
|
||||||
|
|
||||||
@ -36,9 +37,15 @@ class MultiPurposeLabelsController(BaseUserController):
|
|||||||
def mixins(self) -> HttpRepo:
|
def mixins(self) -> HttpRepo:
|
||||||
return HttpRepo(self.repo, self.deps.logger, self.registered_exceptions, "An unexpected error occurred.")
|
return HttpRepo(self.repo, self.deps.logger, self.registered_exceptions, "An unexpected error occurred.")
|
||||||
|
|
||||||
@router.get("", response_model=list[MultiPurposeLabelSummary])
|
@router.get("", response_model=MultiPurposeLabelPagination)
|
||||||
def get_all(self, q: GetAll = Depends(GetAll)):
|
def get_all(self, q: PaginationQuery = Depends(PaginationQuery)):
|
||||||
return self.repo.get_all(start=q.start, limit=q.limit, override=MultiPurposeLabelSummary)
|
response = self.repo.page_all(
|
||||||
|
pagination=q,
|
||||||
|
override=MultiPurposeLabelSummary,
|
||||||
|
)
|
||||||
|
|
||||||
|
response.set_pagination_guides(router.url_path_for("get_all"), q.dict())
|
||||||
|
return response
|
||||||
|
|
||||||
@router.post("", response_model=MultiPurposeLabelOut)
|
@router.post("", response_model=MultiPurposeLabelOut)
|
||||||
def create_one(self, data: MultiPurposeLabelCreate):
|
def create_one(self, data: MultiPurposeLabelCreate):
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
|
|
||||||
|
from fastapi import Depends
|
||||||
from pydantic import UUID4
|
from pydantic import UUID4
|
||||||
|
|
||||||
from mealie.routes._base.base_controllers import BaseUserController
|
from mealie.routes._base.base_controllers import BaseUserController
|
||||||
@ -7,7 +8,8 @@ from mealie.routes._base.controller import controller
|
|||||||
from mealie.routes._base.mixins import HttpRepo
|
from mealie.routes._base.mixins import HttpRepo
|
||||||
from mealie.routes._base.routers import UserAPIRouter
|
from mealie.routes._base.routers import UserAPIRouter
|
||||||
from mealie.schema import mapper
|
from mealie.schema import mapper
|
||||||
from mealie.schema.meal_plan.plan_rules import PlanRulesCreate, PlanRulesOut, PlanRulesSave
|
from mealie.schema.meal_plan.plan_rules import PlanRulesCreate, PlanRulesOut, PlanRulesPagination, PlanRulesSave
|
||||||
|
from mealie.schema.response.pagination import PaginationQuery
|
||||||
|
|
||||||
router = UserAPIRouter(prefix="/groups/mealplans/rules", tags=["Groups: Mealplan Rules"])
|
router = UserAPIRouter(prefix="/groups/mealplans/rules", tags=["Groups: Mealplan Rules"])
|
||||||
|
|
||||||
@ -22,9 +24,15 @@ class GroupMealplanConfigController(BaseUserController):
|
|||||||
def mixins(self):
|
def mixins(self):
|
||||||
return HttpRepo[PlanRulesCreate, PlanRulesOut, PlanRulesOut](self.repo, self.deps.logger)
|
return HttpRepo[PlanRulesCreate, PlanRulesOut, PlanRulesOut](self.repo, self.deps.logger)
|
||||||
|
|
||||||
@router.get("", response_model=list[PlanRulesOut])
|
@router.get("", response_model=PlanRulesPagination)
|
||||||
def get_all(self):
|
def get_all(self, q: PaginationQuery = Depends(PaginationQuery)):
|
||||||
return self.repo.get_all(override=PlanRulesOut)
|
response = self.repo.page_all(
|
||||||
|
pagination=q,
|
||||||
|
override=PlanRulesOut,
|
||||||
|
)
|
||||||
|
|
||||||
|
response.set_pagination_guides(router.url_path_for("get_all"), q.dict())
|
||||||
|
return response
|
||||||
|
|
||||||
@router.post("", response_model=PlanRulesOut, status_code=201)
|
@router.post("", response_model=PlanRulesOut, status_code=201)
|
||||||
def create_one(self, data: PlanRulesCreate):
|
def create_one(self, data: PlanRulesCreate):
|
||||||
|
@ -6,27 +6,25 @@ from pydantic import UUID4
|
|||||||
from mealie.routes._base.base_controllers import BaseUserController
|
from mealie.routes._base.base_controllers import BaseUserController
|
||||||
from mealie.routes._base.controller import controller
|
from mealie.routes._base.controller import controller
|
||||||
from mealie.routes._base.mixins import HttpRepo
|
from mealie.routes._base.mixins import HttpRepo
|
||||||
from mealie.routes._base.routers import MealieCrudRoute
|
|
||||||
from mealie.schema.group.group_shopping_list import (
|
from mealie.schema.group.group_shopping_list import (
|
||||||
ShoppingListCreate,
|
ShoppingListCreate,
|
||||||
ShoppingListItemCreate,
|
ShoppingListItemCreate,
|
||||||
ShoppingListItemOut,
|
ShoppingListItemOut,
|
||||||
ShoppingListItemUpdate,
|
ShoppingListItemUpdate,
|
||||||
ShoppingListOut,
|
ShoppingListOut,
|
||||||
|
ShoppingListPagination,
|
||||||
ShoppingListSave,
|
ShoppingListSave,
|
||||||
ShoppingListSummary,
|
ShoppingListSummary,
|
||||||
ShoppingListUpdate,
|
ShoppingListUpdate,
|
||||||
)
|
)
|
||||||
from mealie.schema.mapper import cast
|
from mealie.schema.mapper import cast
|
||||||
from mealie.schema.query import GetAll
|
from mealie.schema.response.pagination import PaginationQuery
|
||||||
from mealie.schema.response.responses import SuccessResponse
|
from mealie.schema.response.responses import SuccessResponse
|
||||||
from mealie.services.event_bus_service.event_bus_service import EventBusService, EventSource
|
from mealie.services.event_bus_service.event_bus_service import EventBusService, EventSource
|
||||||
from mealie.services.event_bus_service.message_types import EventTypes
|
from mealie.services.event_bus_service.message_types import EventTypes
|
||||||
from mealie.services.group_services.shopping_lists import ShoppingListService
|
from mealie.services.group_services.shopping_lists import ShoppingListService
|
||||||
|
|
||||||
item_router = APIRouter(
|
item_router = APIRouter(prefix="/groups/shopping/items", tags=["Group: Shopping List Items"])
|
||||||
prefix="/groups/shopping/items", tags=["Group: Shopping List Items"], route_class=MealieCrudRoute
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@controller(item_router)
|
@controller(item_router)
|
||||||
@ -98,7 +96,6 @@ class ShoppingListItemController(BaseUserController):
|
|||||||
|
|
||||||
return shopping_list_item
|
return shopping_list_item
|
||||||
|
|
||||||
@item_router.head("/{item_id}", response_model=ShoppingListItemOut)
|
|
||||||
@item_router.get("/{item_id}", response_model=ShoppingListItemOut)
|
@item_router.get("/{item_id}", response_model=ShoppingListItemOut)
|
||||||
def get_one(self, item_id: UUID4):
|
def get_one(self, item_id: UUID4):
|
||||||
return self.mixins.get_one(item_id)
|
return self.mixins.get_one(item_id)
|
||||||
@ -148,7 +145,7 @@ class ShoppingListItemController(BaseUserController):
|
|||||||
return shopping_list_item
|
return shopping_list_item
|
||||||
|
|
||||||
|
|
||||||
router = APIRouter(prefix="/groups/shopping/lists", tags=["Group: Shopping Lists"], route_class=MealieCrudRoute)
|
router = APIRouter(prefix="/groups/shopping/lists", tags=["Group: Shopping Lists"])
|
||||||
|
|
||||||
|
|
||||||
@controller(router)
|
@controller(router)
|
||||||
@ -170,9 +167,15 @@ class ShoppingListController(BaseUserController):
|
|||||||
def mixins(self) -> HttpRepo[ShoppingListCreate, ShoppingListOut, ShoppingListSave]:
|
def mixins(self) -> HttpRepo[ShoppingListCreate, ShoppingListOut, ShoppingListSave]:
|
||||||
return HttpRepo(self.repo, self.deps.logger, self.registered_exceptions, "An unexpected error occurred.")
|
return HttpRepo(self.repo, self.deps.logger, self.registered_exceptions, "An unexpected error occurred.")
|
||||||
|
|
||||||
@router.get("", response_model=list[ShoppingListSummary])
|
@router.get("", response_model=ShoppingListPagination)
|
||||||
def get_all(self, q: GetAll = Depends(GetAll)):
|
def get_all(self, q: PaginationQuery = Depends(PaginationQuery)):
|
||||||
return self.repo.get_all(start=q.start, limit=q.limit, override=ShoppingListSummary)
|
response = self.repo.page_all(
|
||||||
|
pagination=q,
|
||||||
|
override=ShoppingListSummary,
|
||||||
|
)
|
||||||
|
|
||||||
|
response.set_pagination_guides(router.url_path_for("get_all"), q.dict())
|
||||||
|
return response
|
||||||
|
|
||||||
@router.post("", response_model=ShoppingListOut, status_code=201)
|
@router.post("", response_model=ShoppingListOut, status_code=201)
|
||||||
def create_one(self, data: ShoppingListCreate):
|
def create_one(self, data: ShoppingListCreate):
|
||||||
@ -193,7 +196,6 @@ class ShoppingListController(BaseUserController):
|
|||||||
|
|
||||||
return val
|
return val
|
||||||
|
|
||||||
@router.head("/{item_id}", response_model=ShoppingListOut)
|
|
||||||
@router.get("/{item_id}", response_model=ShoppingListOut)
|
@router.get("/{item_id}", response_model=ShoppingListOut)
|
||||||
def get_one(self, item_id: UUID4):
|
def get_one(self, item_id: UUID4):
|
||||||
return self.mixins.get_one(item_id)
|
return self.mixins.get_one(item_id)
|
||||||
|
@ -7,8 +7,8 @@ from mealie.routes._base.base_controllers import BaseUserController
|
|||||||
from mealie.routes._base.controller import controller
|
from mealie.routes._base.controller import 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.group.webhook import CreateWebhook, ReadWebhook, SaveWebhook
|
from mealie.schema.group.webhook import CreateWebhook, ReadWebhook, SaveWebhook, WebhookPagination
|
||||||
from mealie.schema.query import GetAll
|
from mealie.schema.response.pagination import PaginationQuery
|
||||||
|
|
||||||
router = APIRouter(prefix="/groups/webhooks", tags=["Groups: Webhooks"])
|
router = APIRouter(prefix="/groups/webhooks", tags=["Groups: Webhooks"])
|
||||||
|
|
||||||
@ -23,9 +23,15 @@ class ReadWebhookController(BaseUserController):
|
|||||||
def mixins(self) -> HttpRepo:
|
def mixins(self) -> HttpRepo:
|
||||||
return HttpRepo[CreateWebhook, SaveWebhook, CreateWebhook](self.repo, self.deps.logger)
|
return HttpRepo[CreateWebhook, SaveWebhook, CreateWebhook](self.repo, self.deps.logger)
|
||||||
|
|
||||||
@router.get("", response_model=list[ReadWebhook])
|
@router.get("", response_model=WebhookPagination)
|
||||||
def get_all(self, q: GetAll = Depends(GetAll)):
|
def get_all(self, q: PaginationQuery = Depends(PaginationQuery)):
|
||||||
return self.repo.get_all(start=q.start, limit=q.limit, override=ReadWebhook)
|
response = self.repo.page_all(
|
||||||
|
pagination=q,
|
||||||
|
override=ReadWebhook,
|
||||||
|
)
|
||||||
|
|
||||||
|
response.set_pagination_guides(router.url_path_for("get_all"), q.dict())
|
||||||
|
return response
|
||||||
|
|
||||||
@router.post("", response_model=ReadWebhook, status_code=201)
|
@router.post("", response_model=ReadWebhook, status_code=201)
|
||||||
def create_one(self, data: CreateWebhook):
|
def create_one(self, data: CreateWebhook):
|
||||||
|
@ -7,8 +7,9 @@ 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.recipe import CategoryIn, RecipeCategoryResponse
|
from mealie.schema.recipe import CategoryIn, RecipeCategoryResponse
|
||||||
from mealie.schema.recipe.recipe import RecipeCategory
|
from mealie.schema.recipe.recipe import RecipeCategory, RecipeCategoryPagination
|
||||||
from mealie.schema.recipe.recipe_category import CategoryBase, CategorySave
|
from mealie.schema.recipe.recipe_category import CategoryBase, CategorySave
|
||||||
|
from mealie.schema.response.pagination import PaginationQuery
|
||||||
from mealie.services import urls
|
from mealie.services import urls
|
||||||
from mealie.services.event_bus_service.event_bus_service import EventBusService, EventSource
|
from mealie.services.event_bus_service.event_bus_service import EventBusService, EventSource
|
||||||
from mealie.services.event_bus_service.message_types import EventTypes
|
from mealie.services.event_bus_service.message_types import EventTypes
|
||||||
@ -40,10 +41,16 @@ class RecipeCategoryController(BaseUserController):
|
|||||||
def mixins(self):
|
def mixins(self):
|
||||||
return HttpRepo(self.repo, self.deps.logger)
|
return HttpRepo(self.repo, self.deps.logger)
|
||||||
|
|
||||||
@router.get("", response_model=list[RecipeCategory])
|
@router.get("", response_model=RecipeCategoryPagination)
|
||||||
def get_all(self):
|
def get_all(self, q: PaginationQuery = Depends(PaginationQuery)):
|
||||||
"""Returns a list of available categories in the database"""
|
"""Returns a list of available categories in the database"""
|
||||||
return self.repo.get_all(override=RecipeCategory)
|
response = self.repo.page_all(
|
||||||
|
pagination=q,
|
||||||
|
override=RecipeCategory,
|
||||||
|
)
|
||||||
|
|
||||||
|
response.set_pagination_guides(router.url_path_for("get_all"), q.dict())
|
||||||
|
return response
|
||||||
|
|
||||||
@router.post("", status_code=201)
|
@router.post("", status_code=201)
|
||||||
def create_one(self, category: CategoryIn):
|
def create_one(self, category: CategoryIn):
|
||||||
|
@ -7,8 +7,9 @@ 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.recipe import RecipeTagResponse, TagIn
|
from mealie.schema.recipe import RecipeTagResponse, TagIn
|
||||||
from mealie.schema.recipe.recipe import RecipeTag
|
from mealie.schema.recipe.recipe import RecipeTag, RecipeTagPagination
|
||||||
from mealie.schema.recipe.recipe_category import TagSave
|
from mealie.schema.recipe.recipe_category import TagSave
|
||||||
|
from mealie.schema.response.pagination import PaginationQuery
|
||||||
from mealie.services import urls
|
from mealie.services import urls
|
||||||
from mealie.services.event_bus_service.event_bus_service import EventBusService, EventSource
|
from mealie.services.event_bus_service.event_bus_service import EventBusService, EventSource
|
||||||
from mealie.services.event_bus_service.message_types import EventTypes
|
from mealie.services.event_bus_service.message_types import EventTypes
|
||||||
@ -29,10 +30,16 @@ class TagController(BaseUserController):
|
|||||||
def mixins(self):
|
def mixins(self):
|
||||||
return HttpRepo(self.repo, self.deps.logger)
|
return HttpRepo(self.repo, self.deps.logger)
|
||||||
|
|
||||||
@router.get("")
|
@router.get("", response_model=RecipeTagPagination)
|
||||||
async def get_all(self):
|
async def get_all(self, q: PaginationQuery = Depends(PaginationQuery)):
|
||||||
"""Returns a list of available tags in the database"""
|
"""Returns a list of available tags in the database"""
|
||||||
return self.repo.get_all(override=RecipeTag)
|
response = self.repo.page_all(
|
||||||
|
pagination=q,
|
||||||
|
override=RecipeTag,
|
||||||
|
)
|
||||||
|
|
||||||
|
response.set_pagination_guides(router.url_path_for("get_all"), q.dict())
|
||||||
|
return response
|
||||||
|
|
||||||
@router.get("/empty")
|
@router.get("/empty")
|
||||||
def get_empty_tags(self):
|
def get_empty_tags(self):
|
||||||
|
@ -7,9 +7,9 @@ from mealie.routes._base.base_controllers import BaseUserController
|
|||||||
from mealie.routes._base.controller import controller
|
from mealie.routes._base.controller import 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.query import GetAll
|
from mealie.schema.recipe.recipe import RecipeTool, RecipeToolPagination
|
||||||
from mealie.schema.recipe.recipe import RecipeTool
|
|
||||||
from mealie.schema.recipe.recipe_tool import RecipeToolCreate, RecipeToolResponse, RecipeToolSave
|
from mealie.schema.recipe.recipe_tool import RecipeToolCreate, RecipeToolResponse, RecipeToolSave
|
||||||
|
from mealie.schema.response.pagination import PaginationQuery
|
||||||
|
|
||||||
router = APIRouter(prefix="/tools", tags=["Organizer: Tools"])
|
router = APIRouter(prefix="/tools", tags=["Organizer: Tools"])
|
||||||
|
|
||||||
@ -24,9 +24,15 @@ class RecipeToolController(BaseUserController):
|
|||||||
def mixins(self) -> HttpRepo:
|
def mixins(self) -> HttpRepo:
|
||||||
return HttpRepo[RecipeToolCreate, RecipeTool, RecipeToolCreate](self.repo, self.deps.logger)
|
return HttpRepo[RecipeToolCreate, RecipeTool, RecipeToolCreate](self.repo, self.deps.logger)
|
||||||
|
|
||||||
@router.get("", response_model=list[RecipeTool])
|
@router.get("", response_model=RecipeToolPagination)
|
||||||
def get_all(self, q: GetAll = Depends(GetAll)):
|
def get_all(self, q: PaginationQuery = Depends(PaginationQuery)):
|
||||||
return self.repo.get_all(start=q.start, limit=q.limit, override=RecipeTool)
|
response = self.repo.page_all(
|
||||||
|
pagination=q,
|
||||||
|
override=RecipeTool,
|
||||||
|
)
|
||||||
|
|
||||||
|
response.set_pagination_guides(router.url_path_for("get_all"), q.dict())
|
||||||
|
return response
|
||||||
|
|
||||||
@router.post("", response_model=RecipeTool, status_code=201)
|
@router.post("", response_model=RecipeTool, status_code=201)
|
||||||
def create_one(self, data: RecipeToolCreate):
|
def create_one(self, data: RecipeToolCreate):
|
||||||
|
@ -20,9 +20,8 @@ from mealie.repos.repository_recipes import RepositoryRecipes
|
|||||||
from mealie.routes._base import BaseUserController, controller
|
from mealie.routes._base import BaseUserController, controller
|
||||||
from mealie.routes._base.mixins import HttpRepo
|
from mealie.routes._base.mixins import HttpRepo
|
||||||
from mealie.routes._base.routers import MealieCrudRoute, UserAPIRouter
|
from mealie.routes._base.routers import MealieCrudRoute, UserAPIRouter
|
||||||
from mealie.schema.query import GetAll
|
|
||||||
from mealie.schema.recipe import Recipe, RecipeImageTypes, ScrapeRecipe
|
from mealie.schema.recipe import Recipe, RecipeImageTypes, ScrapeRecipe
|
||||||
from mealie.schema.recipe.recipe import CreateRecipe, CreateRecipeByUrlBulk, RecipeSummary
|
from mealie.schema.recipe.recipe import CreateRecipe, CreateRecipeByUrlBulk, RecipePaginationQuery, RecipeSummary
|
||||||
from mealie.schema.recipe.recipe_asset import RecipeAsset
|
from mealie.schema.recipe.recipe_asset import RecipeAsset
|
||||||
from mealie.schema.recipe.recipe_scraper import ScrapeRecipeTest
|
from mealie.schema.recipe.recipe_scraper import ScrapeRecipeTest
|
||||||
from mealie.schema.recipe.request_helpers import RecipeZipTokenResponse, UpdateImageResponse
|
from mealie.schema.recipe.request_helpers import RecipeZipTokenResponse, UpdateImageResponse
|
||||||
@ -53,10 +52,6 @@ class BaseRecipeController(BaseUserController):
|
|||||||
return HttpRepo[CreateRecipe, Recipe, Recipe](self.repo, self.deps.logger)
|
return HttpRepo[CreateRecipe, Recipe, Recipe](self.repo, self.deps.logger)
|
||||||
|
|
||||||
|
|
||||||
class RecipeGetAll(GetAll):
|
|
||||||
load_food: bool = False
|
|
||||||
|
|
||||||
|
|
||||||
class FormatResponse(BaseModel):
|
class FormatResponse(BaseModel):
|
||||||
jjson: list[str] = Field(..., alias="json")
|
jjson: list[str] = Field(..., alias="json")
|
||||||
zip: list[str]
|
zip: list[str]
|
||||||
@ -196,18 +191,16 @@ class RecipeController(BaseRecipeController):
|
|||||||
# CRUD Operations
|
# CRUD Operations
|
||||||
|
|
||||||
@router.get("", response_model=list[RecipeSummary])
|
@router.get("", response_model=list[RecipeSummary])
|
||||||
def get_all(self, q: RecipeGetAll = Depends(RecipeGetAll)):
|
def get_all(self, q: RecipePaginationQuery = Depends(RecipePaginationQuery)):
|
||||||
items = self.repo.summary(
|
response = self.repo.page_all(
|
||||||
self.user.group_id,
|
pagination=q,
|
||||||
start=q.start,
|
load_food=q.load_food,
|
||||||
limit=q.limit,
|
|
||||||
load_foods=q.load_food,
|
|
||||||
order_by=q.order_by,
|
|
||||||
order_descending=q.order_descending,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
response.set_pagination_guides(router.url_path_for("get_all"), q.dict())
|
||||||
|
|
||||||
new_items = []
|
new_items = []
|
||||||
for item in items:
|
for item in response.items:
|
||||||
# Pydantic/FastAPI can't seem to serialize the ingredient field on thier own.
|
# Pydantic/FastAPI can't seem to serialize the ingredient field on thier own.
|
||||||
new_item = item.__dict__
|
new_item = item.__dict__
|
||||||
|
|
||||||
@ -216,10 +209,11 @@ class RecipeController(BaseRecipeController):
|
|||||||
|
|
||||||
new_items.append(new_item)
|
new_items.append(new_item)
|
||||||
|
|
||||||
json_compatible_item_data = jsonable_encoder(RecipeSummary.construct(**x) for x in new_items)
|
response.items = [RecipeSummary.construct(**x) for x in new_items]
|
||||||
|
json_compatible_response = jsonable_encoder(response)
|
||||||
|
|
||||||
# Response is returned directly, to avoid validation and improve performance
|
# Response is returned directly, to avoid validation and improve performance
|
||||||
return JSONResponse(content=json_compatible_item_data)
|
return JSONResponse(content=json_compatible_response)
|
||||||
|
|
||||||
@router.get("/{slug}", response_model=Recipe)
|
@router.get("/{slug}", response_model=Recipe)
|
||||||
def get_one(self, slug: str):
|
def get_one(self, slug: str):
|
||||||
|
@ -8,8 +8,14 @@ from mealie.routes._base.controller import controller
|
|||||||
from mealie.routes._base.mixins import HttpRepo
|
from mealie.routes._base.mixins import HttpRepo
|
||||||
from mealie.routes._base.routers import MealieCrudRoute
|
from mealie.routes._base.routers import MealieCrudRoute
|
||||||
from mealie.schema import mapper
|
from mealie.schema import mapper
|
||||||
from mealie.schema.query import GetAll
|
from mealie.schema.recipe.recipe_ingredient import (
|
||||||
from mealie.schema.recipe.recipe_ingredient import CreateIngredientFood, IngredientFood, MergeFood, SaveIngredientFood
|
CreateIngredientFood,
|
||||||
|
IngredientFood,
|
||||||
|
IngredientFoodPagination,
|
||||||
|
MergeFood,
|
||||||
|
SaveIngredientFood,
|
||||||
|
)
|
||||||
|
from mealie.schema.response.pagination import PaginationQuery
|
||||||
from mealie.schema.response.responses import SuccessResponse
|
from mealie.schema.response.responses import SuccessResponse
|
||||||
|
|
||||||
router = APIRouter(prefix="/foods", tags=["Recipes: Foods"], route_class=MealieCrudRoute)
|
router = APIRouter(prefix="/foods", tags=["Recipes: Foods"], route_class=MealieCrudRoute)
|
||||||
@ -38,9 +44,15 @@ class IngredientFoodsController(BaseUserController):
|
|||||||
self.deps.logger.error(e)
|
self.deps.logger.error(e)
|
||||||
raise HTTPException(500, "Failed to merge foods") from e
|
raise HTTPException(500, "Failed to merge foods") from e
|
||||||
|
|
||||||
@router.get("", response_model=list[IngredientFood])
|
@router.get("", response_model=IngredientFoodPagination)
|
||||||
def get_all(self, q: GetAll = Depends(GetAll)):
|
def get_all(self, q: PaginationQuery = Depends(PaginationQuery)):
|
||||||
return self.repo.get_all(start=q.start, limit=q.limit, order_by=q.order_by, order_descending=q.order_descending)
|
response = self.repo.page_all(
|
||||||
|
pagination=q,
|
||||||
|
override=IngredientFood,
|
||||||
|
)
|
||||||
|
|
||||||
|
response.set_pagination_guides(router.url_path_for("get_all"), q.dict())
|
||||||
|
return response
|
||||||
|
|
||||||
@router.post("", response_model=IngredientFood, status_code=201)
|
@router.post("", response_model=IngredientFood, status_code=201)
|
||||||
def create_one(self, data: CreateIngredientFood):
|
def create_one(self, data: CreateIngredientFood):
|
||||||
|
@ -8,8 +8,14 @@ from mealie.routes._base.controller import controller
|
|||||||
from mealie.routes._base.mixins import HttpRepo
|
from mealie.routes._base.mixins import HttpRepo
|
||||||
from mealie.routes._base.routers import MealieCrudRoute
|
from mealie.routes._base.routers import MealieCrudRoute
|
||||||
from mealie.schema import mapper
|
from mealie.schema import mapper
|
||||||
from mealie.schema.query import GetAll
|
from mealie.schema.recipe.recipe_ingredient import (
|
||||||
from mealie.schema.recipe.recipe_ingredient import CreateIngredientUnit, IngredientUnit, MergeUnit, SaveIngredientUnit
|
CreateIngredientUnit,
|
||||||
|
IngredientUnit,
|
||||||
|
IngredientUnitPagination,
|
||||||
|
MergeUnit,
|
||||||
|
SaveIngredientUnit,
|
||||||
|
)
|
||||||
|
from mealie.schema.response.pagination import PaginationQuery
|
||||||
from mealie.schema.response.responses import SuccessResponse
|
from mealie.schema.response.responses import SuccessResponse
|
||||||
|
|
||||||
router = APIRouter(prefix="/units", tags=["Recipes: Units"], route_class=MealieCrudRoute)
|
router = APIRouter(prefix="/units", tags=["Recipes: Units"], route_class=MealieCrudRoute)
|
||||||
@ -38,9 +44,15 @@ class IngredientUnitsController(BaseUserController):
|
|||||||
self.deps.logger.error(e)
|
self.deps.logger.error(e)
|
||||||
raise HTTPException(500, "Failed to merge units") from e
|
raise HTTPException(500, "Failed to merge units") from e
|
||||||
|
|
||||||
@router.get("", response_model=list[IngredientUnit])
|
@router.get("", response_model=IngredientUnitPagination)
|
||||||
def get_all(self, q: GetAll = Depends(GetAll)):
|
def get_all(self, q: PaginationQuery = Depends(PaginationQuery)):
|
||||||
return self.repo.get_all(start=q.start, limit=q.limit, order_by=q.order_by, order_descending=q.order_descending)
|
response = self.repo.page_all(
|
||||||
|
pagination=q,
|
||||||
|
override=IngredientUnit,
|
||||||
|
)
|
||||||
|
|
||||||
|
response.set_pagination_guides(router.url_path_for("get_all"), q.dict())
|
||||||
|
return response
|
||||||
|
|
||||||
@router.post("", response_model=IngredientUnit, status_code=201)
|
@router.post("", response_model=IngredientUnit, status_code=201)
|
||||||
def create_one(self, data: CreateIngredientUnit):
|
def create_one(self, data: CreateIngredientUnit):
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from fastapi import HTTPException, status
|
from fastapi import Depends, HTTPException, status
|
||||||
from pydantic import UUID4
|
from pydantic import UUID4
|
||||||
|
|
||||||
from mealie.core.security import hash_password, verify_password
|
from mealie.core.security import hash_password, verify_password
|
||||||
@ -8,7 +8,9 @@ from mealie.routes._base.mixins import HttpRepo
|
|||||||
from mealie.routes._base.routers import AdminAPIRouter, UserAPIRouter
|
from mealie.routes._base.routers import AdminAPIRouter, UserAPIRouter
|
||||||
from mealie.routes.users._helpers import assert_user_change_allowed
|
from mealie.routes.users._helpers import assert_user_change_allowed
|
||||||
from mealie.schema.response import ErrorResponse, SuccessResponse
|
from mealie.schema.response import ErrorResponse, SuccessResponse
|
||||||
|
from mealie.schema.response.pagination import PaginationQuery
|
||||||
from mealie.schema.user import ChangePassword, UserBase, UserIn, UserOut
|
from mealie.schema.user import ChangePassword, UserBase, UserIn, UserOut
|
||||||
|
from mealie.schema.user.user import UserPagination
|
||||||
|
|
||||||
user_router = UserAPIRouter(prefix="/users", tags=["Users: CRUD"])
|
user_router = UserAPIRouter(prefix="/users", tags=["Users: CRUD"])
|
||||||
admin_router = AdminAPIRouter(prefix="/users", tags=["Users: Admin CRUD"])
|
admin_router = AdminAPIRouter(prefix="/users", tags=["Users: Admin CRUD"])
|
||||||
@ -20,9 +22,15 @@ class AdminUserController(BaseAdminController):
|
|||||||
def mixins(self) -> HttpRepo:
|
def mixins(self) -> HttpRepo:
|
||||||
return HttpRepo[UserIn, UserOut, UserBase](self.repos.users, self.deps.logger)
|
return HttpRepo[UserIn, UserOut, UserBase](self.repos.users, self.deps.logger)
|
||||||
|
|
||||||
@admin_router.get("", response_model=list[UserOut])
|
@admin_router.get("", response_model=UserPagination)
|
||||||
def get_all_users(self):
|
def get_all(self, q: PaginationQuery = Depends(PaginationQuery)):
|
||||||
return self.repos.users.get_all()
|
response = self.repos.users.page_all(
|
||||||
|
pagination=q,
|
||||||
|
override=UserOut,
|
||||||
|
)
|
||||||
|
|
||||||
|
response.set_pagination_guides(admin_router.url_path_for("get_all"), q.dict())
|
||||||
|
return response
|
||||||
|
|
||||||
@admin_router.post("", response_model=UserOut, status_code=201)
|
@admin_router.post("", response_model=UserOut, status_code=201)
|
||||||
def create_user(self, new_user: UserIn):
|
def create_user(self, new_user: UserIn):
|
||||||
|
@ -3,6 +3,7 @@ from slugify import slugify
|
|||||||
|
|
||||||
from mealie.schema._mealie import MealieModel
|
from mealie.schema._mealie import MealieModel
|
||||||
from mealie.schema.recipe.recipe import RecipeSummary, RecipeTool
|
from mealie.schema.recipe.recipe import RecipeSummary, RecipeTool
|
||||||
|
from mealie.schema.response.pagination import PaginationBase
|
||||||
|
|
||||||
from ..recipe.recipe_category import CategoryBase, TagBase
|
from ..recipe.recipe_category import CategoryBase, TagBase
|
||||||
|
|
||||||
@ -51,6 +52,10 @@ class ReadCookBook(UpdateCookBook):
|
|||||||
orm_mode = True
|
orm_mode = True
|
||||||
|
|
||||||
|
|
||||||
|
class CookBookPagination(PaginationBase):
|
||||||
|
items: list[ReadCookBook]
|
||||||
|
|
||||||
|
|
||||||
class RecipeCookBook(ReadCookBook):
|
class RecipeCookBook(ReadCookBook):
|
||||||
group_id: UUID4
|
group_id: UUID4
|
||||||
recipes: list[RecipeSummary]
|
recipes: list[RecipeSummary]
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
from pydantic import UUID4, NoneStr
|
from pydantic import UUID4, NoneStr
|
||||||
|
|
||||||
from mealie.schema._mealie import MealieModel
|
from mealie.schema._mealie import MealieModel
|
||||||
|
from mealie.schema.response.pagination import PaginationBase
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Group Events Notifier Options
|
# Group Events Notifier Options
|
||||||
@ -83,6 +84,10 @@ class GroupEventNotifierOut(MealieModel):
|
|||||||
orm_mode = True
|
orm_mode = True
|
||||||
|
|
||||||
|
|
||||||
|
class GroupEventPagination(PaginationBase):
|
||||||
|
items: list[GroupEventNotifierOut]
|
||||||
|
|
||||||
|
|
||||||
class GroupEventNotifierPrivate(GroupEventNotifierOut):
|
class GroupEventNotifierPrivate(GroupEventNotifierOut):
|
||||||
apprise_url: str
|
apprise_url: str
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ from pydantic import UUID4
|
|||||||
|
|
||||||
from mealie.schema._mealie import MealieModel
|
from mealie.schema._mealie import MealieModel
|
||||||
from mealie.schema.recipe.recipe_ingredient import IngredientFood, IngredientUnit
|
from mealie.schema.recipe.recipe_ingredient import IngredientFood, IngredientUnit
|
||||||
|
from mealie.schema.response.pagination import PaginationBase
|
||||||
|
|
||||||
|
|
||||||
class ShoppingListItemRecipeRef(MealieModel):
|
class ShoppingListItemRecipeRef(MealieModel):
|
||||||
@ -84,6 +85,10 @@ class ShoppingListSummary(ShoppingListSave):
|
|||||||
orm_mode = True
|
orm_mode = True
|
||||||
|
|
||||||
|
|
||||||
|
class ShoppingListPagination(PaginationBase):
|
||||||
|
items: list[ShoppingListSummary]
|
||||||
|
|
||||||
|
|
||||||
class ShoppingListUpdate(ShoppingListSummary):
|
class ShoppingListUpdate(ShoppingListSummary):
|
||||||
list_items: list[ShoppingListItemOut] = []
|
list_items: list[ShoppingListItemOut] = []
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ from pydantic import UUID4, validator
|
|||||||
from pydantic.datetime_parse import parse_datetime
|
from pydantic.datetime_parse import parse_datetime
|
||||||
|
|
||||||
from mealie.schema._mealie import MealieModel
|
from mealie.schema._mealie import MealieModel
|
||||||
|
from mealie.schema.response.pagination import PaginationBase
|
||||||
|
|
||||||
|
|
||||||
class WebhookType(str, enum.Enum):
|
class WebhookType(str, enum.Enum):
|
||||||
@ -57,3 +58,7 @@ class ReadWebhook(SaveWebhook):
|
|||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
orm_mode = True
|
orm_mode = True
|
||||||
|
|
||||||
|
|
||||||
|
class WebhookPagination(PaginationBase):
|
||||||
|
items: list[ReadWebhook]
|
||||||
|
@ -3,6 +3,7 @@ from __future__ import annotations
|
|||||||
from pydantic import UUID4
|
from pydantic import UUID4
|
||||||
|
|
||||||
from mealie.schema._mealie import MealieModel
|
from mealie.schema._mealie import MealieModel
|
||||||
|
from mealie.schema.response.pagination import PaginationBase
|
||||||
|
|
||||||
|
|
||||||
class MultiPurposeLabelCreate(MealieModel):
|
class MultiPurposeLabelCreate(MealieModel):
|
||||||
@ -25,6 +26,10 @@ class MultiPurposeLabelSummary(MultiPurposeLabelUpdate):
|
|||||||
orm_mode = True
|
orm_mode = True
|
||||||
|
|
||||||
|
|
||||||
|
class MultiPurposeLabelPagination(PaginationBase):
|
||||||
|
items: list[MultiPurposeLabelSummary]
|
||||||
|
|
||||||
|
|
||||||
class MultiPurposeLabelOut(MultiPurposeLabelUpdate):
|
class MultiPurposeLabelOut(MultiPurposeLabelUpdate):
|
||||||
# shopping_list_items: list[ShoppingListItemOut] = []
|
# shopping_list_items: list[ShoppingListItemOut] = []
|
||||||
# foods: list[IngredientFood] = []
|
# foods: list[IngredientFood] = []
|
||||||
|
@ -4,6 +4,7 @@ from enum import Enum
|
|||||||
from pydantic import UUID4
|
from pydantic import UUID4
|
||||||
|
|
||||||
from mealie.schema._mealie import MealieModel
|
from mealie.schema._mealie import MealieModel
|
||||||
|
from mealie.schema.response.pagination import PaginationBase
|
||||||
|
|
||||||
|
|
||||||
class Category(MealieModel):
|
class Category(MealieModel):
|
||||||
@ -63,3 +64,7 @@ class PlanRulesOut(PlanRulesSave):
|
|||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
orm_mode = True
|
orm_mode = True
|
||||||
|
|
||||||
|
|
||||||
|
class PlanRulesPagination(PaginationBase):
|
||||||
|
items: list[PlanRulesOut]
|
||||||
|
@ -1,10 +0,0 @@
|
|||||||
from typing import Optional
|
|
||||||
|
|
||||||
from mealie.schema._mealie import MealieModel
|
|
||||||
|
|
||||||
|
|
||||||
class GetAll(MealieModel):
|
|
||||||
start: int = 0
|
|
||||||
limit: int = 999
|
|
||||||
order_by: Optional[str]
|
|
||||||
order_descending: Optional[bool] = True
|
|
@ -12,6 +12,7 @@ from slugify import slugify
|
|||||||
from mealie.core.config import get_app_dirs
|
from mealie.core.config import get_app_dirs
|
||||||
from mealie.db.models.recipe.recipe import RecipeModel
|
from mealie.db.models.recipe.recipe import RecipeModel
|
||||||
from mealie.schema._mealie import MealieModel
|
from mealie.schema._mealie import MealieModel
|
||||||
|
from mealie.schema.response.pagination import PaginationBase, PaginationQuery
|
||||||
|
|
||||||
from .recipe_asset import RecipeAsset
|
from .recipe_asset import RecipeAsset
|
||||||
from .recipe_comments import RecipeCommentOut
|
from .recipe_comments import RecipeCommentOut
|
||||||
@ -32,15 +33,27 @@ class RecipeTag(MealieModel):
|
|||||||
orm_mode = True
|
orm_mode = True
|
||||||
|
|
||||||
|
|
||||||
|
class RecipeTagPagination(PaginationBase):
|
||||||
|
items: list[RecipeTag]
|
||||||
|
|
||||||
|
|
||||||
class RecipeCategory(RecipeTag):
|
class RecipeCategory(RecipeTag):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class RecipeCategoryPagination(PaginationBase):
|
||||||
|
items: list[RecipeCategory]
|
||||||
|
|
||||||
|
|
||||||
class RecipeTool(RecipeTag):
|
class RecipeTool(RecipeTag):
|
||||||
id: UUID4
|
id: UUID4
|
||||||
on_hand: bool = False
|
on_hand: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
class RecipeToolPagination(PaginationBase):
|
||||||
|
items: list[RecipeTool]
|
||||||
|
|
||||||
|
|
||||||
class CreateRecipeBulk(BaseModel):
|
class CreateRecipeBulk(BaseModel):
|
||||||
url: str
|
url: str
|
||||||
categories: list[RecipeCategory] = None
|
categories: list[RecipeCategory] = None
|
||||||
@ -114,6 +127,14 @@ class RecipeSummary(MealieModel):
|
|||||||
return user_id
|
return user_id
|
||||||
|
|
||||||
|
|
||||||
|
class RecipePaginationQuery(PaginationQuery):
|
||||||
|
load_food: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
class RecipePagination(PaginationBase):
|
||||||
|
items: list[RecipeSummary]
|
||||||
|
|
||||||
|
|
||||||
class Recipe(RecipeSummary):
|
class Recipe(RecipeSummary):
|
||||||
recipe_ingredient: list[RecipeIngredient] = []
|
recipe_ingredient: list[RecipeIngredient] = []
|
||||||
recipe_instructions: Optional[list[RecipeStep]] = []
|
recipe_instructions: Optional[list[RecipeStep]] = []
|
||||||
|
@ -4,6 +4,7 @@ from typing import Optional
|
|||||||
from pydantic import UUID4
|
from pydantic import UUID4
|
||||||
|
|
||||||
from mealie.schema._mealie import MealieModel
|
from mealie.schema._mealie import MealieModel
|
||||||
|
from mealie.schema.response.pagination import PaginationBase
|
||||||
|
|
||||||
|
|
||||||
class UserBase(MealieModel):
|
class UserBase(MealieModel):
|
||||||
@ -39,3 +40,7 @@ class RecipeCommentOut(RecipeCommentCreate):
|
|||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
orm_mode = True
|
orm_mode = True
|
||||||
|
|
||||||
|
|
||||||
|
class RecipeCommentPagination(PaginationBase):
|
||||||
|
items: list[RecipeCommentOut]
|
||||||
|
@ -8,6 +8,7 @@ from pydantic import UUID4, Field, validator
|
|||||||
|
|
||||||
from mealie.schema._mealie import MealieModel
|
from mealie.schema._mealie import MealieModel
|
||||||
from mealie.schema._mealie.types import NoneFloat
|
from mealie.schema._mealie.types import NoneFloat
|
||||||
|
from mealie.schema.response.pagination import PaginationBase
|
||||||
|
|
||||||
|
|
||||||
class UnitFoodBase(MealieModel):
|
class UnitFoodBase(MealieModel):
|
||||||
@ -31,6 +32,10 @@ class IngredientFood(CreateIngredientFood):
|
|||||||
orm_mode = True
|
orm_mode = True
|
||||||
|
|
||||||
|
|
||||||
|
class IngredientFoodPagination(PaginationBase):
|
||||||
|
items: list[IngredientFood]
|
||||||
|
|
||||||
|
|
||||||
class CreateIngredientUnit(UnitFoodBase):
|
class CreateIngredientUnit(UnitFoodBase):
|
||||||
fraction: bool = True
|
fraction: bool = True
|
||||||
abbreviation: str = ""
|
abbreviation: str = ""
|
||||||
@ -48,6 +53,10 @@ class IngredientUnit(CreateIngredientUnit):
|
|||||||
orm_mode = True
|
orm_mode = True
|
||||||
|
|
||||||
|
|
||||||
|
class IngredientUnitPagination(PaginationBase):
|
||||||
|
items: list[IngredientUnit]
|
||||||
|
|
||||||
|
|
||||||
class RecipeIngredient(MealieModel):
|
class RecipeIngredient(MealieModel):
|
||||||
title: Optional[str]
|
title: Optional[str]
|
||||||
note: Optional[str]
|
note: Optional[str]
|
||||||
|
@ -1,9 +1,13 @@
|
|||||||
import enum
|
import enum
|
||||||
from typing import Generic, TypeVar
|
from typing import Any, Generic, TypeVar
|
||||||
|
from urllib.parse import parse_qs, urlencode, urlsplit, urlunsplit
|
||||||
|
|
||||||
|
from humps import camelize
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from pydantic.generics import GenericModel
|
from pydantic.generics import GenericModel
|
||||||
|
|
||||||
|
from mealie.schema._mealie import MealieModel
|
||||||
|
|
||||||
DataT = TypeVar("DataT", bound=BaseModel)
|
DataT = TypeVar("DataT", bound=BaseModel)
|
||||||
|
|
||||||
|
|
||||||
@ -12,11 +16,11 @@ class OrderDirection(str, enum.Enum):
|
|||||||
desc = "desc"
|
desc = "desc"
|
||||||
|
|
||||||
|
|
||||||
class PaginationQuery(BaseModel):
|
class PaginationQuery(MealieModel):
|
||||||
page: int = 1
|
page: int = 1
|
||||||
|
per_page: int = 50
|
||||||
order_by: str = "created_at"
|
order_by: str = "created_at"
|
||||||
order_direction: OrderDirection = OrderDirection.desc
|
order_direction: OrderDirection = OrderDirection.desc
|
||||||
per_page: int = 50
|
|
||||||
|
|
||||||
|
|
||||||
class PaginationBase(GenericModel, Generic[DataT]):
|
class PaginationBase(GenericModel, Generic[DataT]):
|
||||||
@ -24,4 +28,45 @@ class PaginationBase(GenericModel, Generic[DataT]):
|
|||||||
per_page: int = 10
|
per_page: int = 10
|
||||||
total: int = 0
|
total: int = 0
|
||||||
total_pages: int = 0
|
total_pages: int = 0
|
||||||
data: list[DataT]
|
items: list[DataT]
|
||||||
|
next: str | None
|
||||||
|
previous: str | None
|
||||||
|
|
||||||
|
def _set_next(self, route: str, query_params: dict[str, Any]) -> None:
|
||||||
|
if self.page >= self.total_pages:
|
||||||
|
self.next = None
|
||||||
|
return
|
||||||
|
|
||||||
|
# combine params with base route
|
||||||
|
query_params["page"] = self.page + 1
|
||||||
|
self.next = PaginationBase.merge_query_parameters(route, query_params)
|
||||||
|
|
||||||
|
def _set_prev(self, route: str, query_params: dict[str, Any]) -> None:
|
||||||
|
if self.page <= 1:
|
||||||
|
self.previous = None
|
||||||
|
return
|
||||||
|
|
||||||
|
# combine params with base route
|
||||||
|
query_params["page"] = self.page - 1
|
||||||
|
self.previous = PaginationBase.merge_query_parameters(route, query_params)
|
||||||
|
|
||||||
|
def set_pagination_guides(self, route: str, query_params: dict[str, Any] | None) -> None:
|
||||||
|
if not query_params:
|
||||||
|
query_params = {}
|
||||||
|
|
||||||
|
query_params = camelize(query_params)
|
||||||
|
|
||||||
|
# sanitize user input
|
||||||
|
self.page = max(self.page, 1)
|
||||||
|
self._set_next(route, query_params)
|
||||||
|
self._set_prev(route, query_params)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def merge_query_parameters(url: str, params: dict[str, Any]):
|
||||||
|
scheme, netloc, path, query_string, fragment = urlsplit(url)
|
||||||
|
|
||||||
|
query_params = parse_qs(query_string)
|
||||||
|
query_params.update(params)
|
||||||
|
new_query_string = urlencode(query_params, doseq=True)
|
||||||
|
|
||||||
|
return urlunsplit((scheme, netloc, path, new_query_string, fragment))
|
||||||
|
@ -5,6 +5,7 @@ from uuid import UUID
|
|||||||
from pydantic import Field
|
from pydantic import Field
|
||||||
|
|
||||||
from mealie.schema._mealie import MealieModel
|
from mealie.schema._mealie import MealieModel
|
||||||
|
from mealie.schema.response.pagination import PaginationBase
|
||||||
|
|
||||||
|
|
||||||
class ServerTaskNames(str, enum.Enum):
|
class ServerTaskNames(str, enum.Enum):
|
||||||
@ -45,3 +46,7 @@ class ServerTask(ServerTaskCreate):
|
|||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
orm_mode = True
|
orm_mode = True
|
||||||
|
|
||||||
|
|
||||||
|
class ServerTaskPagination(PaginationBase):
|
||||||
|
items: list[ServerTask]
|
||||||
|
@ -11,6 +11,7 @@ from mealie.db.models.users import User
|
|||||||
from mealie.schema._mealie import MealieModel
|
from mealie.schema._mealie import MealieModel
|
||||||
from mealie.schema.group.group_preferences import ReadGroupPreferences
|
from mealie.schema.group.group_preferences import ReadGroupPreferences
|
||||||
from mealie.schema.recipe import RecipeSummary
|
from mealie.schema.recipe import RecipeSummary
|
||||||
|
from mealie.schema.response.pagination import PaginationBase
|
||||||
|
|
||||||
from ..recipe import CategoryBase
|
from ..recipe import CategoryBase
|
||||||
|
|
||||||
@ -113,6 +114,10 @@ class UserOut(UserBase):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class UserPagination(PaginationBase):
|
||||||
|
items: list[UserOut]
|
||||||
|
|
||||||
|
|
||||||
class UserFavorites(UserBase):
|
class UserFavorites(UserBase):
|
||||||
favorite_recipes: list[RecipeSummary] = [] # type: ignore
|
favorite_recipes: list[RecipeSummary] = [] # type: ignore
|
||||||
|
|
||||||
@ -180,6 +185,10 @@ class GroupInDB(UpdateGroup):
|
|||||||
return GroupInDB.get_export_directory(self.id)
|
return GroupInDB.get_export_directory(self.id)
|
||||||
|
|
||||||
|
|
||||||
|
class GroupPagination(PaginationBase):
|
||||||
|
items: list[GroupInDB]
|
||||||
|
|
||||||
|
|
||||||
class LongLiveTokenInDB(CreateToken):
|
class LongLiveTokenInDB(CreateToken):
|
||||||
id: int
|
id: int
|
||||||
user: PrivateUser
|
user: PrivateUser
|
||||||
|
@ -10,13 +10,13 @@ class Routes:
|
|||||||
|
|
||||||
def test_admin_server_tasks_test_and_get(api_client: TestClient, admin_user: TestUser):
|
def test_admin_server_tasks_test_and_get(api_client: TestClient, admin_user: TestUser):
|
||||||
# Bootstrap Timer
|
# Bootstrap Timer
|
||||||
BackgroundExecutor.sleep_time = 0.1
|
BackgroundExecutor.sleep_time = 1
|
||||||
|
|
||||||
response = api_client.post(Routes.base, headers=admin_user.token)
|
response = api_client.post(Routes.base, headers=admin_user.token)
|
||||||
assert response.status_code == 201
|
assert response.status_code == 201
|
||||||
|
|
||||||
response = api_client.get(Routes.base, headers=admin_user.token)
|
response = api_client.get(Routes.base, headers=admin_user.token)
|
||||||
as_dict = response.json()
|
as_dict = response.json()["items"]
|
||||||
|
|
||||||
assert len(as_dict) == 1
|
assert len(as_dict) == 1
|
||||||
|
|
||||||
|
@ -111,7 +111,7 @@ def test_update_cookbooks_many(api_client: TestClient, unique_user: TestUser, co
|
|||||||
|
|
||||||
known_ids = [x.id for x in cookbooks]
|
known_ids = [x.id for x in cookbooks]
|
||||||
|
|
||||||
server_ids = [x["id"] for x in response.json()]
|
server_ids = [x["id"] for x in response.json()["items"]]
|
||||||
|
|
||||||
for know in known_ids: # Hacky check, because other tests don't cleanup after themselves :(
|
for know in known_ids: # Hacky check, because other tests don't cleanup after themselves :(
|
||||||
assert str(know) in server_ids
|
assert str(know) in server_ids
|
||||||
|
@ -22,7 +22,7 @@ class Routes:
|
|||||||
def test_shopping_lists_get_all(api_client: TestClient, unique_user: TestUser, shopping_lists: list[ShoppingListOut]):
|
def test_shopping_lists_get_all(api_client: TestClient, unique_user: TestUser, shopping_lists: list[ShoppingListOut]):
|
||||||
all_lists = api_client.get(Routes.base, headers=unique_user.token)
|
all_lists = api_client.get(Routes.base, headers=unique_user.token)
|
||||||
assert all_lists.status_code == 200
|
assert all_lists.status_code == 200
|
||||||
all_lists = all_lists.json()
|
all_lists = all_lists.json()["items"]
|
||||||
|
|
||||||
assert len(all_lists) == len(shopping_lists)
|
assert len(all_lists) == len(shopping_lists)
|
||||||
|
|
||||||
|
@ -44,7 +44,7 @@ def test_get_all_only_includes_group_recipes(api_client: TestClient, unique_user
|
|||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
||||||
recipes = response.json()
|
recipes = response.json()["items"]
|
||||||
|
|
||||||
assert len(recipes) == 5
|
assert len(recipes) == 5
|
||||||
|
|
||||||
|
@ -47,7 +47,7 @@ def test_multitenant_cases_get_all(
|
|||||||
response = test_case.get_all(token)
|
response = test_case.get_all(token)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
||||||
data = response.json()
|
data = response.json()["items"]
|
||||||
|
|
||||||
assert len(data) == len(item_ids)
|
assert len(data) == len(item_ids)
|
||||||
|
|
||||||
@ -84,7 +84,7 @@ def test_multitenant_cases_same_named_resources(
|
|||||||
response = test_case.get_all(token)
|
response = test_case.get_all(token)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
||||||
data = response.json()
|
data = response.json()["items"]
|
||||||
|
|
||||||
assert len(data) == len(item_ids)
|
assert len(data) == len(item_ids)
|
||||||
|
|
||||||
|
@ -1,3 +1,8 @@
|
|||||||
|
from random import randint
|
||||||
|
from urllib.parse import parse_qsl, urlsplit
|
||||||
|
|
||||||
|
from humps import camelize
|
||||||
|
|
||||||
from mealie.repos.repository_factory import AllRepositories
|
from mealie.repos.repository_factory import AllRepositories
|
||||||
from mealie.schema.response.pagination import PaginationQuery
|
from mealie.schema.response.pagination import PaginationQuery
|
||||||
from mealie.services.seeder.seeder_service import SeederService
|
from mealie.services.seeder.seeder_service import SeederService
|
||||||
@ -21,18 +26,98 @@ def test_repository_pagination(database: AllRepositories, unique_user: TestUser)
|
|||||||
seen = []
|
seen = []
|
||||||
|
|
||||||
for _ in range(10):
|
for _ in range(10):
|
||||||
results = foods_repo.pagination(query)
|
results = foods_repo.page_all(query)
|
||||||
|
|
||||||
assert len(results.data) == 10
|
assert len(results.items) == 10
|
||||||
|
|
||||||
for result in results.data:
|
for result in results.items:
|
||||||
assert result.id not in seen
|
assert result.id not in seen
|
||||||
|
|
||||||
seen += [result.id for result in results.data]
|
seen += [result.id for result in results.items]
|
||||||
|
|
||||||
query.page += 1
|
query.page += 1
|
||||||
|
|
||||||
results = foods_repo.pagination(query)
|
results = foods_repo.page_all(query)
|
||||||
|
|
||||||
for result in results.data:
|
for result in results.items:
|
||||||
assert result.id not in seen
|
assert result.id not in seen
|
||||||
|
|
||||||
|
|
||||||
|
def test_pagination_response_and_metadata(database: AllRepositories, unique_user: TestUser):
|
||||||
|
group = database.groups.get_one(unique_user.group_id)
|
||||||
|
|
||||||
|
seeder = SeederService(database, None, group)
|
||||||
|
seeder.seed_foods("en-US")
|
||||||
|
|
||||||
|
foods_repo = database.ingredient_foods.by_group(unique_user.group_id) # type: ignore
|
||||||
|
|
||||||
|
# this should get all results
|
||||||
|
query = PaginationQuery(
|
||||||
|
page=1,
|
||||||
|
per_page=-1,
|
||||||
|
)
|
||||||
|
|
||||||
|
all_results = foods_repo.page_all(query)
|
||||||
|
assert all_results.total == len(all_results.items)
|
||||||
|
|
||||||
|
# this should get the last page of results
|
||||||
|
query = PaginationQuery(
|
||||||
|
page=-1,
|
||||||
|
per_page=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
last_page_of_results = foods_repo.page_all(query)
|
||||||
|
assert last_page_of_results.page == last_page_of_results.total_pages
|
||||||
|
assert last_page_of_results.items[-1] == all_results.items[-1]
|
||||||
|
|
||||||
|
|
||||||
|
def test_pagination_guides(database: AllRepositories, unique_user: TestUser):
|
||||||
|
group = database.groups.get_one(unique_user.group_id)
|
||||||
|
|
||||||
|
seeder = SeederService(database, None, group)
|
||||||
|
seeder.seed_foods("en-US")
|
||||||
|
|
||||||
|
foods_repo = database.ingredient_foods.by_group(unique_user.group_id) # type: ignore
|
||||||
|
foods_route = (
|
||||||
|
"/foods" # this doesn't actually have to be accurate, it's just a placeholder to test for query params
|
||||||
|
)
|
||||||
|
|
||||||
|
query = PaginationQuery(
|
||||||
|
page=1,
|
||||||
|
per_page=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
first_page_of_results = foods_repo.page_all(query)
|
||||||
|
first_page_of_results.set_pagination_guides(foods_route, query.dict())
|
||||||
|
assert first_page_of_results.next is not None
|
||||||
|
assert first_page_of_results.previous is None
|
||||||
|
|
||||||
|
query = PaginationQuery(
|
||||||
|
page=-1,
|
||||||
|
per_page=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
last_page_of_results = foods_repo.page_all(query)
|
||||||
|
last_page_of_results.set_pagination_guides(foods_route, query.dict())
|
||||||
|
assert last_page_of_results.next is None
|
||||||
|
assert last_page_of_results.previous is not None
|
||||||
|
|
||||||
|
random_page = randint(2, first_page_of_results.total_pages - 1)
|
||||||
|
query = PaginationQuery(
|
||||||
|
page=random_page,
|
||||||
|
per_page=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
random_page_of_results = foods_repo.page_all(query)
|
||||||
|
random_page_of_results.set_pagination_guides(foods_route, query.dict())
|
||||||
|
|
||||||
|
next_params = dict(parse_qsl(urlsplit(random_page_of_results.next).query))
|
||||||
|
assert int(next_params["page"]) == random_page + 1
|
||||||
|
|
||||||
|
prev_params = dict(parse_qsl(urlsplit(random_page_of_results.previous).query))
|
||||||
|
assert int(prev_params["page"]) == random_page - 1
|
||||||
|
|
||||||
|
source_params = camelize(query.dict())
|
||||||
|
for source_param in source_params:
|
||||||
|
assert source_param in next_params
|
||||||
|
assert source_param in prev_params
|
||||||
|
Loading…
x
Reference in New Issue
Block a user