mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-05-31 20:25:14 -04:00
feat: merge food into another (#1143)
* setup food repository * add merge route and payloads * remove type checking * generate types * implement merge dialog * food repo tests * split install from workflow * bum dependencies * revert changes * update copy * refactor URLs to avoid incorrect template being used * stick advanced items under developer mode * use utility component for advanced feature
This commit is contained in:
parent
10784b6e24
commit
b93dae109e
2
.github/workflows/backend-tests.yml
vendored
2
.github/workflows/backend-tests.yml
vendored
@ -32,7 +32,6 @@ jobs:
|
|||||||
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
|
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
|
||||||
ports:
|
ports:
|
||||||
- 5432:5432
|
- 5432:5432
|
||||||
|
|
||||||
# Steps
|
# Steps
|
||||||
steps:
|
steps:
|
||||||
#----------------------------------------------
|
#----------------------------------------------
|
||||||
@ -70,6 +69,7 @@ jobs:
|
|||||||
poetry install
|
poetry install
|
||||||
poetry add "psycopg2-binary==2.8.6"
|
poetry add "psycopg2-binary==2.8.6"
|
||||||
if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
|
if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
|
||||||
|
|
||||||
#----------------------------------------------
|
#----------------------------------------------
|
||||||
# run test suite
|
# run test suite
|
||||||
#----------------------------------------------
|
#----------------------------------------------
|
||||||
|
34
.vscode/settings.json
vendored
34
.vscode/settings.json
vendored
@ -5,11 +5,7 @@
|
|||||||
"backend",
|
"backend",
|
||||||
"code-generation"
|
"code-generation"
|
||||||
],
|
],
|
||||||
"cSpell.enableFiletypes": [
|
"cSpell.enableFiletypes": ["!javascript", "!python", "!yaml"],
|
||||||
"!javascript",
|
|
||||||
"!python",
|
|
||||||
"!yaml"
|
|
||||||
],
|
|
||||||
"cSpell.words": [
|
"cSpell.words": [
|
||||||
"chowdown",
|
"chowdown",
|
||||||
"compression",
|
"compression",
|
||||||
@ -24,9 +20,7 @@
|
|||||||
"source.organizeImports": false
|
"source.organizeImports": false
|
||||||
},
|
},
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"eslint.workingDirectories": [
|
"eslint.workingDirectories": ["./frontend"],
|
||||||
"./frontend"
|
|
||||||
],
|
|
||||||
"files.exclude": {
|
"files.exclude": {
|
||||||
"**/__pycache__": true,
|
"**/__pycache__": true,
|
||||||
"**/.DS_Store": true,
|
"**/.DS_Store": true,
|
||||||
@ -35,9 +29,7 @@
|
|||||||
"**/.svn": true,
|
"**/.svn": true,
|
||||||
"**/CVS": true
|
"**/CVS": true
|
||||||
},
|
},
|
||||||
"i18n-ally.enabledFrameworks": [
|
"i18n-ally.enabledFrameworks": ["vue"],
|
||||||
"vue"
|
|
||||||
],
|
|
||||||
"i18n-ally.keystyle": "nested",
|
"i18n-ally.keystyle": "nested",
|
||||||
"i18n-ally.localesPaths": "frontend/lang/messages",
|
"i18n-ally.localesPaths": "frontend/lang/messages",
|
||||||
"i18n-ally.sourceLanguage": "en-US",
|
"i18n-ally.sourceLanguage": "en-US",
|
||||||
@ -45,26 +37,14 @@
|
|||||||
"python.linting.enabled": true,
|
"python.linting.enabled": true,
|
||||||
"python.linting.flake8Enabled": true,
|
"python.linting.flake8Enabled": true,
|
||||||
"python.linting.pylintEnabled": false,
|
"python.linting.pylintEnabled": false,
|
||||||
"python.linting.pylintArgs": [
|
"python.linting.pylintArgs": ["--rcfile=${workspaceFolder}/.pylintrc"],
|
||||||
"--rcfile=${workspaceFolder}/.pylintrc"
|
|
||||||
],
|
|
||||||
"python.testing.autoTestDiscoverOnSaveEnabled": false,
|
"python.testing.autoTestDiscoverOnSaveEnabled": false,
|
||||||
"python.testing.pytestArgs": [
|
"python.testing.pytestArgs": ["tests"],
|
||||||
"tests"
|
|
||||||
],
|
|
||||||
"python.testing.pytestEnabled": true,
|
"python.testing.pytestEnabled": true,
|
||||||
"python.testing.unittestEnabled": false,
|
"python.testing.unittestEnabled": false,
|
||||||
"python.analysis.typeCheckingMode": "basic",
|
"python.analysis.typeCheckingMode": "off",
|
||||||
"python.linting.mypyEnabled": true,
|
"python.linting.mypyEnabled": true,
|
||||||
"python.sortImports.path": "${workspaceFolder}/.venv/bin/isort",
|
"python.sortImports.path": "${workspaceFolder}/.venv/bin/isort",
|
||||||
"search.mode": "reuseEditor",
|
"search.mode": "reuseEditor",
|
||||||
"vetur.validation.template": false,
|
"python.testing.unittestArgs": ["-v", "-s", "./tests", "-p", "test_*.py"]
|
||||||
"coverage-gutters.lcovname": "${workspaceFolder}/.coverage",
|
|
||||||
"python.testing.unittestArgs": [
|
|
||||||
"-v",
|
|
||||||
"-s",
|
|
||||||
"./tests",
|
|
||||||
"-p",
|
|
||||||
"test_*.py"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
@ -24,7 +24,7 @@ In your instance of Mealie prior to v1, perform an export of your data in the Ad
|
|||||||
|
|
||||||
## Step 3: Using the Migration Tool
|
## Step 3: Using the Migration Tool
|
||||||
|
|
||||||
In your new v1 instance, navigate to `/group/data/migrations` and select "Mealie" from the dropdown selector. Upload the exported data from your pre-v1 instance. Optionally, you can tag all the recipes you've imported with the `mealie_alpha` tag to help you identify them. Once the upload has finished, submit the form and your migration will begin. This may take some time, but when it's complete you'll be provided a new entry in hte "Previous Migrations" table below. Be sure to review the migration report to make sure everything was successful. There may be instances where some of the recipes were not imported, but the migration will still import all the successful recipes.
|
In your new v1 instance, navigate to `/group/migrations` and select "Mealie" from the dropdown selector. Upload the exported data from your pre-v1 instance. Optionally, you can tag all the recipes you've imported with the `mealie_alpha` tag to help you identify them. Once the upload has finished, submit the form and your migration will begin. This may take some time, but when it's complete you'll be provided a new entry in hte "Previous Migrations" table below. Be sure to review the migration report to make sure everything was successful. There may be instances where some of the recipes were not imported, but the migration will still import all the successful recipes.
|
||||||
|
|
||||||
In most cases, it's faster to manually migrate the recipes that didn't take instead of trying to identify why the recipes failed to import. If you're experiencing issues with the migration tool, please open an issue on GitHub.
|
In most cases, it's faster to manually migrate the recipes that didn't take instead of trying to identify why the recipes failed to import. If you're experiencing issues with the migration tool, please open an issue on GitHub.
|
||||||
|
|
||||||
|
@ -6,9 +6,15 @@ const prefix = "/api";
|
|||||||
const routes = {
|
const routes = {
|
||||||
food: `${prefix}/foods`,
|
food: `${prefix}/foods`,
|
||||||
foodsFood: (tag: string) => `${prefix}/foods/${tag}`,
|
foodsFood: (tag: string) => `${prefix}/foods/${tag}`,
|
||||||
|
merge: `${prefix}/foods/merge`,
|
||||||
};
|
};
|
||||||
|
|
||||||
export class FoodAPI extends BaseCRUDAPI<IngredientFood, CreateIngredientFood> {
|
export class FoodAPI extends BaseCRUDAPI<IngredientFood, CreateIngredientFood> {
|
||||||
baseRoute: string = routes.food;
|
baseRoute: string = routes.food;
|
||||||
itemRoute = routes.foodsFood;
|
itemRoute = routes.foodsFood;
|
||||||
|
|
||||||
|
merge(fromId: string, toId: string) {
|
||||||
|
// @ts-ignore TODO: fix this
|
||||||
|
return this.requests.put<IngredientFood>(routes.merge, { fromFood: fromId, toFood: toId });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -48,7 +48,7 @@ export default defineComponent({
|
|||||||
];
|
];
|
||||||
|
|
||||||
function handleRowClick(item: ReportSummary) {
|
function handleRowClick(item: ReportSummary) {
|
||||||
router.push("/group/data/reports/" + item.id);
|
router.push("/group/reports/" + item.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
function capitalize(str: string) {
|
function capitalize(str: string) {
|
||||||
@ -69,5 +69,4 @@ export default defineComponent({
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped></style>
|
||||||
</style>
|
|
||||||
|
@ -6,7 +6,8 @@
|
|||||||
:top-link="topLinks"
|
:top-link="topLinks"
|
||||||
:bottom-links="bottomLinks"
|
:bottom-links="bottomLinks"
|
||||||
:user="{ data: true }"
|
:user="{ data: true }"
|
||||||
:secondary-header="$t('user.admin')"
|
secondary-header="Developer"
|
||||||
|
:secondary-links="developerLinks"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TheSnackbar />
|
<TheSnackbar />
|
||||||
@ -49,11 +50,7 @@ export default defineComponent({
|
|||||||
to: "/admin/site-settings",
|
to: "/admin/site-settings",
|
||||||
title: i18n.t("sidebar.site-settings"),
|
title: i18n.t("sidebar.site-settings"),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
icon: $globals.icons.wrench,
|
|
||||||
to: "/admin/maintenance",
|
|
||||||
title: "Maintenance",
|
|
||||||
},
|
|
||||||
// {
|
// {
|
||||||
// icon: $globals.icons.chart,
|
// icon: $globals.icons.chart,
|
||||||
// to: "/admin/analytics",
|
// to: "/admin/analytics",
|
||||||
@ -74,6 +71,14 @@ export default defineComponent({
|
|||||||
to: "/admin/backups",
|
to: "/admin/backups",
|
||||||
title: i18n.t("sidebar.backups"),
|
title: i18n.t("sidebar.backups"),
|
||||||
},
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const developerLinks: SidebarLinks = [
|
||||||
|
{
|
||||||
|
icon: $globals.icons.wrench,
|
||||||
|
to: "/admin/maintenance",
|
||||||
|
title: "Maintenance",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
icon: $globals.icons.check,
|
icon: $globals.icons.check,
|
||||||
to: "/admin/background-tasks",
|
to: "/admin/background-tasks",
|
||||||
@ -98,6 +103,7 @@ export default defineComponent({
|
|||||||
sidebar,
|
sidebar,
|
||||||
topLinks,
|
topLinks,
|
||||||
bottomLinks,
|
bottomLinks,
|
||||||
|
developerLinks,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -97,7 +97,7 @@
|
|||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
<v-container class="mt-4 d-flex justify-end">
|
<v-container class="mt-4 d-flex justify-end">
|
||||||
<v-btn outlined rounded to="/group/data/migrations"> Looking For Migrations? </v-btn>
|
<v-btn outlined rounded to="/group/migrations"> Looking For Migrations? </v-btn>
|
||||||
</v-container>
|
</v-container>
|
||||||
</v-container>
|
</v-container>
|
||||||
</template>
|
</template>
|
||||||
|
@ -1,5 +1,20 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
|
<!-- Merge Dialog -->
|
||||||
|
<BaseDialog v-model="mergeDialog" :icon="$globals.icons.foods" title="Combine Food" @confirm="mergeFoods">
|
||||||
|
<v-card-text>
|
||||||
|
Combining the selected foods will merge the Source Food and Target Food into a single food. The
|
||||||
|
<strong> Source Food will be deleted </strong> and all of the references to the Source Food will be updated to
|
||||||
|
point to the Target Food.
|
||||||
|
<v-autocomplete v-model="fromFood" return-object :items="foods" item-text="name" label="Source Food" />
|
||||||
|
<v-autocomplete v-model="toFood" return-object :items="foods" item-text="name" label="Target Food" />
|
||||||
|
|
||||||
|
<template v-if="canMerge && fromFood && toFood">
|
||||||
|
<div class="text-center">Merging {{ fromFood.name }} into {{ toFood.name }}</div>
|
||||||
|
</template>
|
||||||
|
</v-card-text>
|
||||||
|
</BaseDialog>
|
||||||
|
|
||||||
<!-- Edit Dialog -->
|
<!-- Edit Dialog -->
|
||||||
<BaseDialog
|
<BaseDialog
|
||||||
v-model="editDialog"
|
v-model="editDialog"
|
||||||
@ -48,7 +63,7 @@
|
|||||||
@edit-one="editEventHandler"
|
@edit-one="editEventHandler"
|
||||||
>
|
>
|
||||||
<template #button-row>
|
<template #button-row>
|
||||||
<BaseButton :disabled="true">
|
<BaseButton @click="mergeDialog = true">
|
||||||
<template #icon> {{ $globals.icons.foods }} </template>
|
<template #icon> {{ $globals.icons.foods }} </template>
|
||||||
Combine
|
Combine
|
||||||
</BaseButton>
|
</BaseButton>
|
||||||
@ -64,6 +79,7 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, onMounted, ref } from "@nuxtjs/composition-api";
|
import { defineComponent, onMounted, ref } from "@nuxtjs/composition-api";
|
||||||
|
import { computed } from "vue-demi";
|
||||||
import { validators } from "~/composables/use-validators";
|
import { validators } from "~/composables/use-validators";
|
||||||
import { useUserApi } from "~/composables/api";
|
import { useUserApi } from "~/composables/api";
|
||||||
import { IngredientFood } from "~/types/api-types/recipe";
|
import { IngredientFood } from "~/types/api-types/recipe";
|
||||||
@ -144,6 +160,29 @@ export default defineComponent({
|
|||||||
deleteDialog.value = false;
|
deleteDialog.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Merge Foods
|
||||||
|
|
||||||
|
const mergeDialog = ref(false);
|
||||||
|
const fromFood = ref<IngredientFood | null>(null);
|
||||||
|
const toFood = ref<IngredientFood | null>(null);
|
||||||
|
|
||||||
|
const canMerge = computed(() => {
|
||||||
|
return fromFood.value && toFood.value && fromFood.value.id !== toFood.value.id;
|
||||||
|
});
|
||||||
|
|
||||||
|
async function mergeFoods() {
|
||||||
|
if (!canMerge.value || !fromFood.value || !toFood.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data } = await userApi.foods.merge(fromFood.value.id, toFood.value.id);
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
refreshFoods();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Labels
|
// Labels
|
||||||
|
|
||||||
@ -170,6 +209,12 @@ export default defineComponent({
|
|||||||
deleteEventHandler,
|
deleteEventHandler,
|
||||||
deleteDialog,
|
deleteDialog,
|
||||||
deleteFood,
|
deleteFood,
|
||||||
|
// Merge
|
||||||
|
canMerge,
|
||||||
|
mergeFoods,
|
||||||
|
mergeDialog,
|
||||||
|
fromFood,
|
||||||
|
toFood,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -312,7 +312,7 @@
|
|||||||
|
|
||||||
<AdvancedOnly>
|
<AdvancedOnly>
|
||||||
<v-container class="narrow-container d-flex justify-end">
|
<v-container class="narrow-container d-flex justify-end">
|
||||||
<v-btn outlined rounded to="/group/data/migrations"> Looking For Migrations? </v-btn>
|
<v-btn outlined rounded to="/group/migrations"> Looking For Migrations? </v-btn>
|
||||||
</v-container>
|
</v-container>
|
||||||
</AdvancedOnly>
|
</AdvancedOnly>
|
||||||
</div>
|
</div>
|
||||||
|
@ -98,15 +98,17 @@
|
|||||||
Manage your preferences, change your password, and update your email
|
Manage your preferences, change your password, and update your email
|
||||||
</UserProfileLinkCard>
|
</UserProfileLinkCard>
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col v-if="user.advanced" cols="12" sm="12" md="6">
|
<AdvancedOnly>
|
||||||
<UserProfileLinkCard
|
<v-col cols="12" sm="12" md="6">
|
||||||
:link="{ text: 'Manage Your API Tokens', to: '/user/profile/api-tokens' }"
|
<UserProfileLinkCard
|
||||||
:image="require('~/static/svgs/manage-api-tokens.svg')"
|
:link="{ text: 'Manage Your API Tokens', to: '/user/profile/api-tokens' }"
|
||||||
>
|
:image="require('~/static/svgs/manage-api-tokens.svg')"
|
||||||
<template #title> API Tokens </template>
|
>
|
||||||
Manage your API Tokens for access from external applications
|
<template #title> API Tokens </template>
|
||||||
</UserProfileLinkCard>
|
Manage your API Tokens for access from external applications
|
||||||
</v-col>
|
</UserProfileLinkCard>
|
||||||
|
</v-col>
|
||||||
|
</AdvancedOnly>
|
||||||
</v-row>
|
</v-row>
|
||||||
</section>
|
</section>
|
||||||
<v-divider class="my-7"></v-divider>
|
<v-divider class="my-7"></v-divider>
|
||||||
@ -134,24 +136,6 @@
|
|||||||
Manage a collection of recipe categories and generate pages for them.
|
Manage a collection of recipe categories and generate pages for them.
|
||||||
</UserProfileLinkCard>
|
</UserProfileLinkCard>
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col v-if="user.advanced" cols="12" sm="12" md="6">
|
|
||||||
<UserProfileLinkCard
|
|
||||||
:link="{ text: 'Manage Webhooks', to: '/group/webhooks' }"
|
|
||||||
:image="require('~/static/svgs/manage-webhooks.svg')"
|
|
||||||
>
|
|
||||||
<template #title> Webhooks </template>
|
|
||||||
Setup webhooks that trigger on days that you have have mealplan scheduled.
|
|
||||||
</UserProfileLinkCard>
|
|
||||||
</v-col>
|
|
||||||
<v-col v-if="user.advanced" cols="12" sm="12" md="6">
|
|
||||||
<UserProfileLinkCard
|
|
||||||
:link="{ text: 'Manage Notifiers', to: '/group/notifiers' }"
|
|
||||||
:image="require('~/static/svgs/manage-notifiers.svg')"
|
|
||||||
>
|
|
||||||
<template #title> Notifiers </template>
|
|
||||||
Setup email and push notifications that trigger on specific events.
|
|
||||||
</UserProfileLinkCard>
|
|
||||||
</v-col>
|
|
||||||
<v-col v-if="user.canManage" cols="12" sm="12" md="6">
|
<v-col v-if="user.canManage" cols="12" sm="12" md="6">
|
||||||
<UserProfileLinkCard
|
<UserProfileLinkCard
|
||||||
:link="{ text: 'Manage Members', to: '/group/members' }"
|
:link="{ text: 'Manage Members', to: '/group/members' }"
|
||||||
@ -161,33 +145,50 @@
|
|||||||
See who's in your group and manage their permissions.
|
See who's in your group and manage their permissions.
|
||||||
</UserProfileLinkCard>
|
</UserProfileLinkCard>
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col v-if="user.advanced" cols="12" sm="12" md="6">
|
<AdvancedOnly>
|
||||||
<UserProfileLinkCard
|
<v-col v-if="user.advanced" cols="12" sm="12" md="6">
|
||||||
:link="{ text: 'Manage Recipe Data', to: '/group/data/recipes' }"
|
<UserProfileLinkCard
|
||||||
:image="require('~/static/svgs/manage-recipes.svg')"
|
:link="{ text: 'Manage Webhooks', to: '/group/webhooks' }"
|
||||||
>
|
:image="require('~/static/svgs/manage-webhooks.svg')"
|
||||||
<template #title> Recipe Data </template>
|
>
|
||||||
Manage your recipe data and make bulk changes
|
<template #title> Webhooks </template>
|
||||||
</UserProfileLinkCard>
|
Setup webhooks that trigger on days that you have have mealplan scheduled.
|
||||||
</v-col>
|
</UserProfileLinkCard>
|
||||||
<v-col v-if="user.advanced" cols="12" sm="12" md="6">
|
</v-col>
|
||||||
<UserProfileLinkCard
|
</AdvancedOnly>
|
||||||
:link="{ text: 'Manage Data', to: '/group/data/foods' }"
|
<AdvancedOnly>
|
||||||
:image="require('~/static/svgs/manage-recipes.svg')"
|
<v-col cols="12" sm="12" md="6">
|
||||||
>
|
<UserProfileLinkCard
|
||||||
<template #title> Manage Data </template>
|
:link="{ text: 'Manage Notifiers', to: '/group/notifiers' }"
|
||||||
Manage your Food and Units (more options coming soon)
|
:image="require('~/static/svgs/manage-notifiers.svg')"
|
||||||
</UserProfileLinkCard>
|
>
|
||||||
</v-col>
|
<template #title> Notifiers </template>
|
||||||
<v-col v-if="user.advanced" cols="12" sm="12" md="6">
|
Setup email and push notifications that trigger on specific events.
|
||||||
<UserProfileLinkCard
|
</UserProfileLinkCard>
|
||||||
:link="{ text: 'Manage Data Migrations', to: '/group/data/migrations' }"
|
</v-col>
|
||||||
:image="require('~/static/svgs/manage-data-migrations.svg')"
|
</AdvancedOnly>
|
||||||
>
|
<AdvancedOnly>
|
||||||
<template #title> Data Migrations </template>
|
<v-col cols="12" sm="12" md="6">
|
||||||
Migrate your existing data from other applications like Nextcloud Recipes and Chowdown
|
<UserProfileLinkCard
|
||||||
</UserProfileLinkCard>
|
:link="{ text: 'Manage Data', to: '/group/data/foods' }"
|
||||||
</v-col>
|
:image="require('~/static/svgs/manage-recipes.svg')"
|
||||||
|
>
|
||||||
|
<template #title> Manage Data </template>
|
||||||
|
Manage your Food and Units (more options coming soon)
|
||||||
|
</UserProfileLinkCard>
|
||||||
|
</v-col>
|
||||||
|
</AdvancedOnly>
|
||||||
|
<AdvancedOnly>
|
||||||
|
<v-col cols="12" sm="12" md="6">
|
||||||
|
<UserProfileLinkCard
|
||||||
|
:link="{ text: 'Manage Data Migrations', to: '/group/migrations' }"
|
||||||
|
:image="require('~/static/svgs/manage-data-migrations.svg')"
|
||||||
|
>
|
||||||
|
<template #title> Data Migrations </template>
|
||||||
|
Migrate your existing data from other applications like Nextcloud Recipes and Chowdown
|
||||||
|
</UserProfileLinkCard>
|
||||||
|
</v-col>
|
||||||
|
</AdvancedOnly>
|
||||||
</v-row>
|
</v-row>
|
||||||
</section>
|
</section>
|
||||||
</v-container>
|
</v-container>
|
||||||
|
@ -113,6 +113,10 @@ export interface MultiPurposeLabelSummary {
|
|||||||
groupId: string;
|
groupId: string;
|
||||||
id: string;
|
id: string;
|
||||||
}
|
}
|
||||||
|
export interface IngredientMerge {
|
||||||
|
fromFood: string;
|
||||||
|
toFood: string;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* A list of ingredient references.
|
* A list of ingredient references.
|
||||||
*/
|
*/
|
||||||
|
132
frontend/types/components.d.ts
vendored
132
frontend/types/components.d.ts
vendored
@ -1,74 +1,76 @@
|
|||||||
// This Code is auto generated by gen_global_components.py
|
// This Code is auto generated by gen_global_components.py
|
||||||
import BaseCardSectionTitle from "@/components/global/BaseCardSectionTitle.vue";
|
import BaseCardSectionTitle from "@/components/global/BaseCardSectionTitle.vue";
|
||||||
import MarkdownEditor from "@/components/global/MarkdownEditor.vue";
|
import MarkdownEditor from "@/components/global/MarkdownEditor.vue";
|
||||||
import AppLoader from "@/components/global/AppLoader.vue";
|
import AppLoader from "@/components/global/AppLoader.vue";
|
||||||
import BaseOverflowButton from "@/components/global/BaseOverflowButton.vue";
|
import BaseOverflowButton from "@/components/global/BaseOverflowButton.vue";
|
||||||
import ReportTable from "@/components/global/ReportTable.vue";
|
import ReportTable from "@/components/global/ReportTable.vue";
|
||||||
import AppToolbar from "@/components/global/AppToolbar.vue";
|
import AppToolbar from "@/components/global/AppToolbar.vue";
|
||||||
import BaseButtonGroup from "@/components/global/BaseButtonGroup.vue";
|
import BaseButtonGroup from "@/components/global/BaseButtonGroup.vue";
|
||||||
import BaseButton from "@/components/global/BaseButton.vue";
|
import BaseButton from "@/components/global/BaseButton.vue";
|
||||||
import BannerExperimental from "@/components/global/BannerExperimental.vue";
|
import BannerExperimental from "@/components/global/BannerExperimental.vue";
|
||||||
import BaseDialog from "@/components/global/BaseDialog.vue";
|
import BaseDialog from "@/components/global/BaseDialog.vue";
|
||||||
import RecipeJsonEditor from "@/components/global/RecipeJsonEditor.vue";
|
import RecipeJsonEditor from "@/components/global/RecipeJsonEditor.vue";
|
||||||
import StatsCards from "@/components/global/StatsCards.vue";
|
import StatsCards from "@/components/global/StatsCards.vue";
|
||||||
import HelpIcon from "@/components/global/HelpIcon.vue";
|
import HelpIcon from "@/components/global/HelpIcon.vue";
|
||||||
import InputLabelType from "@/components/global/InputLabelType.vue";
|
import InputLabelType from "@/components/global/InputLabelType.vue";
|
||||||
import BaseStatCard from "@/components/global/BaseStatCard.vue";
|
import BaseStatCard from "@/components/global/BaseStatCard.vue";
|
||||||
import DevDumpJson from "@/components/global/DevDumpJson.vue";
|
import DevDumpJson from "@/components/global/DevDumpJson.vue";
|
||||||
import LanguageDialog from "@/components/global/LanguageDialog.vue";
|
import LanguageDialog from "@/components/global/LanguageDialog.vue";
|
||||||
import InputQuantity from "@/components/global/InputQuantity.vue";
|
import InputQuantity from "@/components/global/InputQuantity.vue";
|
||||||
import ToggleState from "@/components/global/ToggleState.vue";
|
import ToggleState from "@/components/global/ToggleState.vue";
|
||||||
import AppButtonCopy from "@/components/global/AppButtonCopy.vue";
|
import AppButtonCopy from "@/components/global/AppButtonCopy.vue";
|
||||||
import CrudTable from "@/components/global/CrudTable.vue";
|
import CrudTable from "@/components/global/CrudTable.vue";
|
||||||
import InputColor from "@/components/global/InputColor.vue";
|
import InputColor from "@/components/global/InputColor.vue";
|
||||||
import BaseDivider from "@/components/global/BaseDivider.vue";
|
import BaseDivider from "@/components/global/BaseDivider.vue";
|
||||||
import AutoForm from "@/components/global/AutoForm.vue";
|
import AutoForm from "@/components/global/AutoForm.vue";
|
||||||
import AppButtonUpload from "@/components/global/AppButtonUpload.vue";
|
import AppButtonUpload from "@/components/global/AppButtonUpload.vue";
|
||||||
import AdvancedOnly from "@/components/global/AdvancedOnly.vue";
|
import AdvancedOnly from "@/components/global/AdvancedOnly.vue";
|
||||||
import BasePageTitle from "@/components/global/BasePageTitle.vue";
|
import BasePageTitle from "@/components/global/BasePageTitle.vue";
|
||||||
import ButtonLink from "@/components/global/ButtonLink.vue";
|
import ButtonLink from "@/components/global/ButtonLink.vue";
|
||||||
|
|
||||||
|
import TheSnackbar from "@/components/layout/TheSnackbar.vue";
|
||||||
|
import AppHeader from "@/components/layout/AppHeader.vue";
|
||||||
|
import AppSidebar from "@/components/layout/AppSidebar.vue";
|
||||||
|
import AppFooter from "@/components/layout/AppFooter.vue";
|
||||||
|
|
||||||
import TheSnackbar from "@/components/layout/TheSnackbar.vue";
|
|
||||||
import AppHeader from "@/components/layout/AppHeader.vue";
|
|
||||||
import AppSidebar from "@/components/layout/AppSidebar.vue";
|
|
||||||
import AppFooter from "@/components/layout/AppFooter.vue";
|
|
||||||
|
|
||||||
declare module "vue" {
|
declare module "vue" {
|
||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
// Global Components
|
// Global Components
|
||||||
BaseCardSectionTitle: typeof BaseCardSectionTitle;
|
BaseCardSectionTitle: typeof BaseCardSectionTitle;
|
||||||
MarkdownEditor: typeof MarkdownEditor;
|
MarkdownEditor: typeof MarkdownEditor;
|
||||||
AppLoader: typeof AppLoader;
|
AppLoader: typeof AppLoader;
|
||||||
BaseOverflowButton: typeof BaseOverflowButton;
|
BaseOverflowButton: typeof BaseOverflowButton;
|
||||||
ReportTable: typeof ReportTable;
|
ReportTable: typeof ReportTable;
|
||||||
AppToolbar: typeof AppToolbar;
|
AppToolbar: typeof AppToolbar;
|
||||||
BaseButtonGroup: typeof BaseButtonGroup;
|
BaseButtonGroup: typeof BaseButtonGroup;
|
||||||
BaseButton: typeof BaseButton;
|
BaseButton: typeof BaseButton;
|
||||||
BannerExperimental: typeof BannerExperimental;
|
BannerExperimental: typeof BannerExperimental;
|
||||||
BaseDialog: typeof BaseDialog;
|
BaseDialog: typeof BaseDialog;
|
||||||
RecipeJsonEditor: typeof RecipeJsonEditor;
|
RecipeJsonEditor: typeof RecipeJsonEditor;
|
||||||
StatsCards: typeof StatsCards;
|
StatsCards: typeof StatsCards;
|
||||||
HelpIcon: typeof HelpIcon;
|
HelpIcon: typeof HelpIcon;
|
||||||
InputLabelType: typeof InputLabelType;
|
InputLabelType: typeof InputLabelType;
|
||||||
BaseStatCard: typeof BaseStatCard;
|
BaseStatCard: typeof BaseStatCard;
|
||||||
DevDumpJson: typeof DevDumpJson;
|
DevDumpJson: typeof DevDumpJson;
|
||||||
LanguageDialog: typeof LanguageDialog;
|
LanguageDialog: typeof LanguageDialog;
|
||||||
InputQuantity: typeof InputQuantity;
|
InputQuantity: typeof InputQuantity;
|
||||||
ToggleState: typeof ToggleState;
|
ToggleState: typeof ToggleState;
|
||||||
AppButtonCopy: typeof AppButtonCopy;
|
AppButtonCopy: typeof AppButtonCopy;
|
||||||
CrudTable: typeof CrudTable;
|
CrudTable: typeof CrudTable;
|
||||||
InputColor: typeof InputColor;
|
InputColor: typeof InputColor;
|
||||||
BaseDivider: typeof BaseDivider;
|
BaseDivider: typeof BaseDivider;
|
||||||
AutoForm: typeof AutoForm;
|
AutoForm: typeof AutoForm;
|
||||||
AppButtonUpload: typeof AppButtonUpload;
|
AppButtonUpload: typeof AppButtonUpload;
|
||||||
AdvancedOnly: typeof AdvancedOnly;
|
AdvancedOnly: typeof AdvancedOnly;
|
||||||
BasePageTitle: typeof BasePageTitle;
|
BasePageTitle: typeof BasePageTitle;
|
||||||
ButtonLink: typeof ButtonLink;
|
ButtonLink: typeof ButtonLink;
|
||||||
// Layout Components
|
// Layout Components
|
||||||
TheSnackbar: typeof TheSnackbar;
|
TheSnackbar: typeof TheSnackbar;
|
||||||
AppHeader: typeof AppHeader;
|
AppHeader: typeof AppHeader;
|
||||||
AppSidebar: typeof AppSidebar;
|
AppSidebar: typeof AppSidebar;
|
||||||
AppFooter: typeof AppFooter;
|
AppFooter: typeof AppFooter;
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,6 +27,7 @@ from mealie.db.models.recipe.tool import Tool
|
|||||||
from mealie.db.models.server.task import ServerTaskModel
|
from mealie.db.models.server.task import ServerTaskModel
|
||||||
from mealie.db.models.users import LongLiveToken, User
|
from mealie.db.models.users import LongLiveToken, User
|
||||||
from mealie.db.models.users.password_reset import PasswordResetModel
|
from mealie.db.models.users.password_reset import PasswordResetModel
|
||||||
|
from mealie.repos.repository_foods import RepositoryFood
|
||||||
from mealie.repos.repository_meal_plan_rules import RepositoryMealPlanRules
|
from mealie.repos.repository_meal_plan_rules import RepositoryMealPlanRules
|
||||||
from mealie.schema.cookbook.cookbook import ReadCookBook
|
from mealie.schema.cookbook.cookbook import ReadCookBook
|
||||||
from mealie.schema.group.group_events import GroupEventNotifierOut
|
from mealie.schema.group.group_events import GroupEventNotifierOut
|
||||||
@ -94,8 +95,8 @@ class AllRepositories:
|
|||||||
return RepositoryRecipes(self.session, PK_SLUG, RecipeModel, Recipe)
|
return RepositoryRecipes(self.session, PK_SLUG, RecipeModel, Recipe)
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def ingredient_foods(self) -> RepositoryGeneric[IngredientFood, IngredientFoodModel]:
|
def ingredient_foods(self) -> RepositoryFood:
|
||||||
return RepositoryGeneric(self.session, PK_ID, IngredientFoodModel, IngredientFood)
|
return RepositoryFood(self.session, PK_ID, IngredientFoodModel, IngredientFood)
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def ingredient_units(self) -> RepositoryGeneric[IngredientUnit, IngredientUnitModel]:
|
def ingredient_units(self) -> RepositoryGeneric[IngredientUnit, IngredientUnitModel]:
|
||||||
|
32
mealie/repos/repository_foods.py
Normal file
32
mealie/repos/repository_foods.py
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
from pydantic import UUID4
|
||||||
|
|
||||||
|
from mealie.db.models.recipe.ingredient import IngredientFoodModel
|
||||||
|
from mealie.schema.recipe.recipe_ingredient import IngredientFood
|
||||||
|
|
||||||
|
from .repository_generic import RepositoryGeneric
|
||||||
|
|
||||||
|
|
||||||
|
class RepositoryFood(RepositoryGeneric[IngredientFood, IngredientFoodModel]):
|
||||||
|
def merge(self, from_food: UUID4, to_food: UUID4) -> IngredientFood | None:
|
||||||
|
|
||||||
|
from_model: IngredientFoodModel = (
|
||||||
|
self.session.query(self.sql_model).filter_by(**self._filter_builder(**{"id": from_food})).one()
|
||||||
|
)
|
||||||
|
|
||||||
|
to_model: IngredientFoodModel = (
|
||||||
|
self.session.query(self.sql_model).filter_by(**self._filter_builder(**{"id": to_food})).one()
|
||||||
|
)
|
||||||
|
|
||||||
|
to_model.ingredients += from_model.ingredients
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.session.delete(from_model)
|
||||||
|
self.session.commit()
|
||||||
|
except Exception as e:
|
||||||
|
self.session.rollback()
|
||||||
|
raise e
|
||||||
|
|
||||||
|
return self.get_one(to_food)
|
||||||
|
|
||||||
|
def by_group(self, group_id: UUID4) -> "RepositoryFood":
|
||||||
|
return super().by_group(group_id) # type: ignore
|
@ -1,6 +1,6 @@
|
|||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
from pydantic import UUID4
|
from pydantic import UUID4
|
||||||
|
|
||||||
from mealie.routes._base.abc_controller import BaseUserController
|
from mealie.routes._base.abc_controller import BaseUserController
|
||||||
@ -8,7 +8,13 @@ from mealie.routes._base.controller import controller
|
|||||||
from mealie.routes._base.mixins import CrudMixins
|
from mealie.routes._base.mixins import CrudMixins
|
||||||
from mealie.schema import mapper
|
from mealie.schema import mapper
|
||||||
from mealie.schema.query import GetAll
|
from mealie.schema.query import GetAll
|
||||||
from mealie.schema.recipe.recipe_ingredient import CreateIngredientFood, IngredientFood, SaveIngredientFood
|
from mealie.schema.recipe.recipe_ingredient import (
|
||||||
|
CreateIngredientFood,
|
||||||
|
IngredientFood,
|
||||||
|
IngredientMerge,
|
||||||
|
SaveIngredientFood,
|
||||||
|
)
|
||||||
|
from mealie.schema.response.responses import SuccessResponse
|
||||||
|
|
||||||
router = APIRouter(prefix="/foods", tags=["Recipes: Foods"])
|
router = APIRouter(prefix="/foods", tags=["Recipes: Foods"])
|
||||||
|
|
||||||
@ -27,6 +33,15 @@ class IngredientFoodsController(BaseUserController):
|
|||||||
self.registered_exceptions,
|
self.registered_exceptions,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@router.put("/merge", response_model=SuccessResponse)
|
||||||
|
def merge_one(self, data: IngredientMerge):
|
||||||
|
try:
|
||||||
|
self.repo.merge(data.from_food, data.to_food)
|
||||||
|
return SuccessResponse.respond("Successfully merged foods")
|
||||||
|
except Exception as e:
|
||||||
|
self.deps.logger.error(e)
|
||||||
|
raise HTTPException(500, "Failed to merge foods") from e
|
||||||
|
|
||||||
@router.get("", response_model=list[IngredientFood])
|
@router.get("", response_model=list[IngredientFood])
|
||||||
def get_all(self, q: GetAll = Depends(GetAll)):
|
def get_all(self, q: GetAll = Depends(GetAll)):
|
||||||
return self.repo.get_all(start=q.start, limit=q.limit)
|
return self.repo.get_all(start=q.start, limit=q.limit)
|
||||||
|
@ -95,6 +95,11 @@ class IngredientRequest(MealieModel):
|
|||||||
ingredient: str
|
ingredient: str
|
||||||
|
|
||||||
|
|
||||||
|
class IngredientMerge(MealieModel):
|
||||||
|
from_food: UUID4
|
||||||
|
to_food: UUID4
|
||||||
|
|
||||||
|
|
||||||
from mealie.schema.labels.multi_purpose_label import MultiPurposeLabelSummary
|
from mealie.schema.labels.multi_purpose_label import MultiPurposeLabelSummary
|
||||||
|
|
||||||
IngredientFood.update_forward_refs()
|
IngredientFood.update_forward_refs()
|
||||||
|
18
poetry.lock
generated
18
poetry.lock
generated
@ -374,7 +374,7 @@ cli = ["requests"]
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fastapi"
|
name = "fastapi"
|
||||||
version = "0.74.1"
|
version = "0.75.1"
|
||||||
description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
|
description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
|
||||||
category = "main"
|
category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
@ -387,8 +387,8 @@ starlette = "0.17.1"
|
|||||||
[package.extras]
|
[package.extras]
|
||||||
all = ["requests (>=2.24.0,<3.0.0)", "jinja2 (>=2.11.2,<4.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "itsdangerous (>=1.1.0,<3.0.0)", "pyyaml (>=5.3.1,<6.0.0)", "ujson (>=4.0.1,<5.0.0)", "orjson (>=3.2.1,<4.0.0)", "email_validator (>=1.1.1,<2.0.0)", "uvicorn[standard] (>=0.12.0,<0.16.0)"]
|
all = ["requests (>=2.24.0,<3.0.0)", "jinja2 (>=2.11.2,<4.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "itsdangerous (>=1.1.0,<3.0.0)", "pyyaml (>=5.3.1,<6.0.0)", "ujson (>=4.0.1,<5.0.0)", "orjson (>=3.2.1,<4.0.0)", "email_validator (>=1.1.1,<2.0.0)", "uvicorn[standard] (>=0.12.0,<0.16.0)"]
|
||||||
dev = ["python-jose[cryptography] (>=3.3.0,<4.0.0)", "passlib[bcrypt] (>=1.7.2,<2.0.0)", "autoflake (>=1.4.0,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "uvicorn[standard] (>=0.12.0,<0.16.0)"]
|
dev = ["python-jose[cryptography] (>=3.3.0,<4.0.0)", "passlib[bcrypt] (>=1.7.2,<2.0.0)", "autoflake (>=1.4.0,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "uvicorn[standard] (>=0.12.0,<0.16.0)"]
|
||||||
doc = ["mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "mdx-include (>=1.4.1,<2.0.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.3.0)", "typer-cli (>=0.0.12,<0.0.13)", "pyyaml (>=5.3.1,<6.0.0)"]
|
doc = ["mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "mdx-include (>=1.4.1,<2.0.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.3.0)", "typer (>=0.4.1,<0.5.0)", "pyyaml (>=5.3.1,<6.0.0)"]
|
||||||
test = ["pytest (>=6.2.4,<7.0.0)", "pytest-cov (>=2.12.0,<4.0.0)", "mypy (==0.910)", "flake8 (>=3.8.3,<4.0.0)", "black (==21.9b0)", "isort (>=5.0.6,<6.0.0)", "requests (>=2.24.0,<3.0.0)", "httpx (>=0.14.0,<0.19.0)", "email_validator (>=1.1.1,<2.0.0)", "sqlalchemy (>=1.3.18,<1.5.0)", "peewee (>=3.13.3,<4.0.0)", "databases[sqlite] (>=0.3.2,<0.6.0)", "orjson (>=3.2.1,<4.0.0)", "ujson (>=4.0.1,<5.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "flask (>=1.1.2,<3.0.0)", "anyio[trio] (>=3.2.1,<4.0.0)", "types-ujson (==0.1.1)", "types-orjson (==3.6.0)", "types-dataclasses (==0.1.7)"]
|
test = ["pytest (>=6.2.4,<7.0.0)", "pytest-cov (>=2.12.0,<4.0.0)", "mypy (==0.910)", "flake8 (>=3.8.3,<4.0.0)", "black (==22.3.0)", "isort (>=5.0.6,<6.0.0)", "requests (>=2.24.0,<3.0.0)", "httpx (>=0.14.0,<0.19.0)", "email_validator (>=1.1.1,<2.0.0)", "sqlalchemy (>=1.3.18,<1.5.0)", "peewee (>=3.13.3,<4.0.0)", "databases[sqlite] (>=0.3.2,<0.6.0)", "orjson (>=3.2.1,<4.0.0)", "ujson (>=4.0.1,<5.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "flask (>=1.1.2,<3.0.0)", "anyio[trio] (>=3.2.1,<4.0.0)", "types-ujson (==0.1.1)", "types-orjson (==3.6.0)", "types-dataclasses (==0.1.7)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "filelock"
|
name = "filelock"
|
||||||
@ -1229,7 +1229,7 @@ rdflib = ">=5.0.0"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "recipe-scrapers"
|
name = "recipe-scrapers"
|
||||||
version = "13.23.0"
|
version = "13.28.0"
|
||||||
description = "Python package, scraping recipes from all over the internet"
|
description = "Python package, scraping recipes from all over the internet"
|
||||||
category = "main"
|
category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
@ -1599,7 +1599,7 @@ pgsql = ["psycopg2-binary"]
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "1.1"
|
lock-version = "1.1"
|
||||||
python-versions = "^3.10"
|
python-versions = "^3.10"
|
||||||
content-hash = "7541b47452a32f483ab233daa846f07707a3d9da6f4e50c1285249639b1c40fd"
|
content-hash = "00c0adae74732437eaa473f24757191d620edfde671dceb5fdae28de9843d0c3"
|
||||||
|
|
||||||
[metadata.files]
|
[metadata.files]
|
||||||
aiofiles = [
|
aiofiles = [
|
||||||
@ -1826,8 +1826,8 @@ extruct = [
|
|||||||
{file = "extruct-0.13.0.tar.gz", hash = "sha256:50a5b5bac4c5e19ecf682bf63a28fde0b1bb57433df7057371f60b58c94a2c64"},
|
{file = "extruct-0.13.0.tar.gz", hash = "sha256:50a5b5bac4c5e19ecf682bf63a28fde0b1bb57433df7057371f60b58c94a2c64"},
|
||||||
]
|
]
|
||||||
fastapi = [
|
fastapi = [
|
||||||
{file = "fastapi-0.74.1-py3-none-any.whl", hash = "sha256:b8ec8400623ef0b2ff558ebe06753b349f8e3a5dd38afea650800f2644ddba34"},
|
{file = "fastapi-0.75.1-py3-none-any.whl", hash = "sha256:f46f8fc81261c2bd956584114da9da98c84e2410c807bc2487532dabf55e7ab8"},
|
||||||
{file = "fastapi-0.74.1.tar.gz", hash = "sha256:b58a2c46df14f62ebe6f24a9439927539ba1959b9be55ba0e2f516a683e5b9d4"},
|
{file = "fastapi-0.75.1.tar.gz", hash = "sha256:8b62bde916d657803fb60fffe88e2b2c9fb854583784607e4347681cae20ad01"},
|
||||||
]
|
]
|
||||||
filelock = [
|
filelock = [
|
||||||
{file = "filelock-3.6.0-py3-none-any.whl", hash = "sha256:f8314284bfffbdcfa0ff3d7992b023d4c628ced6feb957351d4c48d059f56bc0"},
|
{file = "filelock-3.6.0-py3-none-any.whl", hash = "sha256:f8314284bfffbdcfa0ff3d7992b023d4c628ced6feb957351d4c48d059f56bc0"},
|
||||||
@ -2527,8 +2527,8 @@ rdflib-jsonld = [
|
|||||||
{file = "rdflib_jsonld-0.6.2-py2.py3-none-any.whl", hash = "sha256:011afe67672353ca9978ab9a4bee964dff91f14042f2d8a28c22a573779d2f8b"},
|
{file = "rdflib_jsonld-0.6.2-py2.py3-none-any.whl", hash = "sha256:011afe67672353ca9978ab9a4bee964dff91f14042f2d8a28c22a573779d2f8b"},
|
||||||
]
|
]
|
||||||
recipe-scrapers = [
|
recipe-scrapers = [
|
||||||
{file = "recipe_scrapers-13.23.0-py3-none-any.whl", hash = "sha256:120b356ca422e4f2afb8c944ecf2b53d3c9c73ac9f5345cf35bc168147056e17"},
|
{file = "recipe_scrapers-13.28.0-py3-none-any.whl", hash = "sha256:114ab8fb8baa85976f8709955baca4e6df07b565bfd5b60404eff89584d68e3f"},
|
||||||
{file = "recipe_scrapers-13.23.0.tar.gz", hash = "sha256:d99fbdaa1323e6d11e1378bfda0adc5536bd6acf3c71dc57380898300c577f45"},
|
{file = "recipe_scrapers-13.28.0.tar.gz", hash = "sha256:a12258f2218f8b222bdb57cf9d9d6b0288b892c258ccaec8efec02a292a8aded"},
|
||||||
]
|
]
|
||||||
requests = [
|
requests = [
|
||||||
{file = "requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"},
|
{file = "requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"},
|
||||||
|
@ -13,7 +13,7 @@ python = "^3.10"
|
|||||||
aiofiles = "0.5.0"
|
aiofiles = "0.5.0"
|
||||||
aniso8601 = "7.0.0"
|
aniso8601 = "7.0.0"
|
||||||
appdirs = "1.4.4"
|
appdirs = "1.4.4"
|
||||||
fastapi = "^0.74.1"
|
fastapi = "^0.75.1"
|
||||||
uvicorn = {extras = ["standard"], version = "^0.13.0"}
|
uvicorn = {extras = ["standard"], version = "^0.13.0"}
|
||||||
APScheduler = "^3.8.1"
|
APScheduler = "^3.8.1"
|
||||||
SQLAlchemy = "^1.4.29"
|
SQLAlchemy = "^1.4.29"
|
||||||
@ -31,7 +31,7 @@ passlib = "^1.7.4"
|
|||||||
lxml = "^4.7.1"
|
lxml = "^4.7.1"
|
||||||
Pillow = "^8.2.0"
|
Pillow = "^8.2.0"
|
||||||
apprise = "^0.9.6"
|
apprise = "^0.9.6"
|
||||||
recipe-scrapers = "^13.23.0"
|
recipe-scrapers = "^13.28.0"
|
||||||
psycopg2-binary = {version = "^2.9.1", optional = true}
|
psycopg2-binary = {version = "^2.9.1", optional = true}
|
||||||
gunicorn = "^20.1.0"
|
gunicorn = "^20.1.0"
|
||||||
emails = "^0.6"
|
emails = "^0.6"
|
||||||
|
48
tests/unit_tests/repository_tests/test_food_repository.py
Normal file
48
tests/unit_tests/repository_tests/test_food_repository.py
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
from mealie.repos.repository_factory import AllRepositories
|
||||||
|
from mealie.schema.recipe.recipe import Recipe
|
||||||
|
from mealie.schema.recipe.recipe_ingredient import RecipeIngredient, SaveIngredientFood
|
||||||
|
from tests.utils.factories import random_string
|
||||||
|
from tests.utils.fixture_schemas import TestUser
|
||||||
|
|
||||||
|
|
||||||
|
def test_food_merger(database: AllRepositories, unique_user: TestUser):
|
||||||
|
slug1 = random_string(10)
|
||||||
|
|
||||||
|
food_1 = database.ingredient_foods.create(
|
||||||
|
SaveIngredientFood(
|
||||||
|
name=random_string(10),
|
||||||
|
group_id=unique_user.group_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
food_2 = database.ingredient_foods.create(
|
||||||
|
SaveIngredientFood(
|
||||||
|
name=random_string(10),
|
||||||
|
group_id=unique_user.group_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
recipe = database.recipes.create(
|
||||||
|
Recipe(
|
||||||
|
name=slug1,
|
||||||
|
user_id=unique_user.group_id,
|
||||||
|
group_id=unique_user.group_id,
|
||||||
|
recipe_ingredient=[
|
||||||
|
RecipeIngredient(note="", food=food_1), # type: ignore
|
||||||
|
RecipeIngredient(note="", food=food_2), # type: ignore
|
||||||
|
],
|
||||||
|
) # type: ignore
|
||||||
|
)
|
||||||
|
|
||||||
|
# Santiy check make sure recipe got created
|
||||||
|
assert recipe.id is not None
|
||||||
|
|
||||||
|
for ing in recipe.recipe_ingredient:
|
||||||
|
assert ing.food.id in [food_1.id, food_2.id] # type: ignore
|
||||||
|
|
||||||
|
database.ingredient_foods.merge(food_2.id, food_1.id)
|
||||||
|
|
||||||
|
recipe = database.recipes.get_one(recipe.slug)
|
||||||
|
|
||||||
|
for ingredient in recipe.recipe_ingredient:
|
||||||
|
assert ingredient.food.id == food_1.id # type: ignore
|
Loading…
x
Reference in New Issue
Block a user