# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later from datetime import timedelta from api.viewsets import is_regex from django_tables2.views import MultiTableMixin from django.db import transaction from django.db.models import Q from django.http import HttpResponseRedirect from django.views.generic import DetailView, UpdateView, CreateView from django.views.generic.list import ListView from django.urls import reverse_lazy from django.utils import timezone from django.utils.translation import gettext_lazy as _ from member.models import Club, Membership from permission.backends import PermissionBackend from permission.views import ProtectQuerysetMixin, ProtectedCreateView, LoginRequiredMixin from .models import Food, BasicFood, TransformedFood, QRCode from .forms import QRCodeForms, BasicFoodForms, TransformedFoodForms, \ ManageIngredientsForm, ManageIngredientsFormSet, AddIngredientForms, \ BasicFoodUpdateForms, TransformedFoodUpdateForms from .tables import FoodTable from .utils import pretty_duration class FoodListView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin, ListView): """ Display Food """ model = Food tables = [FoodTable, FoodTable, FoodTable, ] extra_context = {"title": _('Food')} template_name = 'food/food_list.html' def get_queryset(self, **kwargs): return super().get_queryset(**kwargs).distinct() def get_tables(self): bureau_role_pk = 4 clubs = Club.objects.filter(membership__in=Membership.objects.filter( user=self.request.user, roles=bureau_role_pk).filter( date_end__gte=timezone.now())) tables = [FoodTable] * (clubs.count() + 3) self.tables = tables tables = super().get_tables() tables[0].prefix = 'search-' tables[1].prefix = 'open-' tables[2].prefix = 'served-' for i in range(clubs.count()): tables[i + 3].prefix = clubs[i].name return tables def get_tables_data(self): # table search qs = self.get_queryset().order_by('name') if "search" in self.request.GET and self.request.GET['search']: pattern = self.request.GET['search'] # check regex valid_regex = is_regex(pattern) suffix = '__iregex' if valid_regex else '__istartswith' prefix = '^' if valid_regex else '' qs = qs.filter(Q(**{f'name{suffix}': prefix + pattern})) else: qs = qs.none() search_table = qs.filter(PermissionBackend.filter_queryset(self.request, Food, 'view')) # table open open_table = self.get_queryset().order_by('expiry_date').filter( Q(polymorphic_ctype__model='transformedfood') | Q(polymorphic_ctype__model='basicfood', basicfood__date_type='DLC')).filter( expiry_date__lt=timezone.now()).filter( PermissionBackend.filter_queryset(self.request, Food, 'view')) # table served served_table = self.get_queryset().order_by('-pk').filter( end_of_life='', is_ready=True).exclude( Q(polymorphic_ctype__model='basicfood', basicfood__date_type='DLC', expiry_date__lte=timezone.now(),) | Q(polymorphic_ctype__model='transformedfood', expiry_date__lte=timezone.now(), )) # tables club bureau_role_pk = 4 clubs = Club.objects.filter(membership__in=Membership.objects.filter( user=self.request.user, roles=bureau_role_pk).filter( date_end__gte=timezone.now())) club_table = [] for club in clubs: club_table.append(self.get_queryset().order_by('expiry_date').filter( owner=club, end_of_life='').filter( PermissionBackend.filter_queryset(self.request, Food, 'view') )) return [search_table, open_table, served_table] + club_table def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) tables = context['tables'] # for extends base_search.html we need to name 'search_table' in 'table' for name, table in zip(['table', 'open', 'served'], tables): context[name] = table context['club_tables'] = tables[3:] context['can_add_meal'] = PermissionBackend.check_perm(self.request, 'food.transformedfood_add') return context class QRCodeCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView): """ A view to add qrcode """ model = QRCode template_name = 'food/qrcode.html' form_class = QRCodeForms extra_context = {"title": _("Add a new QRCode")} def get(self, *args, **kwargs): qrcode = kwargs["slug"] if self.model.objects.filter(qr_code_number=qrcode).count() > 0: pk = self.model.objects.get(qr_code_number=qrcode).food_container.pk return HttpResponseRedirect(reverse_lazy("food:food_view", kwargs={"pk": pk})) else: return super().get(*args, **kwargs) @transaction.atomic def form_valid(self, form): qrcode_food_form = QRCodeForms(data=self.request.POST) if not qrcode_food_form.is_valid(): return self.form_invalid(form) qrcode = form.save(commit=False) qrcode.qr_code_number = self.kwargs['slug'] qrcode._force_save = True qrcode.save() qrcode.refresh_from_db() return super().form_valid(form) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['slug'] = self.kwargs['slug'] # get last 10 BasicFood objects with distincts 'name' ordered by '-pk' # we can't use .distinct and .order_by with differents columns hence the generator context['last_items'] = [food for food in BasicFood.get_lastests_objects(10, 'name', '-pk')] return context def get_success_url(self, **kwargs): self.object.refresh_from_db() return reverse_lazy('food:food_view', kwargs={'pk': self.object.food_container.pk}) def get_sample_object(self): return QRCode( qr_code_number=self.kwargs['slug'], food_container_id=1, ) class BasicFoodCreateView(ProtectQuerysetMixin, ProtectedCreateView): """ A view to add basicfood """ model = BasicFood form_class = BasicFoodForms extra_context = {"title": _("Add an aliment")} template_name = "food/food_update.html" def get_sample_object(self): return BasicFood( name="", owner_id=1, expiry_date=timezone.now(), is_ready=True, arrival_date=timezone.now(), date_type='DLC', ) @transaction.atomic def form_valid(self, form): if QRCode.objects.filter(qr_code_number=self.kwargs['slug']).count() > 0: return HttpResponseRedirect(reverse_lazy('food:qrcode_create', kwargs={'slug': self.kwargs['slug']})) food_form = BasicFoodForms(data=self.request.POST) if not food_form.is_valid(): return self.form_invalid(form) food = form.save(commit=False) food.is_ready = False food.save() food.refresh_from_db() qrcode = QRCode() qrcode.qr_code_number = self.kwargs['slug'] qrcode.food_container = food qrcode.save() return super().form_valid(form) def get_success_url(self, **kwargs): self.object.refresh_from_db() return reverse_lazy('food:basicfood_view', kwargs={"pk": self.object.pk}) def get_context_data(self, *args, **kwargs): context = super().get_context_data(*args, **kwargs) copy = self.request.GET.get('copy', None) if copy is not None: food = BasicFood.objects.get(pk=copy) print(context['form'].fields) for field in context['form'].fields: if field == 'allergens': context['form'].fields[field].initial = getattr(food, field).all() else: context['form'].fields[field].initial = getattr(food, field) return context class TransformedFoodCreateView(ProtectQuerysetMixin, ProtectedCreateView): """ A view to add transformedfood """ model = TransformedFood form_class = TransformedFoodForms extra_context = {"title": _("Add a meal")} template_name = "food/food_update.html" def get_sample_object(self): return TransformedFood( name="", owner_id=1, expiry_date=timezone.now(), is_ready=True, ) @transaction.atomic def form_valid(self, form): form.instance.expiry_date = timezone.now() + timedelta(days=3) form.instance.is_ready = False return super().form_valid(form) def get_context_data(self, *args, **kwargs): context = super().get_context_data(*args, **kwargs) context['title'] += ' ' + self.object.name return context def get_success_url(self, **kwargs): self.object.refresh_from_db() return reverse_lazy('food:transformedfood_view', kwargs={"pk": self.object.pk}) MAX_FORMS = 10 class ManageIngredientsView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): """ A view to manage ingredient for a transformed food """ model = TransformedFood fields = ['ingredients'] extra_context = {"title": _("Manage ingredients of:")} template_name = 'food/manage_ingredients.html' @transaction.atomic def form_valid(self, form): old_ingredients = list(self.object.ingredients.all()).copy() old_allergens = list(self.object.allergens.all()).copy() self.object.ingredients.clear() for i in range(self.object.ingredients.all().count() + 1 + MAX_FORMS): prefix = 'form-' + str(i) + '-' if form.data[prefix + 'qrcode'] not in ['0', '']: ingredient = QRCode.objects.get(pk=form.data[prefix + 'qrcode']).food_container self.object.ingredients.add(ingredient) if (prefix + 'fully_used') in form.data and form.data[prefix + 'fully_used'] == 'on': ingredient.end_of_life = _('Fully used in {meal}'.format( meal=self.object.name)) ingredient.save() elif form.data[prefix + 'name'] != '': ingredient = Food.objects.get(pk=form.data[prefix + 'name']) self.object.ingredients.add(ingredient) if (prefix + 'fully_used') in form.data and form.data[prefix + 'fully_used'] == 'on': ingredient.end_of_life = _('Fully used in {meal}'.format( meal=self.object.name)) ingredient.save() self.object.save(old_ingredients=old_ingredients, old_allergens=old_allergens) return HttpResponseRedirect(self.get_success_url()) def get_context_data(self, *args, **kwargs): context = super().get_context_data(*args, **kwargs) context['title'] += ' ' + self.object.name formset = ManageIngredientsFormSet() ingredients = self.object.ingredients.all() formset.extra += ingredients.count() + MAX_FORMS context['form'] = ManageIngredientsForm() context['ingredients_count'] = ingredients.count() display = [True] * (1 + ingredients.count()) + [False] * (formset.extra - ingredients.count() - 1) context['formset'] = zip(display, formset) context['ingredients'] = [] for ingredient in ingredients: qr = QRCode.objects.filter(food_container=ingredient) context['ingredients'].append({ 'food_pk': ingredient.pk, 'food_name': ingredient.name, 'qr_pk': '' if qr.count() == 0 else qr[0].pk, 'qr_number': '' if qr.count() == 0 else qr[0].qr_code_number, 'fully_used': 'true' if ingredient.end_of_life else '', }) return context def get_success_url(self, **kwargs): return reverse_lazy('food:transformedfood_view', kwargs={"pk": self.object.pk}) class AddIngredientView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): """ A view to add ingredient to a meal """ model = Food extra_context = {"title": _("Add the ingredient:")} form_class = AddIngredientForms template_name = 'food/food_update.html' def get_context_data(self, *args, **kwargs): context = super().get_context_data(*args, **kwargs) context['title'] += ' ' + self.object.name return context @transaction.atomic def form_valid(self, form): meals = TransformedFood.objects.filter(pk__in=form.data.getlist('ingredients')).all() if not meals: return HttpResponseRedirect(reverse_lazy('food:food_view', kwargs={"pk": self.object.pk})) for meal in meals: old_ingredients = list(meal.ingredients.all()).copy() old_allergens = list(meal.allergens.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) 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}') else: self.object.end_of_life += ', ' + meal.name if 'fully_used' in form.data: self.object.is_ready = False self.object.save() # We redirect only the first parent parent_pk = meals[0].pk return HttpResponseRedirect(self.get_success_url(parent_pk=parent_pk)) def get_success_url(self, **kwargs): return reverse_lazy('food:transformedfood_view', kwargs={"pk": kwargs['parent_pk']}) class FoodUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): """ A view to update Food """ model = Food extra_context = {"title": _("Update an aliment")} template_name = 'food/food_update.html' @transaction.atomic def form_valid(self, form): form.instance.creater = self.request.user food = Food.objects.get(pk=self.kwargs['pk']) old_allergens = list(food.allergens.all()).copy() if food.polymorphic_ctype.model == 'transformedfood': old_ingredients = food.ingredients.all() form.instance.shelf_life = timedelta( seconds=int(form.data['shelf_life']) * 60 * 60) food_form = self.get_form_class()(data=self.request.POST) if not food_form.is_valid(): return self.form_invalid(form) ans = super().form_valid(form) if food.polymorphic_ctype.model == 'transformedfood': form.instance.save(old_ingredients=old_ingredients) else: form.instance.save(old_allergens=old_allergens) return ans def get_form_class(self, **kwargs): food = Food.objects.get(pk=self.kwargs['pk']) if food.polymorphic_ctype.model == 'basicfood': return BasicFoodUpdateForms else: return TransformedFoodUpdateForms def get_form(self, **kwargs): form = super().get_form(**kwargs) if 'shelf_life' in form.initial: hours = form.initial['shelf_life'].days * 24 + form.initial['shelf_life'].seconds // 3600 form.initial['shelf_life'] = hours return form def get_success_url(self, **kwargs): self.object.refresh_from_db() return reverse_lazy('food:food_view', kwargs={"pk": self.object.pk}) class FoodDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): """ A view to see a food """ model = Food extra_context = {"title": _('Details of:')} context_object_name = "food" template_name = "food/food_detail.html" 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 = dict([(field, getattr(self.object, field)) for field in fields]) if fields["is_ready"]: fields["is_ready"] = _("Yes") else: fields["is_ready"] = _("No") fields["allergens"] = ", ".join( allergen.name for allergen in fields["allergens"].all()) context["fields"] = [( Food._meta.get_field(field).verbose_name.capitalize(), value) for field, value in fields.items()] context["meals"] = self.object.transformed_ingredient_inv.all() context["update"] = PermissionBackend.check_perm(self.request, "food.change_food") context["add_ingredient"] = (self.object.end_of_life == '' and PermissionBackend.check_perm(self.request, "food.change_transformedfood")) return context def get(self, *args, **kwargs): model = Food.objects.get(pk=kwargs['pk']).polymorphic_ctype.model if 'stop_redirect' in kwargs and kwargs['stop_redirect']: return super().get(*args, **kwargs) kwargs = {'pk': kwargs['pk']} if model == 'basicfood': return HttpResponseRedirect(reverse_lazy("food:basicfood_view", kwargs=kwargs)) return HttpResponseRedirect(reverse_lazy("food:transformedfood_view", kwargs=kwargs)) class BasicFoodDetailView(FoodDetailView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) fields = ['arrival_date', 'date_type'] for field in fields: context["fields"].append(( BasicFood._meta.get_field(field).verbose_name.capitalize(), getattr(self.object, field) )) return context def get(self, *args, **kwargs): if Food.objects.filter(pk=kwargs['pk']).count() == 1: kwargs['stop_redirect'] = (Food.objects.get(pk=kwargs['pk']).polymorphic_ctype.model == 'basicfood') return super().get(*args, **kwargs) class TransformedFoodDetailView(FoodDetailView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["fields"].append(( TransformedFood._meta.get_field("creation_date").verbose_name.capitalize(), self.object.creation_date )) context["fields"].append(( TransformedFood._meta.get_field("shelf_life").verbose_name.capitalize(), pretty_duration(self.object.shelf_life) )) context["foods"] = self.object.ingredients.all() context["manage_ingredients"] = True return context def get(self, *args, **kwargs): if Food.objects.filter(pk=kwargs['pk']).count() == 1: kwargs['stop_redirect'] = (Food.objects.get(pk=kwargs['pk']).polymorphic_ctype.model == 'transformedfood') return super().get(*args, **kwargs)