Skip to content

Commit

Permalink
Connect steps and ingredient items through a relation model instead o…
Browse files Browse the repository at this point in the history
…f related field
  • Loading branch information
danielkjellid committed Jun 18, 2024
1 parent ecc55d1 commit 6c963ed
Show file tree
Hide file tree
Showing 13 changed files with 259 additions and 110 deletions.
10 changes: 8 additions & 2 deletions frontend/apps/recipes/edit/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@ interface RecipeEditInnerProps {
recipe: RecipeDetailRecordAPIResponse
ingredients: RecipeIngredientRecordListAPIResponse
}
refetch: () => void
}

function RecipeEditInner({ recipeId, results }: RecipeEditInnerProps) {
function RecipeEditInner({ recipeId, results, refetch }: RecipeEditInnerProps) {
const { data: ingredients } = results.ingredients
const { data: recipe } = results.recipe
const navigate = useNavigate()
Expand All @@ -36,6 +37,7 @@ function RecipeEditInner({ recipeId, results }: RecipeEditInnerProps) {
message: 'Recipe was successfully updated.',
})
navigate(routes.overview.build())
refetch()
} catch (e) {
console.log(e)
}
Expand All @@ -53,11 +55,15 @@ function RecipeEdit() {
urls.recipes.ingredients.list()
)

const refetch = () => {
recipe.reload()
}

