migration to store space and created_by on ShoppingListRecipe

ATTENTION: This deletes all old shopping list recipes that do not have entries associated with it. This should not matter but just for the record
This commit is contained in:
vabene1111
2025-03-14 13:51:49 +01:00
parent 5484506bc3
commit 7b7b726ec5
6 changed files with 93 additions and 21 deletions

View File

@@ -113,7 +113,7 @@ class RecipeShoppingEditor():
if not self.servings:
self.servings = getattr(self.mealplan, 'servings', None) or getattr(self.recipe, 'servings', 1.0)
self._shopping_list_recipe = ShoppingListRecipe.objects.create(recipe=self.recipe, mealplan=self.mealplan, servings=self.servings)
self._shopping_list_recipe = ShoppingListRecipe.objects.create(recipe=self.recipe, mealplan=self.mealplan, servings=self.servings, space=self.space, created_by=self.created_by)
if ingredients:
self._add_ingredients(ingredients=ingredients)

View File

@@ -0,0 +1,53 @@
# Generated by Django 4.2.18 on 2025-03-14 10:50
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
from django.db.models import F, Count
from django_scopes import scopes_disabled
def add_space_and_owner_to_shopping_list_recipe(apps, schema_editor):
print('migrating shopping list recipe space attribute, this might take a while ...')
with scopes_disabled():
ShoppingListRecipe = apps.get_model('cookbook', 'ShoppingListRecipe')
# delete all shopping list recipes that do not have entries as those are of no use anyway
ShoppingListRecipe.objects.annotate(entry_count=Count('entries')).filter(entry_count__lte=0).delete()
shopping_list_recipes = ShoppingListRecipe.objects.all().prefetch_related('entries')
update_list = []
for slr in shopping_list_recipes:
if entry := slr.entries.first():
if entry.space and entry.created_by:
slr.space = entry.space
slr.created_by = entry.created_by
update_list.append(slr)
else:
print(slr, 'missing data on entry')
else:
print(slr, 'missing entry')
ShoppingListRecipe.objects.bulk_update(update_list, ['space', 'created_by'], batch_size=500)
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('cookbook', '0219_connectorconfig_supports_description_field'),
]
operations = [
migrations.AddField(
model_name='shoppinglistrecipe',
name='created_by',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='shoppinglistrecipe',
name='space',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
),
migrations.RunPython(add_space_and_owner_to_shopping_list_recipe),
]

View File

@@ -0,0 +1,26 @@
# Generated by Django 4.2.18 on 2025-03-14 12:41
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('cookbook', '0220_shoppinglistrecipe_created_by_and_more'),
]
operations = [
migrations.AlterField(
model_name='shoppinglistrecipe',
name='created_by',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
),
migrations.AlterField(
model_name='shoppinglistrecipe',
name='space',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cookbook.space'),
),
]

View File

@@ -1179,31 +1179,18 @@ class MealPlan(ExportModelOperationsMixin('meal_plan'), models.Model, Permission
class ShoppingListRecipe(ExportModelOperationsMixin('shopping_list_recipe'), models.Model, PermissionModelMixin):
name = models.CharField(max_length=32, blank=True, default='')
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE, null=True, blank=True) # TODO make required after old shoppinglist deprecated
servings = models.DecimalField(default=1, max_digits=8, decimal_places=4)
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE, null=True, blank=True)
mealplan = models.ForeignKey(MealPlan, on_delete=models.CASCADE, null=True, blank=True)
objects = ScopedManager(space='recipe__space')
created_by = models.ForeignKey(User, on_delete=models.CASCADE)
space = models.ForeignKey(Space, on_delete=models.CASCADE)
@staticmethod
def get_space_key():
return 'recipe', 'space'
def get_space(self):
return self.recipe.space
objects = ScopedManager(space='space')
def __str__(self):
return f'Shopping list recipe {self.id} - {self.recipe}'
def get_owner(self):
try:
if not self.entries.exists():
return 'orphan'
else:
return getattr(self.entries.first(), 'created_by', None)
except AttributeError:
return None
class ShoppingListEntry(ExportModelOperationsMixin('shopping_list_entry'), models.Model, PermissionModelMixin):
list_recipe = models.ForeignKey(ShoppingListRecipe, on_delete=models.CASCADE, null=True, blank=True, related_name='entries')

View File

@@ -1093,6 +1093,7 @@ class ShoppingListRecipeSerializer(serializers.ModelSerializer):
mealplan_from_date = serializers.ReadOnlyField(source='mealplan.from_date')
mealplan_type = serializers.ReadOnlyField(source='mealplan.meal_type.name')
servings = CustomDecimalField()
created_by = UserSerializer(read_only=True)
def get_name(self, obj):
if not isinstance(value := obj.servings, Decimal):
@@ -1106,6 +1107,11 @@ class ShoppingListRecipeSerializer(serializers.ModelSerializer):
or obj.recipe.name
) + f' ({value:.2g})'
def create(self, validated_data):
validated_data['space'] = self.context['request'].space
validated_data['created_by'] = self.context['request'].user
return super().create(validated_data)
def update(self, instance, validated_data):
# TODO remove once old shopping list
if 'servings' in validated_data and self.context.get('view', None).__class__.__name__ != 'ShoppingListViewSet':
@@ -1116,8 +1122,8 @@ class ShoppingListRecipeSerializer(serializers.ModelSerializer):
class Meta:
model = ShoppingListRecipe
fields = ('id', 'recipe_name', 'name', 'recipe', 'mealplan', 'servings', 'mealplan_note', 'mealplan_from_date',
'mealplan_type')
read_only_fields = ('id',)
'mealplan_type', 'created_by')
read_only_fields = ('id', 'created_by',)
class ShoppingListEntrySerializer(WritableNestedModelSerializer):

View File

@@ -12,7 +12,7 @@ DETAIL_URL = 'api:shoppinglistrecipe-detail'
@pytest.fixture()
def obj_1(space_1, u1_s1, recipe_1_s1):
r = ShoppingListRecipe.objects.create(recipe=recipe_1_s1, servings=1)
r = ShoppingListRecipe.objects.create(recipe=recipe_1_s1, servings=1, space=space_1, created_by=auth.get_user(u1_s1))
for ing in r.recipe.steps.first().ingredients.all():
ShoppingListEntry.objects.create(list_recipe=r, ingredient=ing, food=ing.food, unit=ing.unit, amount=ing.amount, created_by=auth.get_user(u1_s1), space=space_1)
return r