From 48b1ef9ec8eedc943e869a9290d7937cbc21db3d Mon Sep 17 00:00:00 2001 From: Ehouarn Date: Sun, 2 Nov 2025 18:43:33 +0100 Subject: [PATCH] Add field 'traces' for model Food --- apps/food/forms.py | 4 +-- apps/food/migrations/0005_food_traces.py | 18 +++++++++++++ apps/food/models.py | 34 +++++++++++++++++++++++- apps/food/tables.py | 2 +- apps/food/views.py | 18 ++++++++++--- 5 files changed, 68 insertions(+), 8 deletions(-) create mode 100644 apps/food/migrations/0005_food_traces.py diff --git a/apps/food/forms.py b/apps/food/forms.py index fc88cbe2..0a7eac9e 100644 --- a/apps/food/forms.py +++ b/apps/food/forms.py @@ -55,7 +55,7 @@ class BasicFoodForms(forms.ModelForm): class Meta: model = BasicFood - fields = ('name', 'owner', 'date_type', 'expiry_date', 'allergens', 'order',) + fields = ('name', 'owner', 'date_type', 'expiry_date', 'allergens', 'traces', 'order',) widgets = { "owner": Autocomplete( model=Club, @@ -98,7 +98,7 @@ class BasicFoodUpdateForms(forms.ModelForm): """ class Meta: model = BasicFood - fields = ('name', 'owner', 'date_type', 'expiry_date', 'end_of_life', 'is_ready', 'order', 'allergens') + fields = ('name', 'owner', 'date_type', 'expiry_date', 'end_of_life', 'is_ready', 'order', 'allergens', 'traces') widgets = { "owner": Autocomplete( model=Club, diff --git a/apps/food/migrations/0005_food_traces.py b/apps/food/migrations/0005_food_traces.py new file mode 100644 index 00000000..4848ce78 --- /dev/null +++ b/apps/food/migrations/0005_food_traces.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.6 on 2025-11-02 17:38 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('food', '0004_alter_foodtransaction_order'), + ] + + operations = [ + migrations.AddField( + model_name='food', + name='traces', + field=models.ManyToManyField(blank=True, related_name='food_with_traces', to='food.allergen', verbose_name='traces'), + ), + ] diff --git a/apps/food/models.py b/apps/food/models.py index 7e6192e4..84a52ff7 100644 --- a/apps/food/models.py +++ b/apps/food/models.py @@ -53,6 +53,13 @@ class Food(PolymorphicModel): verbose_name=_('allergens'), ) + traces = models.ManyToManyField( + Allergen, + blank=True, + verbose_name=_('traces'), + related_name='food_with_traces' + ) + expiry_date = models.DateTimeField( verbose_name=_('expiry date'), null=False, @@ -91,6 +98,19 @@ class Food(PolymorphicModel): if old_allergens != list(parent.allergens.all()): parent.save(old_allergens=old_allergens) + @transaction.atomic + def update_traces(self): + # update parents + for parent in self.transformed_ingredient_inv.iterator(): + old_traces = list(parent.traces.all()).copy() + parent.traces.clear() + for child in parent.ingredients.iterator(): + if child.pk != self.pk: + parent.traces.set(parent.traces.union(child.traces.all())) + parent.traces.set(parent.traces.union(self.traces.all())) + if old_traces != list(parent.traces.all()): + parent.save(old_traces=old_traces) + def update_expiry_date(self): # update parents for parent in self.transformed_ingredient_inv.iterator(): @@ -142,6 +162,10 @@ class BasicFood(Food): and list(self.allergens.all()) != kwargs['old_allergens']): self.update_allergens() + if ('old_traces' in kwargs + and list(self.traces.all()) != kwargs['old_traces']): + self.update_traces() + # Expiry date if ((self.expiry_date != old_food.expiry_date and self.date_type == 'DLC') @@ -214,7 +238,7 @@ class TransformedFood(Food): created = self.pk is None if not created: # Check if important fields are updated - update = {'allergens': False, 'expiry_date': False} + update = {'allergens': False, 'traces': False, 'expiry_date': False} old_food = Food.objects.select_for_update().get(pk=self.pk) if not hasattr(self, "_force_save"): # Allergens @@ -224,6 +248,10 @@ class TransformedFood(Food): and list(self.allergens.all()) != kwargs['old_allergens']): update['allergens'] = True + if ('old_traces' in kwargs + and list(self.traces.all()) != kwargs['old_traces']): + update['traces'] = True + # Expiry date update['expiry_date'] = (self.shelf_life != old_food.shelf_life or self.creation_date != old_food.creation_date) @@ -234,6 +262,7 @@ class TransformedFood(Food): if ('old_ingredients' in kwargs and list(self.ingredients.all()) != list(kwargs['old_ingredients'])): update['allergens'] = True + update['traces'] = True update['expiry_date'] = True # it's preferable to keep a queryset but we allow list too @@ -243,6 +272,8 @@ class TransformedFood(Food): self.check_cycle(self.ingredients.all().difference(kwargs['old_ingredients']), self, []) if update['allergens']: self.update_allergens() + if update['traces']: + self.update_traces() if update['expiry_date']: self.update_expiry_date() @@ -254,6 +285,7 @@ class TransformedFood(Food): for child in self.ingredients.iterator(): self.allergens.set(self.allergens.union(child.allergens.all())) + self.traces.set(self.traces.union(child.traces.all())) if not (child.polymorphic_ctype.model == 'basicfood' and child.date_type == 'DDM'): self.expiry_date = min(self.expiry_date, child.expiry_date) return super().save(force_insert=False, force_update=force_update, using=using, update_fields=update_fields) diff --git a/apps/food/tables.py b/apps/food/tables.py index 964797f1..c2270dfe 100644 --- a/apps/food/tables.py +++ b/apps/food/tables.py @@ -32,7 +32,7 @@ class FoodTable(tables.Table): class Meta: model = Food template_name = 'django_tables2/bootstrap4.html' - fields = ('name', 'owner', 'qr_code_numbers', 'allergens', 'date', 'expiry_date') + fields = ('name', 'owner', 'qr_code_numbers', 'allergens', 'traces', 'date', 'expiry_date') row_attrs = { 'class': 'table-row', 'data-href': lambda record: 'detail/' + str(record.pk), diff --git a/apps/food/views.py b/apps/food/views.py index 3bb08a02..b56e0858 100644 --- a/apps/food/views.py +++ b/apps/food/views.py @@ -235,6 +235,8 @@ class BasicFoodCreateView(ProtectQuerysetMixin, ProtectedCreateView): for field in context['form'].fields: if field == 'allergens': context['form'].fields[field].initial = getattr(food, field).all() + elif field == 'traces': + context['form'].fields[field].initial = getattr(food, field).all() else: context['form'].fields[field].initial = getattr(food, field) @@ -294,6 +296,7 @@ class ManageIngredientsView(LoginRequiredMixin, UpdateView): def form_valid(self, form): old_ingredients = list(self.object.ingredients.all()).copy() old_allergens = list(self.object.allergens.all()).copy() + old_traces = list(self.object.traces.all()).copy() self.object.ingredients.clear() for i in range(self.object.ingredients.all().count() + 1 + MAX_FORMS): prefix = 'form-' + str(i) + '-' @@ -320,13 +323,15 @@ class ManageIngredientsView(LoginRequiredMixin, UpdateView): # We recalculate new expiry date and allergens self.object.expiry_date = self.object.creation_date + self.object.shelf_life self.object.allergens.clear() + self.object.traces.clear() for ingredient in self.object.ingredients.iterator(): if not (ingredient.polymorphic_ctype.model == 'basicfood' and ingredient.date_type == 'DDM'): self.object.expiry_date = min(self.object.expiry_date, ingredient.expiry_date) self.object.allergens.set(self.object.allergens.union(ingredient.allergens.all())) + self.object.tracese.set(self.object.traces.union(ingredient.traces.all())) - self.object.save(old_ingredients=old_ingredients, old_allergens=old_allergens) + self.object.save(old_ingredients=old_ingredients, old_allergens=old_allergens, old_traces=old_traces) return HttpResponseRedirect(self.get_success_url()) def get_context_data(self, *args, **kwargs): @@ -378,13 +383,15 @@ class AddIngredientView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): for meal in meals: old_ingredients = list(meal.ingredients.all()).copy() old_allergens = list(meal.allergens.all()).copy() + old_traces = list(meal.traces.all()).copy() meal.ingredients.add(self.object.pk) # update allergen and expiry date if necessary if not (self.object.polymorphic_ctype.model == 'basicfood' and self.object.date_type == 'DDM'): meal.expiry_date = min(meal.expiry_date, self.object.expiry_date) meal.allergens.set(meal.allergens.union(self.object.allergens.all())) - meal.save(old_ingredients=old_ingredients, old_allergens=old_allergens) + meal.traces.set(meal.traces.union(self.object.traces.all())) + meal.save(old_ingredients=old_ingredients, old_allergens=old_allergens, old_traces=old_traces) if 'fully_used' in form.data: if not self.object.end_of_life: self.object.end_of_life = _(f'Food fully used in : {meal.name}') @@ -414,6 +421,7 @@ class FoodUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): form.instance.creater = self.request.user food = Food.objects.get(pk=self.kwargs['pk']) old_allergens = list(food.allergens.all()).copy() + old_traces = list(food.traces.all()).copy() if food.polymorphic_ctype.model == 'transformedfood': old_ingredients = food.ingredients.all() @@ -427,7 +435,7 @@ class FoodUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): if food.polymorphic_ctype.model == 'transformedfood': form.instance.save(old_ingredients=old_ingredients) else: - form.instance.save(old_allergens=old_allergens) + form.instance.save(old_allergens=old_allergens, old_traces=old_traces) return ans def get_form_class(self, **kwargs): @@ -460,7 +468,7 @@ class FoodDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - fields = ["name", "owner", "expiry_date", "allergens", "is_ready", "end_of_life", "order"] + fields = ["name", "owner", "expiry_date", "allergens", "traces", "is_ready", "end_of_life", "order"] fields = dict([(field, getattr(self.object, field)) for field in fields]) if fields["is_ready"]: @@ -469,6 +477,8 @@ class FoodDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): fields["is_ready"] = _("No") fields["allergens"] = ", ".join( allergen.name for allergen in fields["allergens"].all()) + fields["traces"] = ", ".join( + trace.name for trace in fields["traces"].all()) context["fields"] = [( Food._meta.get_field(field).verbose_name.capitalize(),