return (
<View<object, RecipeEditInnerProps>
results={{ recipe: recipe, ingredients: ingredients }}
component={RecipeEditInner}
componentProps={{ recipeId: recipeId }}
componentProps={{ recipeId: recipeId, refetch: refetch }}
loadingProps={{ description: 'Loading' }}
errorProps={{ description: 'There was an error loading, please try again.' }}
/>
Expand Down
7 changes: 2 additions & 5 deletions nest/recipes/core/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,15 +134,12 @@ def create_or_update_recipe_attributes(
create_or_update_recipe_ingredient_item_groups(
recipe_id=recipe_id, ingredient_item_groups=ingredient_item_groups
)
create_or_update_recipe_steps(recipe_id=recipe_id, steps=steps)

# Once item groups and steps has been successfully updated, create or update
# existing ingredient items.
# Once items groups has been created, create steps and step item relations.
transaction.on_commit(
functools.partial(
create_or_update_recipe_ingredient_items,
create_or_update_recipe_steps,
recipe_id=recipe_id,
groups=ingredient_item_groups,
steps=steps,
)
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Generated by Django 4.2.7 on 2024-06-18 13:11

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('recipes_ingredients', '0001_initial'),
]

operations = [
migrations.RemoveField(
model_name='recipeingredientitem',
name='step',
),
]
7 changes: 0 additions & 7 deletions nest/recipes/ingredients/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,6 @@ class RecipeIngredientItem(BaseModel):
related_name="ingredient_items",
on_delete=models.CASCADE,
)
step = models.ForeignKey(
"recipes_steps.RecipeStep",
related_name="ingredient_items",
on_delete=models.SET_NULL,
null=True,
blank=True,
)

additional_info = models.CharField(max_length=255, null=True, blank=True)

Expand Down
38 changes: 1 addition & 37 deletions nest/recipes/ingredients/selectors.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def _get_recipe_ingredient_items(
qs = RecipeIngredientItem.objects.filter(expression)

ingredient_items = list(
qs.select_related("ingredient_group", "step").prefetch_related(
qs.select_related("ingredient_group").prefetch_related(
"portion_quantity_unit",
"ingredient",
"ingredient__product",
Expand Down Expand Up @@ -85,42 +85,6 @@ def get_recipe_ingredient_items_for_groups(
return records


def get_recipe_ingredient_items_for_steps(
*, step_ids: Iterable[int]
) -> FetchedResult[list[RecipeIngredientItemRecord]]:
"""
Get a list of RecipeIngredientItemRecord that based on related steps.
"""
records: FetchedResult[list[RecipeIngredientItemRecord]] = {}

for step_id in step_ids:
records[step_id] = []

ingredient_items = _get_recipe_ingredient_items(Q(step_id__in=step_ids))

for item in ingredient_items:
item_step_id = getattr(item, "step_id", None)

if item_step_id is None:
continue

records[item_step_id].append(
RecipeIngredientItemRecord(
id=item.id,
group_title=item.ingredient_group.title,
ingredient=RecipeIngredientRecord.from_db_model(item.ingredient),
additional_info=item.additional_info,
portion_quantity=item.portion_quantity,
portion_quantity_unit=UnitRecord.from_unit(item.portion_quantity_unit),
portion_quantity_display="{:f}".format(
item.portion_quantity.normalize()
),
)
)

return records


def get_recipe_ingredient_item_groups_for_recipes(
*,
recipe_ids: Iterable[int],
Expand Down
45 changes: 11 additions & 34 deletions nest/recipes/ingredients/services.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import functools
from datetime import timedelta
from typing import TYPE_CHECKING

import structlog
from django.db import models
from django.db import models, transaction
from django.http import HttpRequest
from pydantic import BaseModel, Field

Expand Down Expand Up @@ -73,45 +74,16 @@ def _get_ingredient_item_group_id(
)


def _get_step_id_for_item(
item: IngredientItem, steps: list["Step"], recipe_steps: models.QuerySet[RecipeStep]
) -> int | None:
# See if we can find the Step based on the possible child in the list.
step_for_item = next(
(step for step in steps if item in step.ingredient_items), None
)

if not step_for_item:
return None

# If a step exists, see if we can find the related RecipeStep that is created
# in the db.
step = next(
(
s.id
for s in recipe_steps
if s.number == step_for_item.number
and s.duration == timedelta(minutes=step_for_item.duration)
and s.step_type == step_for_item.step_type
),
None,
)

return step


def create_or_update_recipe_ingredient_items(
recipe_id: int,
groups: list[IngredientGroupItem],
steps: list["Step"],
) -> None:
logger.info("Creating and updating ingredient items")

ingredient_items_to_create: list[RecipeIngredientItem] = []
ingredient_items_to_update: list[RecipeIngredientItem] = []

recipe_groups = RecipeIngredientItemGroup.objects.filter(recipe_id=recipe_id)
recipe_steps = RecipeStep.objects.filter(recipe_id=recipe_id)
existing_recipe_ingredient_items = list(
RecipeIngredientItem.objects.filter(ingredient_group__recipe_id=recipe_id)
)
Expand All @@ -135,9 +107,6 @@ def create_or_update_recipe_ingredient_items(
ingredient_group_id=_get_ingredient_item_group_id(
group, recipe_groups
),
step_id=_get_step_id_for_item(
ingredient_item, steps, recipe_steps
),
ingredient_id=ingredient_item.ingredient_id,
additional_info=ingredient_item.additional_info,
portion_quantity=ingredient_item.portion_quantity,
Expand All @@ -162,7 +131,6 @@ def create_or_update_recipe_ingredient_items(
ingredient_items_to_update,
fields=[
"ingredient_group_id",
"step_id",
"ingredient_id",
"portion_quantity",
"portion_quantity_unit_id",
Expand Down Expand Up @@ -200,6 +168,7 @@ def _validate_ingredient_item_groups(
)


@transaction.atomic
def create_or_update_recipe_ingredient_item_groups(
recipe_id: int,
ingredient_item_groups: list[IngredientGroupItem],
Expand Down Expand Up @@ -232,3 +201,11 @@ def create_or_update_recipe_ingredient_item_groups(
groups_to_update,
fields=["title", "ordering"],
)

transaction.on_commit(
functools.partial(
create_or_update_recipe_ingredient_items,
recipe_id=recipe_id,
groups=ingredient_item_groups,
)
)
18 changes: 0 additions & 18 deletions nest/recipes/ingredients/tests/test_selectors.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
_get_recipe_ingredient_items,
get_recipe_ingredient_item_groups_for_recipes,
get_recipe_ingredient_items_for_groups,
get_recipe_ingredient_items_for_steps,
get_recipe_ingredients,
)
from .utils import (
Expand Down Expand Up @@ -74,23 +73,6 @@ def test_selector_get_recipe_ingredient_items_for_groups(self, mocker):
assert len(ingredient_items.keys()) == 3
assert all(key in group_ids for key in ingredient_items.keys())

def test_selector_get_ingredient_items_for_steps(self, mocker):
"""
Make sure that the get_ingredient_items_for_steps selector calls the
_get_ingredient_items selector with correct filter expression.
"""

step_ids = [4, 5, 6, 7]
selector_mock = mocker.patch(
"nest.recipes.ingredients.selectors._get_recipe_ingredient_items"
)
ingredient_items = get_recipe_ingredient_items_for_steps(step_ids=step_ids)

selector_mock.assert_called_once_with(Q(step_id__in=step_ids))

assert len(ingredient_items.keys()) == 4
assert all(key in step_ids for key in ingredient_items.keys())

def test_selector_get_ingredient_item_groups_for_recipes(
self, django_assert_num_queries
):
Expand Down
8 changes: 6 additions & 2 deletions nest/recipes/plans/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,14 @@ class RecipePlanItem(BaseModel):
"""

recipe_plan = models.ForeignKey(
RecipePlan, on_delete=models.CASCADE, related_name="plan_items"
RecipePlan,
on_delete=models.CASCADE,
related_name="plan_items",
)
recipe = models.ForeignKey(
Recipe, on_delete=models.CASCADE, related_name="plan_items"
Recipe,
on_delete=models.CASCADE,
related_name="plan_items",
)
ordering = models.PositiveIntegerField("ordering")

Expand Down
4 changes: 4 additions & 0 deletions nest/recipes/steps/managers.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,7 @@

class RecipeStepQuerySet(BaseQuerySet["models.RecipeStep"]):
...


class RecipeStepIngredientItemQuerySet(BaseQuerySet["models.RecipeStepIngredientItem"]):
...
32 changes: 32 additions & 0 deletions nest/recipes/steps/migrations/0002_recipestepingredientitem.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Generated by Django 4.2.7 on 2024-06-18 13:11

from django.db import migrations, models
import django.db.models.deletion
import django.db.models.manager


class Migration(migrations.Migration):

dependencies = [
('recipes_ingredients', '0002_remove_recipeingredientitem_step'),
('recipes_steps', '0001_initial'),
]

operations = [
migrations.CreateModel(
name='RecipeStepIngredientItem',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='created time')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='modified time')),
('ingredient_item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='steps', to='recipes_ingredients.recipeingredientitem')),
('step', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ingredient_items', to='recipes_steps.recipestep')),
],
options={
'abstract': False,
},
managers=[
('_objects', django.db.models.manager.Manager()),
],
),
]
25 changes: 25 additions & 0 deletions nest/recipes/steps/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from .enums import RecipeStepType
from .managers import (
RecipeStepQuerySet,
RecipeStepIngredientItemQuerySet,
)

_RecipeStepManager = models.Manager.from_queryset(RecipeStepQuerySet)
Expand Down Expand Up @@ -39,3 +40,27 @@ def get_step_type(self) -> RecipeStepType:
@property
def duration_minutes(self) -> int:
return round(self.duration.seconds / 60)


_RecipeStepIngredientItemManager = models.Manager.from_queryset(
RecipeStepIngredientItemQuerySet
)


class RecipeStepIngredientItem(BaseModel):
"""
A RecipeStepIngredientItem connects a step and an ingredient item.
"""

step = models.ForeignKey(
RecipeStep,
related_name="ingredient_items",
on_delete=models.CASCADE,
)
ingredient_item = models.ForeignKey(
"recipes_ingredients.RecipeIngredientItem",
related_name="steps",
on_delete=models.CASCADE,
)

objects = _RecipeStepIngredientItemManager()
Loading

0 comments on commit 6c963ed

Please sign in to comment.