add merge funcions for units (#1146)

This commit is contained in:
Hayden 2022-04-09 19:57:49 -08:00 committed by GitHub
parent b93dae109e
commit db095656e1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 172 additions and 25 deletions

View File

@ -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 });
}
}

View File

@ -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,
};
},
});

View File

@ -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]:

View 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

View File

@ -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")

View File

@ -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)

View File

@ -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()

View 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