mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-05-24 01:12:54 -04:00
add merge funcions for units (#1146)
This commit is contained in:
parent
b93dae109e
commit
db095656e1
@ -6,9 +6,15 @@ const prefix = "/api";
|
||||
const routes = {
|
||||
unit: `${prefix}/units`,
|
||||
unitsUnit: (tag: string) => `${prefix}/units/${tag}`,
|
||||
merge: `${prefix}/units/merge`,
|
||||
};
|
||||
|
||||
export class UnitAPI extends BaseCRUDAPI<IngredientUnit, CreateIngredientUnit> {
|
||||
baseRoute: string = routes.unit;
|
||||
itemRoute = routes.unitsUnit;
|
||||
|
||||
merge(fromId: string, toId: string) {
|
||||
// @ts-ignore TODO: fix this
|
||||
return this.requests.put<IngredientUnit>(routes.merge, { fromUnit: fromId, toUnit: toId });
|
||||
}
|
||||
}
|
||||
|
@ -1,15 +1,30 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- Merge Dialog -->
|
||||
<BaseDialog v-model="mergeDialog" :icon="$globals.icons.units" title="Combine Unit" @confirm="mergeUnits">
|
||||
<v-card-text>
|
||||
Combining the selected units will merge the Source Unit and Target Unit into a single unit. The
|
||||
<strong> Source Unit will be deleted </strong> and all of the references to the Source Unit will be updated to
|
||||
point to the Target Unit.
|
||||
<v-autocomplete v-model="fromUnit" return-object :items="units" item-text="name" label="Source Unit" />
|
||||
<v-autocomplete v-model="toUnit" return-object :items="units" item-text="name" label="Target Unit" />
|
||||
|
||||
<template v-if="canMerge && fromUnit && toUnit">
|
||||
<div class="text-center">Merging {{ fromUnit.name }} into {{ toUnit.name }}</div>
|
||||
</template>
|
||||
</v-card-text>
|
||||
</BaseDialog>
|
||||
|
||||
<!-- Edit Dialog -->
|
||||
<BaseDialog
|
||||
v-model="editDialog"
|
||||
:icon="$globals.icons.units"
|
||||
title="Edit Food"
|
||||
title="Edit Unit"
|
||||
:submit-text="$tc('general.save')"
|
||||
@submit="editSaveFood"
|
||||
@submit="editSaveUnit"
|
||||
>
|
||||
<v-card-text v-if="editTarget">
|
||||
<v-form ref="domCreateFoodForm">
|
||||
<v-form ref="domCreateUnitForm">
|
||||
<v-text-field v-model="editTarget.name" label="Name" :rules="[validators.required]"></v-text-field>
|
||||
<v-text-field v-model="editTarget.abbreviation" label="Abbreviation"></v-text-field>
|
||||
<v-text-field v-model="editTarget.description" label="Description"></v-text-field>
|
||||
@ -24,7 +39,7 @@
|
||||
:title="$tc('general.confirm')"
|
||||
:icon="$globals.icons.alertCircle"
|
||||
color="error"
|
||||
@confirm="deleteFood"
|
||||
@confirm="deleteUnit"
|
||||
>
|
||||
<v-card-text>
|
||||
{{ $t("general.confirm-delete-generic") }}
|
||||
@ -41,6 +56,12 @@
|
||||
@delete-one="deleteEventHandler"
|
||||
@edit-one="editEventHandler"
|
||||
>
|
||||
<template #button-row>
|
||||
<BaseButton @click="mergeDialog = true">
|
||||
<template #icon> {{ $globals.icons.units }} </template>
|
||||
Combine
|
||||
</BaseButton>
|
||||
</template>
|
||||
<template #item.fraction="{ item }">
|
||||
<v-icon :color="item.fraction ? 'success' : undefined">
|
||||
{{ item.fraction ? $globals.icons.check : $globals.icons.close }}
|
||||
@ -51,7 +72,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, onMounted, ref } from "@nuxtjs/composition-api";
|
||||
import { computed, defineComponent, onMounted, ref } from "@nuxtjs/composition-api";
|
||||
import { validators } from "~/composables/use-validators";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
import { IngredientUnit } from "~/types/api-types/recipe";
|
||||
@ -92,12 +113,12 @@ export default defineComponent({
|
||||
},
|
||||
];
|
||||
const units = ref<IngredientUnit[]>([]);
|
||||
async function refreshFoods() {
|
||||
async function refreshUnits() {
|
||||
const { data } = await userApi.units.getAll();
|
||||
units.value = data ?? [];
|
||||
}
|
||||
onMounted(() => {
|
||||
refreshFoods();
|
||||
refreshUnits();
|
||||
});
|
||||
const editDialog = ref(false);
|
||||
const editTarget = ref<IngredientUnit | null>(null);
|
||||
@ -105,14 +126,14 @@ export default defineComponent({
|
||||
editTarget.value = item;
|
||||
editDialog.value = true;
|
||||
}
|
||||
async function editSaveFood() {
|
||||
async function editSaveUnit() {
|
||||
if (!editTarget.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { data } = await userApi.units.updateOne(editTarget.value.id, editTarget.value);
|
||||
if (data) {
|
||||
refreshFoods();
|
||||
refreshUnits();
|
||||
}
|
||||
|
||||
editDialog.value = false;
|
||||
@ -123,18 +144,41 @@ export default defineComponent({
|
||||
deleteTarget.value = item;
|
||||
deleteDialog.value = true;
|
||||
}
|
||||
async function deleteFood() {
|
||||
async function deleteUnit() {
|
||||
if (!deleteTarget.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { data } = await userApi.units.deleteOne(deleteTarget.value.id);
|
||||
if (data) {
|
||||
refreshFoods();
|
||||
refreshUnits();
|
||||
}
|
||||
deleteDialog.value = false;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Merge Units
|
||||
|
||||
const mergeDialog = ref(false);
|
||||
const fromUnit = ref<IngredientUnit | null>(null);
|
||||
const toUnit = ref<IngredientUnit | null>(null);
|
||||
|
||||
const canMerge = computed(() => {
|
||||
return fromUnit.value && toUnit.value && fromUnit.value.id !== toUnit.value.id;
|
||||
});
|
||||
|
||||
async function mergeUnits() {
|
||||
if (!canMerge.value || !fromUnit.value || !toUnit.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { data } = await userApi.units.merge(fromUnit.value.id, toUnit.value.id);
|
||||
|
||||
if (data) {
|
||||
refreshUnits();
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Labels
|
||||
|
||||
@ -155,12 +199,18 @@ export default defineComponent({
|
||||
// Edit
|
||||
editDialog,
|
||||
editEventHandler,
|
||||
editSaveFood,
|
||||
editSaveUnit,
|
||||
editTarget,
|
||||
// Delete
|
||||
deleteEventHandler,
|
||||
deleteDialog,
|
||||
deleteFood,
|
||||
deleteUnit,
|
||||
// Merge
|
||||
canMerge,
|
||||
mergeUnits,
|
||||
mergeDialog,
|
||||
fromUnit,
|
||||
toUnit,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
@ -29,6 +29,7 @@ from mealie.db.models.users import LongLiveToken, User
|
||||
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_units import RepositoryUnit
|
||||
from mealie.schema.cookbook.cookbook import ReadCookBook
|
||||
from mealie.schema.group.group_events import GroupEventNotifierOut
|
||||
from mealie.schema.group.group_exports import GroupDataExport
|
||||
@ -99,8 +100,8 @@ class AllRepositories:
|
||||
return RepositoryFood(self.session, PK_ID, IngredientFoodModel, IngredientFood)
|
||||
|
||||
@cached_property
|
||||
def ingredient_units(self) -> RepositoryGeneric[IngredientUnit, IngredientUnitModel]:
|
||||
return RepositoryGeneric(self.session, PK_ID, IngredientUnitModel, IngredientUnit)
|
||||
def ingredient_units(self) -> RepositoryUnit:
|
||||
return RepositoryUnit(self.session, PK_ID, IngredientUnitModel, IngredientUnit)
|
||||
|
||||
@cached_property
|
||||
def tools(self) -> RepositoryGeneric[RecipeTool, Tool]:
|
||||
|
32
mealie/repos/repository_units.py
Normal file
32
mealie/repos/repository_units.py
Normal file
@ -0,0 +1,32 @@
|
||||
from pydantic import UUID4
|
||||
|
||||
from mealie.db.models.recipe.ingredient import IngredientUnitModel
|
||||
from mealie.schema.recipe.recipe_ingredient import IngredientUnit
|
||||
|
||||
from .repository_generic import RepositoryGeneric
|
||||
|
||||
|
||||
class RepositoryUnit(RepositoryGeneric[IngredientUnit, IngredientUnitModel]):
|
||||
def merge(self, from_unit: UUID4, to_unit: UUID4) -> IngredientUnit | None:
|
||||
|
||||
from_model: IngredientUnitModel = (
|
||||
self.session.query(self.sql_model).filter_by(**self._filter_builder(**{"id": from_unit})).one()
|
||||
)
|
||||
|
||||
to_model: IngredientUnitModel = (
|
||||
self.session.query(self.sql_model).filter_by(**self._filter_builder(**{"id": to_unit})).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_unit)
|
||||
|
||||
def by_group(self, group_id: UUID4) -> "RepositoryUnit":
|
||||
return super().by_group(group_id) # type: ignore
|
@ -8,12 +8,7 @@ from mealie.routes._base.controller import controller
|
||||
from mealie.routes._base.mixins import CrudMixins
|
||||
from mealie.schema import mapper
|
||||
from mealie.schema.query import GetAll
|
||||
from mealie.schema.recipe.recipe_ingredient import (
|
||||
CreateIngredientFood,
|
||||
IngredientFood,
|
||||
IngredientMerge,
|
||||
SaveIngredientFood,
|
||||
)
|
||||
from mealie.schema.recipe.recipe_ingredient import CreateIngredientFood, IngredientFood, MergeFood, SaveIngredientFood
|
||||
from mealie.schema.response.responses import SuccessResponse
|
||||
|
||||
router = APIRouter(prefix="/foods", tags=["Recipes: Foods"])
|
||||
@ -34,7 +29,7 @@ class IngredientFoodsController(BaseUserController):
|
||||
)
|
||||
|
||||
@router.put("/merge", response_model=SuccessResponse)
|
||||
def merge_one(self, data: IngredientMerge):
|
||||
def merge_one(self, data: MergeFood):
|
||||
try:
|
||||
self.repo.merge(data.from_food, data.to_food)
|
||||
return SuccessResponse.respond("Successfully merged foods")
|
||||
|
@ -1,6 +1,6 @@
|
||||
from functools import cached_property
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from pydantic import UUID4
|
||||
|
||||
from mealie.routes._base.abc_controller import BaseUserController
|
||||
@ -8,7 +8,8 @@ from mealie.routes._base.controller import controller
|
||||
from mealie.routes._base.mixins import CrudMixins
|
||||
from mealie.schema import mapper
|
||||
from mealie.schema.query import GetAll
|
||||
from mealie.schema.recipe.recipe_ingredient import CreateIngredientUnit, IngredientUnit, SaveIngredientUnit
|
||||
from mealie.schema.recipe.recipe_ingredient import CreateIngredientUnit, IngredientUnit, MergeUnit, SaveIngredientUnit
|
||||
from mealie.schema.response.responses import SuccessResponse
|
||||
|
||||
router = APIRouter(prefix="/units", tags=["Recipes: Units"])
|
||||
|
||||
@ -27,6 +28,15 @@ class IngredientUnitsController(BaseUserController):
|
||||
self.registered_exceptions,
|
||||
)
|
||||
|
||||
@router.put("/merge", response_model=SuccessResponse)
|
||||
def merge_one(self, data: MergeUnit):
|
||||
try:
|
||||
self.repo.merge(data.from_unit, data.to_unit)
|
||||
return SuccessResponse.respond("Successfully merged units")
|
||||
except Exception as e:
|
||||
self.deps.logger.error(e)
|
||||
raise HTTPException(500, "Failed to merge units") from e
|
||||
|
||||
@router.get("", response_model=list[IngredientUnit])
|
||||
def get_all(self, q: GetAll = Depends(GetAll)):
|
||||
return self.repo.get_all(start=q.start, limit=q.limit)
|
||||
|
@ -95,11 +95,16 @@ class IngredientRequest(MealieModel):
|
||||
ingredient: str
|
||||
|
||||
|
||||
class IngredientMerge(MealieModel):
|
||||
class MergeFood(MealieModel):
|
||||
from_food: UUID4
|
||||
to_food: UUID4
|
||||
|
||||
|
||||
class MergeUnit(MealieModel):
|
||||
from_unit: UUID4
|
||||
to_unit: UUID4
|
||||
|
||||
|
||||
from mealie.schema.labels.multi_purpose_label import MultiPurposeLabelSummary
|
||||
|
||||
IngredientFood.update_forward_refs()
|
||||
|
48
tests/unit_tests/repository_tests/test_unit_repository.py
Normal file
48
tests/unit_tests/repository_tests/test_unit_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, SaveIngredientUnit
|
||||
from tests.utils.factories import random_string
|
||||
from tests.utils.fixture_schemas import TestUser
|
||||
|
||||
|
||||
def test_unit_merger(database: AllRepositories, unique_user: TestUser):
|
||||
slug1 = random_string(10)
|
||||
|
||||
unit_1 = database.ingredient_units.create(
|
||||
SaveIngredientUnit(
|
||||
name=random_string(10),
|
||||
group_id=unique_user.group_id,
|
||||
)
|
||||
)
|
||||
|
||||
unit_2 = database.ingredient_units.create(
|
||||
SaveIngredientUnit(
|
||||
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="", unit=unit_1), # type: ignore
|
||||
RecipeIngredient(note="", unit=unit_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.unit.id in [unit_1.id, unit_2.id] # type: ignore
|
||||
|
||||
database.ingredient_units.merge(unit_2.id, unit_1.id)
|
||||
|
||||
recipe = database.recipes.get_one(recipe.slug)
|
||||
|
||||
for ingredient in recipe.recipe_ingredient:
|
||||
assert ingredient.unit.id == unit_1.id # type: ignore
|
Loading…
x
Reference in New Issue
Block a user