# 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 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 AddIngredientForms, BasicFoodForms, TransformedFoodForms, BasicFoodUpdateForms, TransformedFoodUpdateForms, QRCodeForms 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) # 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, ProtectedCreateView): """ 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_success_url(self, **kwargs): self.object.refresh_from_db() return reverse_lazy('food:transformedfood_view', kwargs={"pk": self.object.pk}) class AddIngredientView(ProtectQuerysetMixin, 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() 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): 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() return context def get(self, *args, **kwargs): kwargs['stop_redirect'] = (Food.objects.get(pk=kwargs['pk']).polymorphic_ctype.model == 'transformedfood') return super().get(*args, **kwargs)