diff --git a/apps/food/admin.py b/apps/food/admin.py
index 89f042e1..613ebade 100644
--- a/apps/food/admin.py
+++ b/apps/food/admin.py
@@ -2,36 +2,58 @@
# SPDX-License-Identifier: GPL-3.0-or-later
from django.contrib import admin
-from django.db import transaction
+from polymorphic.admin import PolymorphicChildModelAdmin, PolymorphicParentModelAdmin
from note_kfet.admin import admin_site
-from .models import Allergen, BasicFood, QRCode, TransformedFood
-
-
-@admin.register(QRCode, site=admin_site)
-class QRCodeAdmin(admin.ModelAdmin):
- pass
-
-
-@admin.register(BasicFood, site=admin_site)
-class BasicFoodAdmin(admin.ModelAdmin):
- @transaction.atomic
- def save_related(self, *args, **kwargs):
- ans = super().save_related(*args, **kwargs)
- args[1].instance.update()
- return ans
-
-
-@admin.register(TransformedFood, site=admin_site)
-class TransformedFoodAdmin(admin.ModelAdmin):
- exclude = ["allergens", "expiry_date"]
-
- @transaction.atomic
- def save_related(self, request, form, *args, **kwargs):
- super().save_related(request, form, *args, **kwargs)
- form.instance.update()
+from .models import Allergen, Food, BasicFood, TransformedFood, QRCode
@admin.register(Allergen, site=admin_site)
class AllergenAdmin(admin.ModelAdmin):
- pass
+ """
+ Admin customisation for Allergen
+ """
+ ordering = ['name']
+
+
+@admin.register(Food, site=admin_site)
+class FoodAdmin(PolymorphicParentModelAdmin):
+ """
+ Admin customisation for Food
+ """
+ child_models = (Food, BasicFood, TransformedFood)
+ list_display = ('name', 'expiry_date', 'owner', 'is_ready')
+ list_filter = ('is_ready', 'end_of_life')
+ search_fields = ['name']
+ ordering = ['expiry_date', 'name']
+
+
+@admin.register(BasicFood, site=admin_site)
+class BasicFood(PolymorphicChildModelAdmin):
+ """
+ Admin customisation for BasicFood
+ """
+ list_display = ('name', 'expiry_date', 'date_type', 'owner', 'is_ready')
+ list_filter = ('is_ready', 'date_type', 'end_of_life')
+ search_fields = ['name']
+ ordering = ['expiry_date', 'name']
+
+
+@admin.register(TransformedFood, site=admin_site)
+class TransformedFood(PolymorphicChildModelAdmin):
+ """
+ Admin customisation for TransformedFood
+ """
+ list_display = ('name', 'expiry_date', 'shelf_life', 'owner', 'is_ready')
+ list_filter = ('is_ready', 'end_of_life', 'shelf_life')
+ search_fields = ['name']
+ ordering = ['expiry_date', 'name']
+
+
+@admin.register(QRCode, site=admin_site)
+class QRCodeAdmin(admin.ModelAdmin):
+ """
+ Admin customisation for QRCode
+ """
+ list_diplay = ('qr_code_number', 'food_container')
+ search_fields = ['food_container__name']
diff --git a/apps/food/api/serializers.py b/apps/food/api/serializers.py
index acac2ba9..fa0641e8 100644
--- a/apps/food/api/serializers.py
+++ b/apps/food/api/serializers.py
@@ -3,7 +3,7 @@
from rest_framework import serializers
-from ..models import Allergen, BasicFood, QRCode, TransformedFood
+from ..models import Allergen, BasicFood, TransformedFood, QRCode
class AllergenSerializer(serializers.ModelSerializer):
@@ -11,7 +11,6 @@ class AllergenSerializer(serializers.ModelSerializer):
REST API Serializer for Allergen.
The djangorestframework plugin will analyse the model `Allergen` and parse all fields in the API.
"""
-
class Meta:
model = Allergen
fields = '__all__'
@@ -22,29 +21,26 @@ class BasicFoodSerializer(serializers.ModelSerializer):
REST API Serializer for BasicFood.
The djangorestframework plugin will analyse the model `BasicFood` and parse all fields in the API.
"""
-
class Meta:
model = BasicFood
fields = '__all__'
-class QRCodeSerializer(serializers.ModelSerializer):
- """
- REST API Serializer for QRCode.
- The djangorestframework plugin will analyse the model `QRCode` and parse all fields in the API.
- """
-
- class Meta:
- model = QRCode
- fields = '__all__'
-
-
class TransformedFoodSerializer(serializers.ModelSerializer):
"""
REST API Serializer for TransformedFood.
The djangorestframework plugin will analyse the model `TransformedFood` and parse all fields in the API.
"""
-
class Meta:
model = TransformedFood
fields = '__all__'
+
+
+class QRCodeSerializer(serializers.ModelSerializer):
+ """
+ REST API Serializer for QRCode.
+ The djangorestframework plugin will analyse the model `QRCode` and parse all fields in the API.
+ """
+ class Meta:
+ model = QRCode
+ fields = '__all__'
diff --git a/apps/food/api/urls.py b/apps/food/api/urls.py
index 23c67bdd..5a8ce881 100644
--- a/apps/food/api/urls.py
+++ b/apps/food/api/urls.py
@@ -1,7 +1,7 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
-from .views import AllergenViewSet, BasicFoodViewSet, QRCodeViewSet, TransformedFoodViewSet
+from .views import AllergenViewSet, BasicFoodViewSet, TransformedFoodViewSet, QRCodeViewSet
def register_food_urls(router, path):
@@ -9,6 +9,6 @@ def register_food_urls(router, path):
Configure router for Food REST API.
"""
router.register(path + '/allergen', AllergenViewSet)
- router.register(path + '/basic_food', BasicFoodViewSet)
+ router.register(path + '/basicfood', BasicFoodViewSet)
+ router.register(path + '/transformedfood', TransformedFoodViewSet)
router.register(path + '/qrcode', QRCodeViewSet)
- router.register(path + '/transformed_food', TransformedFoodViewSet)
diff --git a/apps/food/api/views.py b/apps/food/api/views.py
index af616074..2c75a570 100644
--- a/apps/food/api/views.py
+++ b/apps/food/api/views.py
@@ -5,8 +5,8 @@ from api.viewsets import ReadProtectedModelViewSet
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.filters import SearchFilter
-from .serializers import AllergenSerializer, BasicFoodSerializer, QRCodeSerializer, TransformedFoodSerializer
-from ..models import Allergen, BasicFood, QRCode, TransformedFood
+from .serializers import AllergenSerializer, BasicFoodSerializer, TransformedFoodSerializer, QRCodeSerializer
+from ..models import Allergen, BasicFood, TransformedFood, QRCode
class AllergenViewSet(ReadProtectedModelViewSet):
@@ -26,7 +26,7 @@ class BasicFoodViewSet(ReadProtectedModelViewSet):
"""
REST API View set.
The djangorestframework plugin will get all `BasicFood` objects, serialize it to JSON with the given serializer,
- then render it on /api/food/basic_food/
+ then render it on /api/food/basicfood/
"""
queryset = BasicFood.objects.order_by('id')
serializer_class = BasicFoodSerializer
@@ -35,6 +35,19 @@ class BasicFoodViewSet(ReadProtectedModelViewSet):
search_fields = ['$name', ]
+class TransformedFoodViewSet(ReadProtectedModelViewSet):
+ """
+ REST API View set.
+ The djangorestframework plugin will get all `TransformedFood` objects, serialize it to JSON with the given serializer,
+ then render it on /api/food/transformedfood/
+ """
+ queryset = TransformedFood.objects.order_by('id')
+ serializer_class = TransformedFoodSerializer
+ filter_backends = [DjangoFilterBackend, SearchFilter]
+ filterset_fields = ['name', ]
+ search_fields = ['$name', ]
+
+
class QRCodeViewSet(ReadProtectedModelViewSet):
"""
REST API View set.
@@ -46,16 +59,3 @@ class QRCodeViewSet(ReadProtectedModelViewSet):
filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ['qr_code_number', ]
search_fields = ['$qr_code_number', ]
-
-
-class TransformedFoodViewSet(ReadProtectedModelViewSet):
- """
- REST API View set.
- The djangorestframework plugin will get all `TransformedFood` objects, serialize it to JSON with the given serializer,
- then render it on /api/food/transformed_food/
- """
- queryset = TransformedFood.objects.order_by('id')
- serializer_class = TransformedFoodSerializer
- filter_backends = [DjangoFilterBackend, SearchFilter]
- filterset_fields = ['name', ]
- search_fields = ['$name', ]
diff --git a/apps/food/fixtures/initial.json b/apps/food/fixtures/initial.json
new file mode 100644
index 00000000..43a0ffe1
--- /dev/null
+++ b/apps/food/fixtures/initial.json
@@ -0,0 +1,100 @@
+[
+ {
+ "model": "food.allergen",
+ "pk": 1,
+ "fields": {
+ "name": "Lait"
+ }
+ },
+ {
+ "model": "food.allergen",
+ "pk": 2,
+ "fields": {
+ "name": "Oeufs"
+ }
+ },
+ {
+ "model": "food.allergen",
+ "pk": 3,
+ "fields": {
+ "name": "Gluten"
+ }
+ },
+ {
+ "model": "food.allergen",
+ "pk": 4,
+ "fields": {
+ "name": "Fruits à coques"
+ }
+ },
+ {
+ "model": "food.allergen",
+ "pk": 5,
+ "fields": {
+ "name": "Arachides"
+ }
+ },
+ {
+ "model": "food.allergen",
+ "pk": 6,
+ "fields": {
+ "name": "Sésame"
+ }
+ },
+ {
+ "model": "food.allergen",
+ "pk": 7,
+ "fields": {
+ "name": "Soja"
+ }
+ },
+ {
+ "model": "food.allergen",
+ "pk": 8,
+ "fields": {
+ "name": "Céléri"
+ }
+ },
+ {
+ "model": "food.allergen",
+ "pk": 9,
+ "fields": {
+ "name": "Lupin"
+ }
+ },
+ {
+ "model": "food.allergen",
+ "pk": 10,
+ "fields": {
+ "name": "Moutarde"
+ }
+ },
+ {
+ "model": "food.allergen",
+ "pk": 11,
+ "fields": {
+ "name": "Sulfites"
+ }
+ },
+ {
+ "model": "food.allergen",
+ "pk": 12,
+ "fields": {
+ "name": "Crustacés"
+ }
+ },
+ {
+ "model": "food.allergen",
+ "pk": 13,
+ "fields": {
+ "name": "Mollusques"
+ }
+ },
+ {
+ "model": "food.allergen",
+ "pk": 14,
+ "fields": {
+ "name": "Poissons"
+ }
+ }
+]
diff --git a/apps/food/forms.py b/apps/food/forms.py
index af468c7f..c823b0b1 100644
--- a/apps/food/forms.py
+++ b/apps/food/forms.py
@@ -3,42 +3,42 @@
from random import shuffle
-from django import forms
-from django.utils.translation import gettext_lazy as _
-from django.utils import timezone
-from member.models import Club
from bootstrap_datepicker_plus.widgets import DateTimePickerInput
+from django import forms
+from django.forms.widgets import NumberInput
+from django.utils.translation import gettext_lazy as _
+from member.models import Club
from note_kfet.inputs import Autocomplete
from note_kfet.middlewares import get_current_request
from permission.backends import PermissionBackend
-from .models import BasicFood, QRCode, TransformedFood
+from .models import BasicFood, TransformedFood, QRCode
-class AddIngredientForms(forms.ModelForm):
+class QRCodeForms(forms.ModelForm):
"""
- Form for add an ingredient
+ Form for create QRCode for container
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
- self.fields['ingredient'].queryset = self.fields['ingredient'].queryset.filter(
- polymorphic_ctype__model='transformedfood',
+ self.fields['food_container'].queryset = self.fields['food_container'].queryset.filter(
is_ready=False,
- is_active=True,
- was_eaten=False,
- )
- # Caution, the logic is inverted here, we flip the logic on saving in AddIngredientView
- self.fields['is_active'].initial = True
- self.fields['is_active'].label = _("Fully used")
+ end_of_life__isnull=True,
+ polymorphic_ctype__model='transformedfood',
+ ).filter(PermissionBackend.filter_queryset(
+ get_current_request(),
+ TransformedFood,
+ "view",
+ ))
class Meta:
- model = TransformedFood
- fields = ('ingredient', 'is_active')
+ model = QRCode
+ fields = ('food_container',)
class BasicFoodForms(forms.ModelForm):
"""
- Form for add non-transformed food
+ Form for add basicfood
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -51,64 +51,103 @@ class BasicFoodForms(forms.ModelForm):
clubs = list(Club.objects.filter(PermissionBackend.filter_queryset(get_current_request(), Club, "change")).all())
shuffle(clubs)
self.fields['owner'].widget.attrs["placeholder"] = ", ".join(club.name for club in clubs[:4]) + ", ..."
+ self.fields['order'].widget.attrs["placeholder"] = _("Specific order given to GCKs")
class Meta:
model = BasicFood
- fields = ('name', 'owner', 'date_type', 'expiry_date', 'is_active', 'was_eaten', 'allergens',)
+ fields = ('name', 'owner', 'date_type', 'expiry_date', 'allergens', 'order',)
widgets = {
"owner": Autocomplete(
model=Club,
attrs={"api_url": "/api/members/club/"},
),
- 'expiry_date': DateTimePickerInput(),
+ "expiry_date": DateTimePickerInput(),
}
-class QRCodeForms(forms.ModelForm):
- """
- Form for create QRCode
- """
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
- self.fields['food_container'].queryset = self.fields['food_container'].queryset.filter(
- is_active=True,
- was_eaten=False,
- polymorphic_ctype__model='transformedfood',
- )
-
- class Meta:
- model = QRCode
- fields = ('food_container',)
-
-
class TransformedFoodForms(forms.ModelForm):
"""
- Form for add transformed food
+ Form for add transformedfood
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
- self.fields['name'].widget.attrs.update({"autofocus": "autofocus"})
self.fields['name'].required = True
self.fields['owner'].required = True
- self.fields['creation_date'].required = True
- self.fields['creation_date'].initial = timezone.now
- self.fields['is_active'].initial = True
- self.fields['is_ready'].initial = False
- self.fields['was_eaten'].initial = False
# Some example
self.fields['name'].widget.attrs.update({"placeholder": _("Lasagna")})
clubs = list(Club.objects.filter(PermissionBackend.filter_queryset(get_current_request(), Club, "change")).all())
shuffle(clubs)
self.fields['owner'].widget.attrs["placeholder"] = ", ".join(club.name for club in clubs[:4]) + ", ..."
+ self.fields['order'].widget.attrs["placeholder"] = _("Specific order given to GCKs")
class Meta:
model = TransformedFood
- fields = ('name', 'creation_date', 'owner', 'is_active', 'is_ready', 'was_eaten', 'shelf_life')
+ fields = ('name', 'owner', 'order',)
widgets = {
"owner": Autocomplete(
model=Club,
attrs={"api_url": "/api/members/club/"},
),
- 'creation_date': DateTimePickerInput(),
}
+
+
+class BasicFoodUpdateForms(forms.ModelForm):
+ """
+ Form for update basicfood object
+ """
+ class Meta:
+ model = BasicFood
+ fields = ('name', 'owner', 'date_type', 'expiry_date', 'end_of_life', 'is_ready', 'order', 'allergens')
+ widgets = {
+ "owner": Autocomplete(
+ model=Club,
+ attrs={"api_url": "/api/members/club/"},
+ ),
+ "expiry_date": DateTimePickerInput(),
+ }
+
+
+class TransformedFoodUpdateForms(forms.ModelForm):
+ """
+ Form for update transformedfood object
+ """
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.fields['shelf_life'].label = _('Shelf life (in hours)')
+
+ class Meta:
+ model = TransformedFood
+ fields = ('name', 'owner', 'end_of_life', 'is_ready', 'order', 'shelf_life')
+ widgets = {
+ "owner": Autocomplete(
+ model=Club,
+ attrs={"api_url": "/api/members/club/"},
+ ),
+ "expiry_date": DateTimePickerInput(),
+ "shelf_life": NumberInput(),
+ }
+
+
+class AddIngredientForms(forms.ModelForm):
+ """
+ Form for add an ingredient
+ """
+ fully_used = forms.BooleanField()
+ fully_used.initial = True
+ fully_used.required = False
+ fully_used.label = _("Fully used")
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ # TODO find a better way to get pk (be not url scheme dependant)
+ pk = get_current_request().path.split('/')[-1]
+ self.fields['ingredients'].queryset = self.fields['ingredients'].queryset.filter(
+ polymorphic_ctype__model="transformedfood",
+ is_ready=False,
+ end_of_life='',
+ ).filter(PermissionBackend.filter_queryset(get_current_request(), TransformedFood, "change")).exclude(pk=pk)
+
+ class Meta:
+ model = TransformedFood
+ fields = ('ingredients',)
diff --git a/apps/food/migrations/0001_initial.py b/apps/food/migrations/0001_initial.py
index 011d0f3f..706a0590 100644
--- a/apps/food/migrations/0001_initial.py
+++ b/apps/food/migrations/0001_initial.py
@@ -1,84 +1,199 @@
-# Generated by Django 2.2.28 on 2024-07-05 08:57
+# Generated by Django 4.2.20 on 2025-04-17 21:43
+import datetime
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
-
initial = True
dependencies = [
- ('contenttypes', '0002_remove_content_type_name'),
- ('member', '0011_profile_vss_charter_read'),
+ ("contenttypes", "0002_remove_content_type_name"),
+ ("member", "0013_auto_20240801_1436"),
]
operations = [
migrations.CreateModel(
- name='Allergen',
+ name="Allergen",
fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('name', models.CharField(max_length=255, verbose_name='name')),
+ (
+ "id",
+ models.AutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("name", models.CharField(max_length=255, verbose_name="name")),
],
options={
- 'verbose_name': 'Allergen',
- 'verbose_name_plural': 'Allergens',
+ "verbose_name": "Allergen",
+ "verbose_name_plural": "Allergens",
},
),
migrations.CreateModel(
- name='Food',
+ name="Food",
fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('name', models.CharField(max_length=255, verbose_name='name')),
- ('expiry_date', models.DateTimeField(verbose_name='expiry date')),
- ('was_eaten', models.BooleanField(default=False, verbose_name='was eaten')),
- ('is_ready', models.BooleanField(default=False, verbose_name='is ready')),
- ('allergens', models.ManyToManyField(blank=True, to='food.Allergen', verbose_name='allergen')),
- ('owner', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='member.Club', verbose_name='owner')),
- ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_food.food_set+', to='contenttypes.ContentType')),
+ (
+ "id",
+ models.AutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ ("name", models.CharField(max_length=255, verbose_name="name")),
+ ("expiry_date", models.DateTimeField(verbose_name="expiry date")),
+ (
+ "end_of_life",
+ models.CharField(max_length=255, verbose_name="end of life"),
+ ),
+ (
+ "is_ready",
+ models.BooleanField(max_length=255, verbose_name="is ready"),
+ ),
+ ("order", models.CharField(max_length=255, verbose_name="order")),
+ (
+ "allergens",
+ models.ManyToManyField(
+ blank=True, to="food.allergen", verbose_name="allergens"
+ ),
+ ),
+ (
+ "owner",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.PROTECT,
+ related_name="+",
+ to="member.club",
+ verbose_name="owner",
+ ),
+ ),
+ (
+ "polymorphic_ctype",
+ models.ForeignKey(
+ editable=False,
+ null=True,
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="polymorphic_%(app_label)s.%(class)s_set+",
+ to="contenttypes.contenttype",
+ ),
+ ),
],
options={
- 'verbose_name': 'foods',
+ "verbose_name": "Food",
+ "verbose_name_plural": "Foods",
},
),
migrations.CreateModel(
- name='BasicFood',
+ name="BasicFood",
fields=[
- ('food_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='food.Food')),
- ('date_type', models.CharField(choices=[('DLC', 'DLC'), ('DDM', 'DDM')], max_length=255)),
- ('arrival_date', models.DateTimeField(default=django.utils.timezone.now, verbose_name='arrival date')),
+ (
+ "food_ptr",
+ models.OneToOneField(
+ auto_created=True,
+ on_delete=django.db.models.deletion.CASCADE,
+ parent_link=True,
+ primary_key=True,
+ serialize=False,
+ to="food.food",
+ ),
+ ),
+ (
+ "arrival_date",
+ models.DateTimeField(
+ default=django.utils.timezone.now, verbose_name="arrival date"
+ ),
+ ),
+ (
+ "date_type",
+ models.CharField(
+ choices=[("DLC", "DLC"), ("DDM", "DDM")], max_length=255
+ ),
+ ),
],
options={
- 'verbose_name': 'Basic food',
- 'verbose_name_plural': 'Basic foods',
+ "verbose_name": "Basic food",
+ "verbose_name_plural": "Basic foods",
},
- bases=('food.food',),
+ bases=("food.food",),
),
migrations.CreateModel(
- name='QRCode',
+ name="QRCode",
fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('qr_code_number', models.PositiveIntegerField(unique=True, verbose_name='QR-code number')),
- ('food_container', models.OneToOneField(on_delete=django.db.models.deletion.PROTECT, related_name='QR_code', to='food.Food', verbose_name='food container')),
+ (
+ "id",
+ models.AutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ (
+ "qr_code_number",
+ models.PositiveIntegerField(
+ unique=True, verbose_name="qr code number"
+ ),
+ ),
+ (
+ "food_container",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="QR_code",
+ to="food.food",
+ verbose_name="food container",
+ ),
+ ),
],
options={
- 'verbose_name': 'QR-code',
- 'verbose_name_plural': 'QR-codes',
+ "verbose_name": "QR-code",
+ "verbose_name_plural": "QR-codes",
},
),
migrations.CreateModel(
- name='TransformedFood',
+ name="TransformedFood",
fields=[
- ('food_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='food.Food')),
- ('creation_date', models.DateTimeField(verbose_name='creation date')),
- ('is_active', models.BooleanField(default=True, verbose_name='is active')),
- ('ingredient', models.ManyToManyField(blank=True, related_name='transformed_ingredient_inv', to='food.Food', verbose_name='transformed ingredient')),
+ (
+ "food_ptr",
+ models.OneToOneField(
+ auto_created=True,
+ on_delete=django.db.models.deletion.CASCADE,
+ parent_link=True,
+ primary_key=True,
+ serialize=False,
+ to="food.food",
+ ),
+ ),
+ (
+ "creation_date",
+ models.DateTimeField(
+ default=django.utils.timezone.now, verbose_name="creation date"
+ ),
+ ),
+ (
+ "shelf_life",
+ models.DurationField(
+ default=datetime.timedelta(days=3), verbose_name="shelf life"
+ ),
+ ),
+ (
+ "ingredients",
+ models.ManyToManyField(
+ blank=True,
+ related_name="transformed_ingredient_inv",
+ to="food.food",
+ verbose_name="transformed ingredient",
+ ),
+ ),
],
options={
- 'verbose_name': 'Transformed food',
- 'verbose_name_plural': 'Transformed foods',
+ "verbose_name": "Transformed food",
+ "verbose_name_plural": "Transformed foods",
},
- bases=('food.food',),
+ bases=("food.food",),
),
]
diff --git a/apps/food/migrations/0002_transformedfood_shelf_life.py b/apps/food/migrations/0002_transformedfood_shelf_life.py
deleted file mode 100644
index 46673643..00000000
--- a/apps/food/migrations/0002_transformedfood_shelf_life.py
+++ /dev/null
@@ -1,19 +0,0 @@
-# Generated by Django 2.2.28 on 2024-07-06 20:37
-
-import datetime
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('food', '0001_initial'),
- ]
-
- operations = [
- migrations.AddField(
- model_name='transformedfood',
- name='shelf_life',
- field=models.DurationField(default=datetime.timedelta(days=3), verbose_name='shelf life'),
- ),
- ]
diff --git a/apps/food/migrations/0003_create_14_allergens_mandatory.py b/apps/food/migrations/0003_create_14_allergens_mandatory.py
deleted file mode 100644
index 236eaea4..00000000
--- a/apps/food/migrations/0003_create_14_allergens_mandatory.py
+++ /dev/null
@@ -1,62 +0,0 @@
-from django.db import migrations
-
-def create_14_mandatory_allergens(apps, schema_editor):
- """
- There are 14 mandatory allergens, they are pre-injected
- """
-
- Allergen = apps.get_model("food", "allergen")
-
- Allergen.objects.get_or_create(
- name="Gluten",
- )
- Allergen.objects.get_or_create(
- name="Fruits à coques",
- )
- Allergen.objects.get_or_create(
- name="Crustacés",
- )
- Allergen.objects.get_or_create(
- name="Céléri",
- )
- Allergen.objects.get_or_create(
- name="Oeufs",
- )
- Allergen.objects.get_or_create(
- name="Moutarde",
- )
- Allergen.objects.get_or_create(
- name="Poissons",
- )
- Allergen.objects.get_or_create(
- name="Soja",
- )
- Allergen.objects.get_or_create(
- name="Lait",
- )
- Allergen.objects.get_or_create(
- name="Sulfites",
- )
- Allergen.objects.get_or_create(
- name="Sésame",
- )
- Allergen.objects.get_or_create(
- name="Lupin",
- )
- Allergen.objects.get_or_create(
- name="Arachides",
- )
- Allergen.objects.get_or_create(
- name="Mollusques",
- )
-
-class Migration(migrations.Migration):
- dependencies = [
- ('food', '0002_transformedfood_shelf_life'),
- ]
-
- operations = [
- migrations.RunPython(create_14_mandatory_allergens),
- ]
-
-
diff --git a/apps/food/migrations/0004_auto_20240813_2358.py b/apps/food/migrations/0004_auto_20240813_2358.py
deleted file mode 100644
index d7fdf200..00000000
--- a/apps/food/migrations/0004_auto_20240813_2358.py
+++ /dev/null
@@ -1,28 +0,0 @@
-# Generated by Django 2.2.28 on 2024-08-13 21:58
-
-from django.db import migrations, models
-import django.db.models.deletion
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('food', '0003_create_14_allergens_mandatory'),
- ]
-
- operations = [
- migrations.RemoveField(
- model_name='transformedfood',
- name='is_active',
- ),
- migrations.AddField(
- model_name='food',
- name='is_active',
- field=models.BooleanField(default=True, verbose_name='is active'),
- ),
- migrations.AlterField(
- model_name='qrcode',
- name='food_container',
- field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='QR_code', to='food.Food', verbose_name='food container'),
- ),
- ]
diff --git a/apps/food/migrations/0005_alter_food_polymorphic_ctype.py b/apps/food/migrations/0005_alter_food_polymorphic_ctype.py
deleted file mode 100644
index 5473bffc..00000000
--- a/apps/food/migrations/0005_alter_food_polymorphic_ctype.py
+++ /dev/null
@@ -1,20 +0,0 @@
-# Generated by Django 4.2.15 on 2024-08-28 08:00
-
-from django.db import migrations, models
-import django.db.models.deletion
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('contenttypes', '0002_remove_content_type_name'),
- ('food', '0004_auto_20240813_2358'),
- ]
-
- operations = [
- migrations.AlterField(
- model_name='food',
- name='polymorphic_ctype',
- field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype'),
- ),
- ]
diff --git a/apps/food/models.py b/apps/food/models.py
index 199dcdd7..c0b25078 100644
--- a/apps/food/models.py
+++ b/apps/food/models.py
@@ -6,37 +6,13 @@ from datetime import timedelta
from django.db import models, transaction
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
-from member.models import Club
from polymorphic.models import PolymorphicModel
-
-
-class QRCode(models.Model):
- """
- An QRCode model
- """
- qr_code_number = models.PositiveIntegerField(
- verbose_name=_("QR-code number"),
- unique=True,
- )
-
- food_container = models.ForeignKey(
- 'Food',
- on_delete=models.CASCADE,
- related_name='QR_code',
- verbose_name=_('food container'),
- )
-
- class Meta:
- verbose_name = _("QR-code")
- verbose_name_plural = _("QR-codes")
-
- def __str__(self):
- return _("QR-code number {qr_code_number}").format(qr_code_number=self.qr_code_number)
+from member.models import Club
class Allergen(models.Model):
"""
- A list of allergen and alimentary restrictions
+ Allergen and alimentary restrictions
"""
name = models.CharField(
verbose_name=_('name'),
@@ -44,16 +20,19 @@ class Allergen(models.Model):
)
class Meta:
- verbose_name = _('Allergen')
- verbose_name_plural = _('Allergens')
+ verbose_name = _("Allergen")
+ verbose_name_plural = _("Allergens")
def __str__(self):
return self.name
class Food(PolymorphicModel):
+ """
+ Describe any type of food
+ """
name = models.CharField(
- verbose_name=_('name'),
+ verbose_name=_("name"),
max_length=255,
)
@@ -67,7 +46,7 @@ class Food(PolymorphicModel):
allergens = models.ManyToManyField(
Allergen,
blank=True,
- verbose_name=_('allergen'),
+ verbose_name=_('allergens'),
)
expiry_date = models.DateTimeField(
@@ -75,41 +54,69 @@ class Food(PolymorphicModel):
null=False,
)
- was_eaten = models.BooleanField(
- default=False,
- verbose_name=_('was eaten'),
+ end_of_life = models.CharField(
+ blank=True,
+ verbose_name=_('end of life'),
+ max_length=255,
)
- # is_ready != is_active : is_ready signifie que la nourriture est prête à être manger,
- # is_active signifie que la nourriture n'est pas encore archivé
- # il sert dans les cas où il est plus intéressant que de l'open soit conservé (confiture par ex)
-
is_ready = models.BooleanField(
- default=False,
verbose_name=_('is ready'),
+ max_length=255,
)
- is_active = models.BooleanField(
- default=True,
- verbose_name=_('is active'),
+ order = models.CharField(
+ blank=True,
+ verbose_name=_('order'),
+ max_length=255,
)
def __str__(self):
return self.name
@transaction.atomic
- def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
- return super().save(force_insert, force_update, using, update_fields)
+ def update_allergens(self):
+ # update parents
+ for parent in self.transformed_ingredient_inv.iterator():
+ old_allergens = list(parent.allergens.all()).copy()
+ parent.allergens.clear()
+ for child in parent.ingredients.iterator():
+ if child.pk != self.pk:
+ parent.allergens.set(parent.allergens.union(child.allergens.all()))
+ parent.allergens.set(parent.allergens.union(self.allergens.all()))
+ if old_allergens != list(parent.allergens.all()):
+ parent.save(old_allergens=old_allergens)
+
+ def update_expiry_date(self):
+ # update parents
+ for parent in self.transformed_ingredient_inv.iterator():
+ old_expiry_date = parent.expiry_date
+ parent.expiry_date = parent.shelf_life + parent.creation_date
+ for child in parent.ingredients.iterator():
+ if (child.pk != self.pk
+ and not (child.polymorphic_ctype.model == 'basicfood'
+ and child.date_type == 'DDM')):
+ parent.expiry_date = min(parent.expiry_date, child.expiry_date)
+
+ if self.polymorphic_ctype.model == 'basicfood' and self.date_type == 'DLC':
+ parent.expiry_date = min(parent.expiry_date, self.expiry_date)
+ if old_expiry_date != parent.expiry_date:
+ parent.save()
class Meta:
- verbose_name = _('food')
- verbose_name = _('foods')
+ verbose_name = _('Food')
+ verbose_name_plural = _('Foods')
class BasicFood(Food):
"""
- Food which has been directly buy on supermarket
+ A basic food is a food directly buy and stored
"""
+ arrival_date = models.DateTimeField(
+ default=timezone.now,
+ verbose_name=_('arrival date'),
+ )
+
date_type = models.CharField(
max_length=255,
choices=(
@@ -118,50 +125,70 @@ class BasicFood(Food):
)
)
- arrival_date = models.DateTimeField(
- verbose_name=_('arrival date'),
- default=timezone.now,
- )
-
- # label = models.ImageField(
- # verbose_name=_('food label'),
- # max_length=255,
- # blank=False,
- # null=False,
- # upload_to='label/',
- # )
-
@transaction.atomic
- def update_allergens(self):
- # update parents
- for parent in self.transformed_ingredient_inv.iterator():
- parent.update_allergens()
+ def save(self, force_insert=False, force_update=False, using=None, update_fields=None, **kwargs):
+ created = self.pk is None
+ if not created:
+ # Check if important fields are updated
+ old_food = Food.objects.select_for_update().get(pk=self.pk)
+ if not hasattr(self, "_force_save"):
+ # Allergens
- @transaction.atomic
- def update_expiry_date(self):
- # update parents
- for parent in self.transformed_ingredient_inv.iterator():
- parent.update_expiry_date()
+ if ('old_allergens' in kwargs
+ and list(self.allergens.all()) != kwargs['old_allergens']):
+ self.update_allergens()
- @transaction.atomic
- def update(self):
- self.update_allergens()
- self.update_expiry_date()
+ # Expiry date
+ if ((self.expiry_date != old_food.expiry_date
+ and self.date_type == 'DLC')
+ or old_food.date_type != self.date_type):
+ self.update_expiry_date()
+
+ return super().save(force_insert, force_update, using, update_fields)
+
+ @staticmethod
+ def get_lastests_objects(number, distinct_field, order_by_field):
+ """
+ Get the last object with distinct field and ranked with order_by
+ This methods exist because we can't distinct with one field and
+ order with another
+ """
+ foods = BasicFood.objects.order_by(order_by_field).all()
+ field = []
+ for food in foods:
+ if getattr(food, distinct_field) in field:
+ continue
+ else:
+ field.append(getattr(food, distinct_field))
+ number -= 1
+ yield food
+ if not number:
+ return
class Meta:
verbose_name = _('Basic food')
verbose_name_plural = _('Basic foods')
+ def __str__(self):
+ return self.name
+
class TransformedFood(Food):
"""
- Transformed food are a mix between basic food and meal
+ A transformed food is a food with ingredients
"""
creation_date = models.DateTimeField(
+ default=timezone.now,
verbose_name=_('creation date'),
)
- ingredient = models.ManyToManyField(
+ # Without microbiological analyzes, the storage time is 3 days
+ shelf_life = models.DurationField(
+ default=timedelta(days=3),
+ verbose_name=_('shelf life'),
+ )
+
+ ingredients = models.ManyToManyField(
Food,
blank=True,
symmetrical=False,
@@ -169,58 +196,91 @@ class TransformedFood(Food):
verbose_name=_('transformed ingredient'),
)
- # Without microbiological analyzes, the storage time is 3 days
- shelf_life = models.DurationField(
- verbose_name=_("shelf life"),
- default=timedelta(days=3),
- )
+ def check_cycle(self, ingredients, origin, checked):
+ for ingredient in ingredients:
+ if ingredient == origin:
+ # We break the cycle
+ self.ingredients.remove(ingredient)
+ if ingredient.polymorphic_ctype.model == 'transformedfood' and ingredient not in checked:
+ ingredient.check_cycle(ingredient.ingredients.all(), origin, checked)
+ checked.append(ingredient)
@transaction.atomic
- def archive(self):
- # When a meal are archived, if it was eaten, update ingredient fully used for this meal
- raise NotImplementedError
+ def save(self, force_insert=False, force_update=False, using=None, update_fields=None, **kwargs):
+ created = self.pk is None
+ if not created:
+ # Check if important fields are updated
+ update = {'allergens': False, 'expiry_date': False}
+ old_food = Food.objects.select_for_update().get(pk=self.pk)
+ if not hasattr(self, "_force_save"):
+ # Allergens
+ # Unfortunately with the many-to-many relation we can't access
+ # to old allergens
+ if ('old_allergens' in kwargs
+ and list(self.allergens.all()) != kwargs['old_allergens']):
+ update['allergens'] = True
- @transaction.atomic
- def update_allergens(self):
- # When allergens are changed, simply update the parents' allergens
- old_allergens = list(self.allergens.all())
- self.allergens.clear()
- for ingredient in self.ingredient.iterator():
- self.allergens.set(self.allergens.union(ingredient.allergens.all()))
+ # Expiry date
+ update['expiry_date'] = (self.shelf_life != old_food.shelf_life
+ or self.creation_date != old_food.creation_date)
+ if update['expiry_date']:
+ self.expiry_date = self.creation_date + self.shelf_life
+ # Unfortunately with the set method ingredients are already save,
+ # we check cycle after if possible
+ if ('old_ingredients' in kwargs
+ and list(self.ingredients.all()) != list(kwargs['old_ingredients'])):
+ update['allergens'] = True
+ update['expiry_date'] = True
- if old_allergens == list(self.allergens.all()):
- return
- super().save()
+ # it's preferable to keep a queryset but we allow list too
+ if type(kwargs['old_ingredients']) is list:
+ kwargs['old_ingredients'] = Food.objects.filter(
+ pk__in=[food.pk for food in kwargs['old_ingredients']])
+ self.check_cycle(self.ingredients.all().difference(kwargs['old_ingredients']), self, [])
+ if update['allergens']:
+ self.update_allergens()
+ if update['expiry_date']:
+ self.update_expiry_date()
- # update parents
- for parent in self.transformed_ingredient_inv.iterator():
- parent.update_allergens()
+ if created:
+ self.expiry_date = self.shelf_life + self.creation_date
- @transaction.atomic
- def update_expiry_date(self):
- # When expiry_date is changed, simply update the parents' expiry_date
- old_expiry_date = self.expiry_date
- self.expiry_date = self.creation_date + self.shelf_life
- for ingredient in self.ingredient.iterator():
- self.expiry_date = min(self.expiry_date, ingredient.expiry_date)
+ # We save here because we need pk for many-to-many relation
+ super().save(force_insert, force_update, using, update_fields)
- if old_expiry_date == self.expiry_date:
- return
- super().save()
-
- # update parents
- for parent in self.transformed_ingredient_inv.iterator():
- parent.update_expiry_date()
-
- @transaction.atomic
- def update(self):
- self.update_allergens()
- self.update_expiry_date()
-
- @transaction.atomic
- def save(self, *args, **kwargs):
- super().save(*args, **kwargs)
+ for child in self.ingredients.iterator():
+ self.allergens.set(self.allergens.union(child.allergens.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, force_update, using, update_fields)
class Meta:
verbose_name = _('Transformed food')
verbose_name_plural = _('Transformed foods')
+
+ def __str__(self):
+ return self.name
+
+
+class QRCode(models.Model):
+ """
+ QR-code for register food
+ """
+ qr_code_number = models.PositiveIntegerField(
+ unique=True,
+ verbose_name=_('qr code number'),
+ )
+
+ food_container = models.ForeignKey(
+ Food,
+ on_delete=models.CASCADE,
+ related_name='QR_code',
+ verbose_name=_('food container'),
+ )
+
+ class Meta:
+ verbose_name = _('QR-code')
+ verbose_name_plural = _('QR-codes')
+
+ def __str__(self):
+ return _('QR-code number') + ' ' + str(self.qr_code_number)
diff --git a/apps/food/tables.py b/apps/food/tables.py
index 4ab15879..7789ad76 100644
--- a/apps/food/tables.py
+++ b/apps/food/tables.py
@@ -2,18 +2,20 @@
# SPDX-License-Identifier: GPL-3.0-or-later
import django_tables2 as tables
-from django_tables2 import A
-from .models import TransformedFood
+from .models import Food
-class TransformedFoodTable(tables.Table):
- name = tables.LinkColumn(
- 'food:food_view',
- args=[A('pk'), ],
- )
-
+class FoodTable(tables.Table):
+ """
+ List all foods.
+ """
class Meta:
- model = TransformedFood
+ model = Food
template_name = 'django_tables2/bootstrap4.html'
- fields = ('name', "owner", "allergens", "expiry_date")
+ fields = ('name', 'owner', 'allergens', 'expiry_date')
+ row_attrs = {
+ 'class': 'table-row',
+ 'data-href': lambda record: 'detail/' + str(record.pk),
+ 'style': 'cursor:pointer',
+ }
diff --git a/apps/food/templates/food/add_ingredient_form.html b/apps/food/templates/food/add_ingredient_form.html
deleted file mode 100644
index 395928e4..00000000
--- a/apps/food/templates/food/add_ingredient_form.html
+++ /dev/null
@@ -1,20 +0,0 @@
-{% extends "base.html" %}
-{% comment %}
-SPDX-License-Identifier: GPL-3.0-or-later
-{% endcomment %}
-{% load i18n crispy_forms_tags %}
-
-{% block content %}
-
-{% endblock %}
diff --git a/apps/food/templates/food/basicfood_detail.html b/apps/food/templates/food/basicfood_detail.html
deleted file mode 100644
index 846fadba..00000000
--- a/apps/food/templates/food/basicfood_detail.html
+++ /dev/null
@@ -1,37 +0,0 @@
-{% extends "base.html" %}
-{% comment %}
-SPDX-License-Identifier: GPL-3.0-or-later
-{% endcomment %}
-{% load i18n crispy_forms_tags %}
-
-{% block content %}
-
-
-
-
- {% trans 'Owner' %} : {{ food.owner }}
- {% trans 'Arrival date' %} : {{ food.arrival_date }}
- {% trans 'Expiry date' %} : {{ food.expiry_date }} ({{ food.date_type }})
- - {% trans 'Allergens' %} :
-
- {% for allergen in food.allergens.iterator %}
- - {{ allergen.name }}
- {% endfor %}
-
-
-
{% trans 'Active' %} : {{ food.is_active }}
- {% trans 'Eaten' %} : {{ food.was_eaten }}
-
- {% if can_update %}
-
{% trans 'Update' %}
- {% endif %}
- {% if can_add_ingredient %}
-
- {% trans 'Add to a meal' %}
-
- {% endif %}
-
-
-{% endblock %}
diff --git a/apps/food/templates/food/create_qrcode_form.html b/apps/food/templates/food/create_qrcode_form.html
deleted file mode 100644
index 456b9970..00000000
--- a/apps/food/templates/food/create_qrcode_form.html
+++ /dev/null
@@ -1,55 +0,0 @@
-{% extends "base.html" %}
-{% comment %}
-SPDX-License-Identifier: GPL-3.0-or-later
-{% endcomment %}
-{% load render_table from django_tables2 %}
-{% load i18n crispy_forms_tags %}
-
-{% block content %}
-
-
-
-
- {% trans 'New basic food' %}
-
-
-
-
{% trans "Copy constructor" %}
-
-
-
-
- {% trans "Name" %}
- |
-
- {% trans "Owner" %}
- |
-
- {% trans "Arrival date" %}
- |
-
- {% trans "Expiry date" %}
- |
-
-
-
- {% for basic in last_basic %}
-
- {{ basic.name }} |
- {{ basic.owner }} |
- {{ basic.arrival_date }} |
- {{ basic.expiry_date }} |
-
- {% endfor %}
-
-
-
-
-
-{% endblock %}
diff --git a/apps/food/templates/food/food_detail.html b/apps/food/templates/food/food_detail.html
new file mode 100644
index 00000000..d330ad64
--- /dev/null
+++ b/apps/food/templates/food/food_detail.html
@@ -0,0 +1,48 @@
+{% extends "base.html" %}
+{% comment %}
+Copyright (C) by BDE ENS Paris-Saclay
+SPDX-License-Identifier: GPL-3.0-or-later
+{% endcomment %}
+{% load i18n crispy_forms_tags %}
+
+{% block content %}
+
+{% endblock %}
diff --git a/apps/food/templates/food/food_list.html b/apps/food/templates/food/food_list.html
new file mode 100644
index 00000000..efc7a554
--- /dev/null
+++ b/apps/food/templates/food/food_list.html
@@ -0,0 +1,71 @@
+{% extends "base_search.html" %}
+{% comment %}
+Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
+SPDX-License-Identifier: GPL-3.0-or-later
+{% endcomment %}
+{% load render_table from django_tables2 %}
+{% load i18n %}
+
+{% block content %}
+{{ block.super }}
+
+
+
+ {% if can_add_meal %}
+
+ {% endif %}
+ {% if served.data %}
+ {% render_table served %}
+ {% else %}
+
+
+ {% trans "There is no meal served." %}
+
+
+
+ {% endif %}
+
+
+ {% if open.data %}
+ {% render_table open %}
+ {% else %}
+
+
+ {% trans "There is no free food." %}
+
+
+ {% endif %}
+
+{% if club_tables %}
+
+
+
+ {% for table in club_tables %}
+
+
+ {% if table.data %}
+ {% render_table table %}
+ {% else %}
+
+
+ {% trans "Yours club has not food yet." %}
+
+
+ {% endif %}
+
+ {% endfor %}
+ {% endif %}
+
+{% endblock %}
diff --git a/apps/food/templates/food/basicfood_form.html b/apps/food/templates/food/food_update.html
similarity index 92%
rename from apps/food/templates/food/basicfood_form.html
rename to apps/food/templates/food/food_update.html
index 6fe6f06f..67de3e27 100644
--- a/apps/food/templates/food/basicfood_form.html
+++ b/apps/food/templates/food/food_update.html
@@ -1,5 +1,6 @@
{% extends "base.html" %}
{% comment %}
+Copyright (C) by BDE ENS Paris-Saclay
SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %}
{% load i18n crispy_forms_tags %}
diff --git a/apps/food/templates/food/qrcode.html b/apps/food/templates/food/qrcode.html
new file mode 100644
index 00000000..49c9eccb
--- /dev/null
+++ b/apps/food/templates/food/qrcode.html
@@ -0,0 +1,52 @@
+{% extends "base.html" %}
+{% comment %}
+Copyright (C) by BDE ENS Paris-Saclay
+SPDX-License-Identifier: GPL-3.0-or-later
+{% endcomment %}
+{% load i18n crispy_forms_tags %}
+{% load render_table from django_tables2 %}
+
+{% block content %}
+
+
+
+
+
+
+
+
+
+
+ {% trans "Name" %}
+ |
+
+ {% trans "Owner" %}
+ |
+
+ {% trans "Expiry date" %}
+ |
+
+
+
+ {% for food in last_items %}
+
+ {{ food.name }} |
+ {{ food.owner }} |
+ {{ food.expiry_date }} |
+
+ {% endfor %}
+
+
+
+
+
+{% endblock %}
diff --git a/apps/food/templates/food/qrcode_detail.html b/apps/food/templates/food/qrcode_detail.html
deleted file mode 100644
index 6e3e8110..00000000
--- a/apps/food/templates/food/qrcode_detail.html
+++ /dev/null
@@ -1,39 +0,0 @@
-{% extends "base.html" %}
-{% comment %}
-SPDX-License-Identifier: GPL-3.0-or-later
-{% endcomment %}
-{% load i18n crispy_forms_tags %}
-
-{% block content %}
-
-{% endblock %}
diff --git a/apps/food/templates/food/transformedfood_detail.html b/apps/food/templates/food/transformedfood_detail.html
deleted file mode 100644
index ca32bc06..00000000
--- a/apps/food/templates/food/transformedfood_detail.html
+++ /dev/null
@@ -1,51 +0,0 @@
-{% extends "base.html" %}
-{% comment %}
-SPDX-License-Identifier: GPL-3.0-or-later
-{% endcomment %}
-{% load i18n crispy_forms_tags %}
-
-{% block content %}
-
-
-
-
- {% trans 'Owner' %} : {{ food.owner }}
- {% if can_see_ready %}
- {% trans 'Ready' %} : {{ food.is_ready }}
- {% endif %}
- {% trans 'Creation date' %} : {{ food.creation_date }}
- {% trans 'Expiry date' %} : {{ food.expiry_date }}
- - {% trans 'Allergens' %} :
-
- {% for allergen in food.allergens.iterator %}
- - {{ allergen.name }}
- {% endfor %}
-
-
-
- {% trans 'Ingredients' %} :
-
-
-
{% trans 'Shelf life' %} : {{ food.shelf_life }}
- {% trans 'Ready' %} : {{ food.is_ready }}
- {% trans 'Active' %} : {{ food.is_active }}
- {% trans 'Eaten' %} : {{ food.was_eaten }}
-
- {% if can_update %}
-
- {% trans 'Update' %}
-
- {% endif %}
- {% if can_add_ingredient %}
-
- {% trans 'Add to a meal' %}
-
- {% endif %}
-
-
-{% endblock %}
diff --git a/apps/food/templates/food/transformedfood_form.html b/apps/food/templates/food/transformedfood_form.html
deleted file mode 100644
index 395928e4..00000000
--- a/apps/food/templates/food/transformedfood_form.html
+++ /dev/null
@@ -1,20 +0,0 @@
-{% extends "base.html" %}
-{% comment %}
-SPDX-License-Identifier: GPL-3.0-or-later
-{% endcomment %}
-{% load i18n crispy_forms_tags %}
-
-{% block content %}
-
-{% endblock %}
diff --git a/apps/food/templates/food/transformedfood_list.html b/apps/food/templates/food/transformedfood_list.html
deleted file mode 100644
index 4416cdb7..00000000
--- a/apps/food/templates/food/transformedfood_list.html
+++ /dev/null
@@ -1,60 +0,0 @@
-{% extends "base.html" %}
-{% comment %}
-SPDX-License-Identifier: GPL-3.0-or-later
-{% endcomment %}
-{% load render_table from django_tables2 %}
-{% load i18n %}
-
-{% block content %}
-
-
- {% if can_create_meal %}
-
- {% endif %}
- {% if served.data %}
- {% render_table served %}
- {% else %}
-
-
- {% trans "There is no meal served." %}
-
-
- {% endif %}
-
-
-
-
- {% if open.data %}
- {% render_table open %}
- {% else %}
-
-
- {% trans "There is no free meal." %}
-
-
- {% endif %}
-
-
-
-
- {% if table.data %}
- {% render_table table %}
- {% else %}
-
-
- {% trans "There is no meal." %}
-
-
- {% endif %}
-
-{% endblock %}
diff --git a/apps/food/tests.py b/apps/food/tests.py
deleted file mode 100644
index a79ca8be..00000000
--- a/apps/food/tests.py
+++ /dev/null
@@ -1,3 +0,0 @@
-# from django.test import TestCase
-
-# Create your tests here.
diff --git a/apps/food/tests/test_food.py b/apps/food/tests/test_food.py
new file mode 100644
index 00000000..9c314bf7
--- /dev/null
+++ b/apps/food/tests/test_food.py
@@ -0,0 +1,170 @@
+# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from api.tests import TestAPI
+from django.contrib.auth.models import User
+from django.test import TestCase
+from django.urls import reverse
+from django.utils import timezone
+
+from ..api.views import AllergenViewSet, BasicFoodViewSet, TransformedFoodViewSet, QRCodeViewSet
+from ..models import Allergen, BasicFood, TransformedFood, QRCode
+
+
+class TestFood(TestCase):
+ """
+ Test food
+ """
+ fixtures = ('initial',)
+
+ def setUp(self):
+ self.user = User.objects.create_superuser(
+ username='admintoto',
+ password='toto1234',
+ email='toto@example.com'
+ )
+ self.client.force_login(self.user)
+
+ sess = self.client.session
+ sess['permission_mask'] = 42
+ sess.save()
+
+ self.allergen = Allergen.objects.create(
+ name='allergen',
+ )
+
+ self.basicfood = BasicFood.objects.create(
+ name='basicfood',
+ owner_id=1,
+ expiry_date=timezone.now(),
+ is_ready=False,
+ date_type='DLC',
+ )
+
+ self.transformedfood = TransformedFood.objects.create(
+ name='transformedfood',
+ owner_id=1,
+ expiry_date=timezone.now(),
+ is_ready=False,
+ )
+
+ self.qrcode = QRCode.objects.create(
+ qr_code_number=1,
+ food_container=self.basicfood,
+ )
+
+ def test_food_list(self):
+ """
+ Display food list
+ """
+ response = self.client.get(reverse('food:food_list'))
+ self.assertEqual(response.status_code, 200)
+
+ def test_qrcode_create(self):
+ """
+ Display QRCode creation
+ """
+ response = self.client.get(reverse('food:qrcode_create'))
+ self.assertEqual(response.status_code, 200)
+
+ def test_basicfood_create(self):
+ """
+ Display BasicFood creation
+ """
+ response = self.client.get(reverse('food:basicfood_create'))
+ self.assertEqual(response.status_code, 200)
+
+ def test_transformedfood_create(self):
+ """
+ Display TransformedFood creation
+ """
+ response = self.client.get(reverse('food:transformedfood_create'))
+ self.assertEqual(response.status_code, 200)
+
+ def test_food_create(self):
+ """
+ Display Food update
+ """
+ response = self.client.get(reverse('food:food_update'))
+ self.assertEqual(response.status_code, 200)
+
+ def test_food_view(self):
+ """
+ Display Food detail
+ """
+ response = self.client.get(reverse('food:food_view'))
+ self.assertEqual(response.status_code, 302)
+
+ def test_basicfood_view(self):
+ """
+ Display BasicFood detail
+ """
+ response = self.client.get(reverse('food:basicfood_view'))
+ self.assertEqual(response.status_code, 200)
+
+ def test_transformedfood_view(self):
+ """
+ Display TransformedFood detail
+ """
+ response = self.client.get(reverse('food:transformedfood_view'))
+ self.assertEqual(response.status_code, 200)
+
+ def test_add_ingredient(self):
+ """
+ Display add ingredient view
+ """
+ response = self.client.get(reverse('food:add_ingredient'))
+ self.assertEqual(response.status_code, 200)
+
+
+class TestFoodAPI(TestAPI):
+ def setUp(self) -> None:
+ super().setUP()
+
+ self.allergen = Allergen.objects.create(
+ name='name',
+ )
+
+ self.basicfood = BasicFood.objects.create(
+ name='basicfood',
+ owner_id=1,
+ expiry_date=timezone.now(),
+ is_ready=False,
+ date_type='DLC',
+ )
+
+ self.transformedfood = TransformedFood.objects.create(
+ name='transformedfood',
+ owner_id=1,
+ expiry_date=timezone.now(),
+ is_ready=False,
+ )
+
+ self.qrcode = QRCode.objects.create(
+ qr_code_number=1,
+ food_container=self.basicfood,
+ )
+
+ def test_allergen_api(self):
+ """
+ Load Allergen API page and test all filters and permissions
+ """
+ self.check_viewset(AllergenViewSet, '/api/food/allergen/')
+
+ def test_basicfood_api(self):
+ """
+ Load BasicFood API page and test all filters and permissions
+ """
+ self.check_viewset(BasicFoodViewSet, '/api/food/basicfood/')
+
+ def test_transformedfood_api(self):
+ """
+ Load TransformedFood API page and test all filters and permissions
+ """
+ self.check_viewset(TransformedFoodViewSet, '/api/food/transformedfood/')
+
+ def test_qrcode_api(self):
+ """
+ Load QRCode API page and test all filters and permissions
+ """
+ self.check_viewset(QRCodeViewSet, '/api/food/qrcode/')
diff --git a/apps/food/urls.py b/apps/food/urls.py
index 59063cfe..8137a6f1 100644
--- a/apps/food/urls.py
+++ b/apps/food/urls.py
@@ -8,14 +8,13 @@ from . import views
app_name = 'food'
urlpatterns = [
- path('', views.TransformedListView.as_view(), name='food_list'),
- path('', views.QRCodeView.as_view(), name='qrcode_view'),
- path('detail/', views.FoodView.as_view(), name='food_view'),
-
- path('/create_qrcode', views.QRCodeCreateView.as_view(), name='qrcode_create'),
- path('/create_qrcode/basic', views.QRCodeBasicFoodCreateView.as_view(), name='qrcode_basic_create'),
- path('create/transformed', views.TransformedFoodCreateView.as_view(), name='transformed_create'),
- path('update/basic/', views.BasicFoodUpdateView.as_view(), name='basic_update'),
- path('update/transformed/', views.TransformedFoodUpdateView.as_view(), name='transformed_update'),
- path('add/', views.AddIngredientView.as_view(), name='add_ingredient'),
+ path('', views.FoodListView.as_view(), name='food_list'),
+ path('', views.QRCodeCreateView.as_view(), name='qrcode_create'),
+ path('/add/basic', views.BasicFoodCreateView.as_view(), name='basicfood_create'),
+ path('add/transformed', views.TransformedFoodCreateView.as_view(), name='transformedfood_create'),
+ path('update/', views.FoodUpdateView.as_view(), name='food_update'),
+ path('detail/', views.FoodDetailView.as_view(), name='food_view'),
+ path('detail/basic/', views.BasicFoodDetailView.as_view(), name='basicfood_view'),
+ path('detail/transformed/', views.TransformedFoodDetailView.as_view(), name='transformedfood_view'),
+ path('add/ingredient/', views.AddIngredientView.as_view(), name='add_ingredient'),
]
diff --git a/apps/food/utils.py b/apps/food/utils.py
new file mode 100644
index 00000000..a08d949a
--- /dev/null
+++ b/apps/food/utils.py
@@ -0,0 +1,53 @@
+# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from django.utils.translation import gettext_lazy as _
+
+seconds = (_('second'), _('seconds'))
+minutes = (_('minute'), _('minutes'))
+hours = (_('hour'), _('hours'))
+days = (_('day'), _('days'))
+weeks = (_('week'), _('weeks'))
+
+
+def plural(x):
+ if x == 1:
+ return 0
+ return 1
+
+
+def pretty_duration(duration):
+ """
+ I receive datetime.timedelta object
+ You receive string object
+ """
+ text = []
+ sec = duration.seconds
+ d = duration.days
+
+ if d >= 7:
+ w = d // 7
+ text.append(str(w) + ' ' + weeks[plural(w)])
+ d -= w * 7
+ if d > 0:
+ text.append(str(d) + ' ' + days[plural(d)])
+
+ if sec >= 3600:
+ h = sec // 3600
+ text.append(str(h) + ' ' + hours[plural(h)])
+ sec -= h * 3600
+
+ if sec >= 60:
+ m = sec // 60
+ text.append(str(m) + ' ' + minutes[plural(m)])
+ sec -= m * 60
+
+ if sec > 0:
+ text.append(str(sec) + ' ' + seconds[plural(sec)])
+
+ if len(text) == 0:
+ return ''
+ if len(text) == 1:
+ return text[0]
+ if len(text) >= 2:
+ return ', '.join(t for t in text[:-1]) + ' ' + _('and') + ' ' + text[-1]
diff --git a/apps/food/views.py b/apps/food/views.py
index 8c63530c..96c6b89e 100644
--- a/apps/food/views.py
+++ b/apps/food/views.py
@@ -1,421 +1,408 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
-from django.db import transaction
-from django.contrib.auth.mixins import LoginRequiredMixin
-from django.http import HttpResponseRedirect
+from datetime import timedelta
+
+from api.viewsets import is_regex
from django_tables2.views import MultiTableMixin
-from django.urls import reverse
-from django.utils.translation import gettext_lazy as _
-from django.utils import timezone
+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.forms import HiddenInput
+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
+from permission.views import ProtectQuerysetMixin, ProtectedCreateView, LoginRequiredMixin
-from .forms import AddIngredientForms, BasicFoodForms, QRCodeForms, TransformedFoodForms
-from .models import BasicFood, Food, QRCode, TransformedFood
-from .tables import TransformedFoodTable
+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 AddIngredientView(ProtectQuerysetMixin, UpdateView):
+class FoodListView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin, ListView):
"""
- A view to add an ingredient
+ Display Food
"""
model = Food
- template_name = 'food/add_ingredient_form.html'
- extra_context = {"title": _("Add the ingredient")}
- form_class = AddIngredientForms
+ tables = [FoodTable, FoodTable, FoodTable, ]
+ extra_context = {"title": _('Food')}
+ template_name = 'food/food_list.html'
- def get_context_data(self, **kwargs):
- context = super().get_context_data(**kwargs)
- context["pk"] = self.kwargs["pk"]
- return context
+ def get_queryset(self, **kwargs):
+ return super().get_queryset(**kwargs).distinct()
- @transaction.atomic
- def form_valid(self, form):
- form.instance.creater = self.request.user
- food = Food.objects.get(pk=self.kwargs['pk'])
- add_ingredient_form = AddIngredientForms(data=self.request.POST)
- if food.is_ready:
- form.add_error(None, _("The product is already prepared"))
- return self.form_invalid(form)
- if not add_ingredient_form.is_valid():
- return self.form_invalid(form)
+ 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()))
- # We flip logic ""fully used = not is_active""
- food.is_active = not food.is_active
- # Save the aliment and the allergens associed
- for transformed_pk in self.request.POST.getlist('ingredient'):
- transformed = TransformedFood.objects.get(pk=transformed_pk)
- if not transformed.is_ready:
- transformed.ingredient.add(food)
- transformed.update()
- food.save()
+ 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
- return HttpResponseRedirect(self.get_success_url())
+ 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']
- def get_success_url(self, **kwargs):
- return reverse('food:food_list')
-
-
-class BasicFoodUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
- """
- A view to update a basic food
- """
- model = BasicFood
- form_class = BasicFoodForms
- template_name = 'food/basicfood_form.html'
- extra_context = {"title": _("Update an aliment")}
-
- @transaction.atomic
- def form_valid(self, form):
- form.instance.creater = self.request.user
- basic_food_form = BasicFoodForms(data=self.request.POST)
- if not basic_food_form.is_valid():
- return self.form_invalid(form)
-
- ans = super().form_valid(form)
- form.instance.update()
- return ans
-
- def get_success_url(self, **kwargs):
- self.object.refresh_from_db()
- return reverse('food:food_view', kwargs={"pk": self.object.pk})
-
- def get_context_data(self, **kwargs):
- context = super().get_context_data(**kwargs)
- return context
-
-
-class FoodView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
- """
- A view to see a food
- """
- model = Food
- extra_context = {"title": _("Details of:")}
- context_object_name = "food"
+ # 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).filter(
+ 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)
- context["can_update"] = PermissionBackend.check_perm(self.request, "food.change_food")
- context["can_add_ingredient"] = PermissionBackend.check_perm(self.request, "food.change_transformedfood")
- return context
-
-
-class QRCodeBasicFoodCreateView(ProtectQuerysetMixin, ProtectedCreateView):
- #####################################################################
- # TO DO
- # - this feature is very pratical for meat or fish, nevertheless we can implement this later
- # - fix picture save
- # - implement solution crop and convert image (reuse or recode ImageForm from members apps)
- #####################################################################
- """
- A view to add a basic food with a qrcode
- """
- model = BasicFood
- form_class = BasicFoodForms
- template_name = 'food/basicfood_form.html'
- extra_context = {"title": _("Add a new basic food with QRCode")}
-
- @transaction.atomic
- def form_valid(self, form):
- form.instance.creater = self.request.user
- basic_food_form = BasicFoodForms(data=self.request.POST)
- if not basic_food_form.is_valid():
- return self.form_invalid(form)
-
- # Save the aliment and the allergens associed
- basic_food = form.save(commit=False)
- # We assume the date of labeling and the same as the date of arrival
- basic_food.arrival_date = timezone.now()
- basic_food.is_ready = False
- basic_food.is_active = True
- basic_food.was_eaten = False
- basic_food._force_save = True
- basic_food.save()
- basic_food.refresh_from_db()
-
- qrcode = QRCode()
- qrcode.qr_code_number = self.kwargs['slug']
- qrcode.food_container = basic_food
- qrcode.save()
-
- return super().form_valid(form)
-
- def get_success_url(self, **kwargs):
- self.object.refresh_from_db()
- return reverse('food:qrcode_view', kwargs={"slug": self.kwargs['slug']})
-
- def get_sample_object(self):
-
- # We choose a club which may work or BDE else
- owner_id = 1
- for membership in self.request.user.memberships.all():
- club_id = membership.club.id
- food = BasicFood(name="", expiry_date=timezone.now(), owner_id=club_id)
- if PermissionBackend.check_perm(self.request, "food.add_basicfood", food):
- owner_id = club_id
-
- return BasicFood(
- name="",
- expiry_date=timezone.now(),
- owner_id=owner_id,
- )
-
- def get_context_data(self, **kwargs):
- # Some field are hidden on create
- context = super().get_context_data(**kwargs)
-
- form = context['form']
- form.fields['is_active'].widget = HiddenInput()
- form.fields['was_eaten'].widget = HiddenInput()
-
- copy = self.request.GET.get('copy', None)
- if copy is not None:
- basic = BasicFood.objects.get(pk=copy)
- for field in ['date_type', 'expiry_date', 'name', 'owner']:
- form.fields[field].initial = getattr(basic, field)
- for field in ['allergens']:
- form.fields[field].initial = getattr(basic, field).all()
+ 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 a new qrcode
+ A view to add qrcode
"""
model = QRCode
- template_name = 'food/create_qrcode_form.html'
+ 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:
- return HttpResponseRedirect(reverse("food:qrcode_view", kwargs=kwargs))
+ 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)
- def get_context_data(self, **kwargs):
- context = super().get_context_data(**kwargs)
- context["slug"] = self.kwargs["slug"]
-
- context["last_basic"] = BasicFood.objects.order_by('-pk').all()[:10]
-
- return context
-
@transaction.atomic
def form_valid(self, form):
- form.instance.creater = self.request.user
qrcode_food_form = QRCodeForms(data=self.request.POST)
if not qrcode_food_form.is_valid():
return self.form_invalid(form)
- # Save the qrcode
qrcode = form.save(commit=False)
- qrcode.qr_code_number = self.kwargs["slug"]
+ qrcode.qr_code_number = self.kwargs['slug']
qrcode._force_save = True
qrcode.save()
qrcode.refresh_from_db()
+ return super().form_valid(form)
- qrcode.food_container.save()
+ 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('food:qrcode_view', kwargs={"slug": self.kwargs['slug']})
+ return reverse_lazy('food:basicfood_view', kwargs={"pk": self.object.pk})
- def get_sample_object(self):
- return QRCode(
- qr_code_number=self.kwargs["slug"],
- food_container_id=1
- )
+ 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)
-class QRCodeView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
- """
- A view to see a qrcode
- """
- model = QRCode
- extra_context = {"title": _("QRCode")}
- context_object_name = "qrcode"
- slug_field = "qr_code_number"
-
- def get(self, *args, **kwargs):
- qrcode = kwargs["slug"]
- if self.model.objects.filter(qr_code_number=qrcode).count() > 0:
- return super().get(*args, **kwargs)
- else:
- return HttpResponseRedirect(reverse("food:qrcode_create", kwargs=kwargs))
-
- def get_context_data(self, **kwargs):
- context = super().get_context_data(**kwargs)
-
- qr_code_number = self.kwargs['slug']
- qrcode = self.model.objects.get(qr_code_number=qr_code_number)
-
- model = qrcode.food_container.polymorphic_ctype.model
-
- if model == "basicfood":
- context["can_update_basic"] = PermissionBackend.check_perm(self.request, "food.change_basicfood")
- context["can_view_detail"] = PermissionBackend.check_perm(self.request, "food.view_basicfood")
- if model == "transformedfood":
- context["can_update_transformed"] = PermissionBackend.check_perm(self.request, "food.change_transformedfood")
- context["can_view_detail"] = PermissionBackend.check_perm(self.request, "food.view_transformedfood")
- context["can_add_ingredient"] = PermissionBackend.check_perm(self.request, "food.change_transformedfood")
return context
class TransformedFoodCreateView(ProtectQuerysetMixin, ProtectedCreateView):
"""
- A view to add a tranformed food
+ A view to add transformedfood
"""
model = TransformedFood
- template_name = 'food/transformedfood_form.html'
form_class = TransformedFoodForms
- extra_context = {"title": _("Add a new meal")}
-
- @transaction.atomic
- def form_valid(self, form):
- form.instance.creater = self.request.user
- transformed_food_form = TransformedFoodForms(data=self.request.POST)
- if not transformed_food_form.is_valid():
- return self.form_invalid(form)
-
- # Save the aliment and allergens associated
- transformed_food = form.save(commit=False)
- transformed_food.expiry_date = transformed_food.creation_date
- transformed_food.is_active = True
- transformed_food.is_ready = False
- transformed_food.was_eaten = False
- transformed_food._force_save = True
- transformed_food.save()
- transformed_food.refresh_from_db()
- ans = super().form_valid(form)
- transformed_food.update()
- return ans
-
- def get_success_url(self, **kwargs):
- self.object.refresh_from_db()
- return reverse('food:food_view', kwargs={"pk": self.object.pk})
+ extra_context = {"title": _("Add a meal")}
+ template_name = "food/food_update.html"
def get_sample_object(self):
- # We choose a club which may work or BDE else
- owner_id = 1
- for membership in self.request.user.memberships.all():
- club_id = membership.club.id
- food = TransformedFood(name="",
- creation_date=timezone.now(),
- expiry_date=timezone.now(),
- owner_id=club_id)
- if PermissionBackend.check_perm(self.request, "food.add_transformedfood", food):
- owner_id = club_id
- break
-
return TransformedFood(
name="",
- owner_id=owner_id,
- creation_date=timezone.now(),
+ owner_id=1,
expiry_date=timezone.now(),
+ is_ready=True,
)
- def get_context_data(self, **kwargs):
- context = super().get_context_data(**kwargs)
+ @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)
- # Some field are hidden on create
- form = context['form']
- form.fields['is_active'].widget = HiddenInput()
- form.fields['is_ready'].widget = HiddenInput()
- form.fields['was_eaten'].widget = HiddenInput()
- form.fields['shelf_life'].widget = HiddenInput()
+ 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))
-class TransformedFoodUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
+ 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 transformed product
+ A view to update Food
"""
- model = TransformedFood
- template_name = 'food/transformedfood_form.html'
- form_class = TransformedFoodForms
- extra_context = {'title': _('Update a meal')}
+ 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
- transformedfood_form = TransformedFoodForms(data=self.request.POST)
- if not transformedfood_form.is_valid():
- return self.form_invalid(form)
+ 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)
- form.instance.update()
+ 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('food:food_view', kwargs={"pk": self.object.pk})
+ 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 TransformedListView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin, ListView):
- """
- Displays ready TransformedFood
- """
- model = TransformedFood
- tables = [TransformedFoodTable, TransformedFoodTable, TransformedFoodTable]
- extra_context = {"title": _("Transformed food")}
-
- def get_queryset(self, **kwargs):
- return super().get_queryset(**kwargs).distinct()
-
- def get_tables(self):
- tables = super().get_tables()
-
- tables[0].prefix = "all-"
- tables[1].prefix = "open-"
- tables[2].prefix = "served-"
- return tables
-
- def get_tables_data(self):
- # first table = all transformed food, second table = free, third = served
- return [
- self.get_queryset().order_by("-creation_date"),
- TransformedFood.objects.filter(is_ready=True, is_active=True, was_eaten=False, expiry_date__lt=timezone.now())
- .filter(PermissionBackend.filter_queryset(self.request, TransformedFood, "view"))
- .distinct()
- .order_by("-creation_date"),
- TransformedFood.objects.filter(is_ready=True, is_active=True, was_eaten=False, expiry_date__gte=timezone.now())
- .filter(PermissionBackend.filter_queryset(self.request, TransformedFood, "view"))
- .distinct()
- .order_by("-creation_date")
- ]
+class BasicFoodDetailView(FoodDetailView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
-
- # We choose a club which should work
- for membership in self.request.user.memberships.all():
- club_id = membership.club.id
- food = TransformedFood(
- name="",
- owner_id=club_id,
- creation_date=timezone.now(),
- expiry_date=timezone.now(),
- )
- if PermissionBackend.check_perm(self.request, "food.add_transformedfood", food):
- context['can_create_meal'] = True
- break
-
- tables = context["tables"]
- for name, table in zip(["table", "open", "served"], tables):
- context[name] = table
+ 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)
diff --git a/apps/permission/fixtures/initial.json b/apps/permission/fixtures/initial.json
index dc2ca4c0..f1f01dcc 100644
--- a/apps/permission/fixtures/initial.json
+++ b/apps/permission/fixtures/initial.json
@@ -3307,452 +3307,184 @@
}
},
{
- "model": "permission.permission",
- "pk": 211,
- "fields": {
- "model": [
- "food",
- "transformedfood"
- ],
- "query": "{}",
- "type": "view",
- "mask": 3,
- "field": "",
- "permanent": false,
- "description": "Voir tout les plats"
- }
+ "model": "permission.permission",
+ "pk": 211,
+ "fields": {
+ "model": [
+ "food",
+ "qrcode"
+ ],
+ "query": "{}",
+ "type": "view",
+ "mask": 2,
+ "permanent": false,
+ "description": "Voir n'importe quel QR-code"
+ }
},
{
- "model": "permission.permission",
- "pk": 212,
- "fields": {
- "model": [
- "food",
- "transformedfood"
- ],
- "query": "{\"owner\": [\"club\"]}",
- "type": "view",
- "mask": 3,
- "field": "",
- "permanent": false,
- "description": "Voir tout les plats de son club"
- }
+ "model": "permission.permission",
+ "pk": 212,
+ "fields": {
+ "model": [
+ "food",
+ "allergen"
+ ],
+ "query": "{}",
+ "type": "view",
+ "mask": 1,
+ "permanent": false,
+ "description": "Voir n'importe quel allergène"
+ }
},
{
- "model": "permission.permission",
- "pk": 213,
- "fields": {
- "model": [
- "food",
- "transformedfood"
- ],
- "query": "{\"is_ready\": true, \"is_active\": true, \"was_eaten\": false}",
- "type": "view",
- "mask": 1,
- "field": "",
- "permanent": false,
- "description": "Voir les plats préparés actifs servis"
- }
+ "model": "permission.permission",
+ "pk": 213,
+ "fields": {
+ "model": [
+ "food",
+ "food"
+ ],
+ "query": "{}",
+ "type": "view",
+ "mask": 2,
+ "permanent": false,
+ "description": "Voir n'importe quelle bouffe"
+ }
},
{
- "model": "permission.permission",
- "pk": 214,
- "fields": {
- "model": [
- "food",
- "qrcode"
- ],
- "query": "{}",
- "type": "add",
- "mask": 3,
- "field": "",
- "permanent": false,
- "description": "Initialiser un QR code de traçabilité"
- }
+ "model": "permission.permission",
+ "pk": 214,
+ "fields": {
+ "model": [
+ "food",
+ "qrcode"
+ ],
+ "query": "{}",
+ "type": "add",
+ "mask": 2,
+ "permanent": false,
+ "description": "Ajouter n'importe quel QR-code"
+ }
},
{
- "model": "permission.permission",
- "pk": 215,
- "fields": {
- "model": [
- "food",
- "basicfood"
- ],
- "query": "{\"owner\": [\"club\"]}",
- "type": "add",
- "mask": 3,
- "field": "",
- "permanent": false,
- "description": "Créer un nouvel ingrédient pour son club"
- }
+ "model": "permission.permission",
+ "pk": 215,
+ "fields": {
+ "model": [
+ "food",
+ "food"
+ ],
+ "query": "{}",
+ "type": "add",
+ "mask": 2,
+ "permanent": false,
+ "description": "Ajouter n'importe quelle bouffe"
+ }
},
{
- "model": "permission.permission",
- "pk": 216,
- "fields": {
- "model": [
- "food",
- "basicfood"
- ],
- "query": "{}",
- "type": "add",
- "mask": 3,
- "field": "",
- "permanent": false,
- "description": "Créer un nouvel ingrédient"
- }
+ "model": "permission.permission",
+ "pk": 216,
+ "fields": {
+ "model": [
+ "food",
+ "food"
+ ],
+ "query": "{}",
+ "type": "change",
+ "mask": 2,
+ "permanent": false,
+ "description": "Modifier n'importe quelle bouffe"
+ }
},
{
- "model": "permission.permission",
- "pk": 217,
- "fields": {
- "model": [
- "food",
- "basicfood"
- ],
- "query": "{}",
- "type": "view",
- "mask": 3,
- "field": "",
- "permanent": false,
- "description": "Voir toute la bouffe"
- }
+ "model": "permission.permission",
+ "pk": 217,
+ "fields": {
+ "model": [
+ "food",
+ "qrcode"
+ ],
+ "query": "{\"food_container__owner\": [\"club\"]}",
+ "type": "view",
+ "mask": 2,
+ "permanent": false,
+ "description": "Voir un QR-code lié à son club"
+ }
},
{
- "model": "permission.permission",
- "pk": 218,
- "fields": {
- "model": [
- "food",
- "basicfood"
- ],
- "query": "{\"is_active\": true}",
- "type": "view",
- "mask": 3,
- "field": "",
- "permanent": false,
- "description": "Voir toute la bouffe active"
- }
+ "model": "permission.permission",
+ "pk": 218,
+ "fields": {
+ "model": [
+ "food",
+ "food"
+ ],
+ "query": "{\"owner\": [\"club\"]}",
+ "type": "view",
+ "mask": 2,
+ "permanent": false,
+ "description": "Voir la bouffe de son club"
+ }
},
{
- "model": "permission.permission",
- "pk": 219,
- "fields": {
- "model": [
- "food",
- "basicfood"
- ],
- "query": "{\"is_active\": true, \"owner\": [\"club\"]}",
- "type": "view",
- "mask": 3,
- "field": "",
- "permanent": false,
- "description": "Voir la bouffe active de son club"
- }
+ "model": "permission.permission",
+ "pk": 219,
+ "fields": {
+ "model": [
+ "food",
+ "qrcode"
+ ],
+ "query": "{\"food_container__owner\": [\"club\"]}",
+ "type": "add",
+ "mask": 2,
+ "permanent": false,
+ "description": "Ajouter un QR-code pour son club"
+ }
},
{
- "model": "permission.permission",
- "pk": 220,
- "fields": {
- "model": [
- "food",
- "basicfood"
- ],
- "query": "{}",
- "type": "change",
- "mask": 3,
- "field": "",
- "permanent": false,
- "description": "Modifier de la bouffe"
- }
+ "model": "permission.permission",
+ "pk": 220,
+ "fields": {
+ "model": [
+ "food",
+ "food"
+ ],
+ "query": "{\"owner\": [\"club\"]}",
+ "type": "add",
+ "mask": 2,
+ "permanent": false,
+ "description": "Ajouter de la bouffe appartenant à son club"
+ }
},
{
- "model": "permission.permission",
- "pk": 221,
- "fields": {
- "model": [
- "food",
- "basicfood"
- ],
- "query": "{\"is_active\": true, \"was_eaten\": false}",
- "type": "change",
- "mask": 3,
- "field": "allergens",
- "permanent": false,
- "description": "Modifier les allergènes de la bouffe existante"
- }
+ "model": "permission.permission",
+ "pk": 221,
+ "fields": {
+ "model": [
+ "food",
+ "food"
+ ],
+ "query": "{\"owner\": [\"club\"]}",
+ "type": "change",
+ "mask": 2,
+ "permanent": false,
+ "description": "Modifier la bouffe appartenant à son club"
+ }
},
{
- "model": "permission.permission",
- "pk": 222,
- "fields": {
- "model": [
- "food",
- "basicfood"
- ],
- "query": "{\"is_active\": true, \"was_eaten\": false, \"owner\": [\"club\"]}",
- "type": "change",
- "mask": 3,
- "field": "allergens",
- "permanent": false,
- "description": "Modifier les allergènes de la bouffe appartenant à son club"
- }
- },
- {
- "model": "permission.permission",
- "pk": 223,
- "fields": {
- "model": [
- "food",
- "transformedfood"
- ],
- "query": "{}",
- "type": "add",
- "mask": 3,
- "field": "",
- "permanent": false,
- "description": "Créer un plat"
- }
- },
- {
- "model": "permission.permission",
- "pk": 224,
- "fields": {
- "model": [
- "food",
- "transformedfood"
- ],
- "query": "{\"owner\": [\"club\"]}",
- "type": "add",
- "mask": 3,
- "field": "",
- "permanent": false,
- "description": "Créer un plat pour son club"
- }
- },
- {
- "model": "permission.permission",
- "pk": 225,
- "fields": {
- "model": [
- "food",
- "transformedfood"
- ],
- "query": "{}",
- "type": "change",
- "mask": 3,
- "field": "",
- "permanent": false,
- "description": "Modifier tout les plats"
- }
- },
- {
- "model": "permission.permission",
- "pk": 226,
- "fields": {
- "model": [
- "food",
- "transformedfood"
- ],
- "query": "{\"is_active\": true}",
- "type": "change",
- "mask": 3,
- "field": "was_eaten",
- "permanent": false,
- "description": "Indiquer si un plat a été mangé"
- }
- },
- {
- "model": "permission.permission",
- "pk": 227,
- "fields": {
- "model": [
- "food",
- "transformedfood"
- ],
- "query": "{\"is_active\": true, \"owner\": [\"club\"]}",
- "type": "change",
- "mask": 3,
- "field": "is_ready",
- "permanent": false,
- "description": "Indiquer si un plat de son club est prêt"
- }
- },
- {
- "model": "permission.permission",
- "pk": 228,
- "fields": {
- "model": [
- "food",
- "transformedfood"
- ],
- "query": "{\"is_active\": true}",
- "type": "change",
- "mask": 3,
- "field": "is_active",
- "permanent": false,
- "description": "Archiver un plat"
- }
- },
- {
- "model": "permission.permission",
- "pk": 229,
- "fields": {
- "model": [
- "food",
- "basicfood"
- ],
- "query": "{\"is_active\": true}",
- "type": "change",
- "mask": 3,
- "field": "is_active",
- "permanent": false,
- "description": "Archiver de la bouffe"
- }
- },
- {
- "model": "permission.permission",
- "pk": 230,
- "fields": {
- "model": [
- "food",
- "transformedfood"
- ],
- "query": "{\"is_active\": true}",
- "type": "view",
- "mask": 3,
- "field": "",
- "permanent": false,
- "description": "Voir tout les plats actifs"
- }
- },
- {
- "model": "permission.permission",
- "pk": 231,
- "fields": {
- "model": [
- "food",
- "qrcode"
- ],
- "query": "{}",
- "type": "view",
- "mask": 3,
- "field": "",
- "permanent": false,
- "description": "Voir tous les QR codes"
- }
- },
- {
- "model": "permission.permission",
- "pk": 232,
- "fields": {
- "model": [
- "food",
- "qrcode"
- ],
- "query": "{\"food_container__is_active\": true}",
- "type": "view",
- "mask": 3,
- "field": "",
- "permanent": false,
- "description": "Voir tous les QR codes actifs"
- }
- },
- {
- "model": "permission.permission",
- "pk": 233,
- "fields": {
- "model": [
- "food",
- "qrcode"
- ],
- "query": "{\"food_container__owner\": [\"club\"], \"food_container__is_active\": true}",
- "type": "view",
- "mask": 3,
- "field": "",
- "permanent": false,
- "description": "Voir tous les QR codes actifs de son club"
- }
- },
- {
- "model": "permission.permission",
- "pk" : 234,
- "fields": {
- "model": [
- "food",
- "transformedfood"
- ],
- "query": "{\"owner\": [\"club\"], \"is_active\": true}",
- "type": "change",
- "mask": 3,
- "field": "ingredients",
- "permanent": false,
- "description": "Changer les ingrédients d'un plat actif de son club"
- }
- },
- {
- "model": "permission.permission",
- "pk": 235,
- "fields": {
- "model": [
- "food",
- "food"
- ],
- "query": "{}",
- "type": "view",
- "mask": 3,
- "field": "",
- "permanent": false,
- "description": "Voir bouffe"
- }
- },
- {
- "model": "permission.permission",
- "pk": 236,
- "fields": {
- "model": [
- "food",
- "food"
- ],
- "query": "{\"is_active\": true}",
- "type": "view",
- "mask": 3,
- "field": "",
- "permanent": false,
- "description": "Voir bouffe active"
- }
- },
- {
- "model": "permission.permission",
- "pk": 237,
- "fields": {
- "model": [
- "food",
- "food"
- ],
- "query": "{\"is_active\": true, \"owner\": [\"club\"]}",
- "type": "view",
- "mask": 3,
- "field": "",
- "permanent": false,
- "description": "Voir bouffe active de son club"
- }
- },
- {
- "model": "permission.permission",
- "pk": 238,
- "fields": {
- "model": [
- "food",
- "food"
- ],
- "query": "{}",
- "type": "change",
- "mask": 3,
- "field": "",
- "permanent": false,
- "description": "Modifier bouffe"
- }
+ "model": "permission.permission",
+ "pk": 222,
+ "fields": {
+ "model": [
+ "food",
+ "food"
+ ],
+ "query": "{\"end_of_life\": \"\"}",
+ "type": "view",
+ "mask": 1,
+ "permanent": false,
+ "description": "Voir la bouffe servie"
+ }
},
{
"model": "permission.permission",
@@ -4359,7 +4091,8 @@
158,
159,
160,
- 213
+ 212,
+ 222
]
}
},
@@ -4400,16 +4133,11 @@
50,
141,
169,
- 212,
- 214,
- 215,
- 219,
- 222,
- 224,
- 227,
- 233,
- 234,
- 237,
+ 217,
+ 218,
+ 219,
+ 220,
+ 221,
247,
258,
259
@@ -4590,21 +4318,7 @@
166,
167,
168,
- 182,
- 212,
- 214,
- 215,
- 218,
- 221,
- 224,
- 226,
- 227,
- 228,
- 229,
- 230,
- 232,
- 234,
- 236
+ 182
]
}
},
@@ -4812,8 +4526,7 @@
168,
176,
177,
- 197,
- 211
+ 197
]
}
},
@@ -4841,15 +4554,11 @@
"permissions": [
137,
211,
- 214,
- 216,
- 217,
- 220,
- 223,
- 225,
- 231,
- 235,
- 238
+ 212,
+ 213,
+ 214,
+ 215,
+ 216
]
}
},
diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po
index e33ee287..14dc6a1e 100644
--- a/locale/fr/LC_MESSAGES/django.po
+++ b/locale/fr/LC_MESSAGES/django.po
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2025-03-25 11:16+0100\n"
+"POT-Creation-Date: 2025-04-24 18:22+0200\n"
"PO-Revision-Date: 2022-04-11 22:05+0200\n"
"Last-Translator: bleizi \n"
"Language-Team: French \n"
@@ -25,7 +25,7 @@ msgid "This opener already exists"
msgstr "Cette amitié existe déjà"
#: apps/activity/apps.py:10 apps/activity/models.py:129
-#: apps/activity/models.py:169 apps/activity/models.py:328
+#: apps/activity/models.py:169 apps/activity/models.py:329
msgid "activity"
msgstr "activité"
@@ -37,29 +37,29 @@ msgstr "La note du club est inactive."
msgid "The end date must be after the start date."
msgstr "La date de fin doit être après celle de début."
-#: apps/activity/forms.py:83 apps/activity/models.py:276
+#: apps/activity/forms.py:83 apps/activity/models.py:277
msgid "You can't invite someone once the activity is started."
msgstr ""
"Vous ne pouvez pas inviter quelqu'un une fois que l'activité a démarré."
-#: apps/activity/forms.py:86 apps/activity/models.py:279
+#: apps/activity/forms.py:86 apps/activity/models.py:280
msgid "This activity is not validated yet."
msgstr "Cette activité n'est pas encore validée."
-#: apps/activity/forms.py:96 apps/activity/models.py:287
+#: apps/activity/forms.py:96 apps/activity/models.py:288
msgid "This person has been already invited 5 times this year."
msgstr "Cette personne a déjà été invitée 5 fois cette année."
-#: apps/activity/forms.py:100 apps/activity/models.py:291
+#: apps/activity/forms.py:100 apps/activity/models.py:292
msgid "This person is already invited."
msgstr "Cette personne est déjà invitée."
-#: apps/activity/forms.py:104 apps/activity/models.py:295
+#: apps/activity/forms.py:104 apps/activity/models.py:296
msgid "You can't invite more than 3 people to this activity."
msgstr "Vous ne pouvez pas inviter plus de 3 personnes à cette activité."
-#: apps/activity/models.py:28 apps/activity/models.py:63 apps/food/models.py:42
-#: apps/food/models.py:56 apps/member/models.py:203
+#: apps/activity/models.py:28 apps/activity/models.py:63 apps/food/models.py:18
+#: apps/food/models.py:35 apps/member/models.py:203
#: apps/member/templates/member/includes/club_info.html:4
#: apps/member/templates/member/includes/profile_info.html:4
#: apps/note/models/notes.py:263 apps/note/models/transactions.py:26
@@ -121,7 +121,7 @@ msgid "type"
msgstr "type"
#: apps/activity/models.py:91 apps/logs/models.py:22 apps/member/models.py:325
-#: apps/note/models/notes.py:148 apps/treasury/models.py:293
+#: apps/note/models/notes.py:148 apps/treasury/models.py:294
#: apps/wei/models.py:171 apps/wei/templates/wei/attribute_bus_1A.html:13
#: apps/wei/templates/wei/survey.html:15
msgid "user"
@@ -205,21 +205,21 @@ msgstr "Entrée de la note {note} pour l'activité « {activity} »"
msgid "Already entered on "
msgstr "Déjà rentré·e le "
-#: apps/activity/models.py:204 apps/activity/tables.py:58
+#: apps/activity/models.py:205 apps/activity/tables.py:58
msgid "{:%Y-%m-%d %H:%M:%S}"
msgstr "{:%d/%m/%Y %H:%M:%S}"
-#: apps/activity/models.py:212
+#: apps/activity/models.py:213
msgid "The balance is negative."
msgstr "La note est en négatif."
-#: apps/activity/models.py:242
+#: apps/activity/models.py:243
#: apps/treasury/templates/treasury/sogecredit_detail.html:14
#: apps/wei/templates/wei/attribute_bus_1A.html:16
msgid "last name"
msgstr "nom de famille"
-#: apps/activity/models.py:247
+#: apps/activity/models.py:248
#: apps/member/templates/member/includes/profile_info.html:4
#: apps/registration/templates/registration/future_profile_detail.html:16
#: apps/treasury/templates/treasury/sogecredit_detail.html:17
@@ -228,36 +228,36 @@ msgstr "nom de famille"
msgid "first name"
msgstr "prénom"
-#: apps/activity/models.py:252
+#: apps/activity/models.py:253
msgid "school"
msgstr "école"
-#: apps/activity/models.py:259
+#: apps/activity/models.py:260
msgid "inviter"
msgstr "hôte"
-#: apps/activity/models.py:263
+#: apps/activity/models.py:264
msgid "guest"
msgstr "invité·e"
-#: apps/activity/models.py:264
+#: apps/activity/models.py:265
msgid "guests"
msgstr "invité·e·s"
-#: apps/activity/models.py:317
+#: apps/activity/models.py:318
msgid "Invitation"
msgstr "Invitation"
-#: apps/activity/models.py:335 apps/activity/models.py:339
+#: apps/activity/models.py:336 apps/activity/models.py:340
msgid "Opener"
msgstr "Ouvreur⋅se"
-#: apps/activity/models.py:340
+#: apps/activity/models.py:341
#: apps/activity/templates/activity/activity_detail.html:16
msgid "Openers"
msgstr "Ouvreur⋅ses"
-#: apps/activity/models.py:344
+#: apps/activity/models.py:345
#, fuzzy, python-brace-format
#| msgid "Entry for {note} to the activity {activity}"
msgid "{opener} is opener of activity {acivity}"
@@ -285,7 +285,7 @@ msgstr "Entré·e le "
msgid "remove"
msgstr "supprimer"
-#: apps/activity/tables.py:84 apps/note/forms.py:69 apps/treasury/models.py:208
+#: apps/activity/tables.py:84 apps/note/forms.py:69 apps/treasury/models.py:209
msgid "Type"
msgstr "Type"
@@ -382,10 +382,8 @@ msgid "Entry done!"
msgstr "Entrée effectuée !"
#: apps/activity/templates/activity/activity_form.html:16
-#: apps/food/templates/food/add_ingredient_form.html:16
-#: apps/food/templates/food/basicfood_form.html:16
-#: apps/food/templates/food/create_qrcode_form.html:20
-#: apps/food/templates/food/transformedfood_form.html:16
+#: apps/food/templates/food/food_update.html:17
+#: apps/food/templates/food/qrcode.html:18
#: apps/member/templates/member/add_members.html:46
#: apps/member/templates/member/club_form.html:16
#: apps/note/templates/note/transactiontemplate_form.html:18
@@ -493,257 +491,277 @@ msgstr "Entrées pour l'activité « {} »"
msgid "API"
msgstr "API"
-#: apps/food/apps.py:11 apps/food/models.py:105
+#: apps/food/apps.py:11
msgid "food"
msgstr "bouffe"
-#: apps/food/forms.py:32
-msgid "Fully used"
-msgstr "Entièrement utilisé"
-
#: apps/food/forms.py:50
msgid "Pasta METRO 5kg"
msgstr "Pâtes METRO 5kg"
-#: apps/food/forms.py:100
+#: apps/food/forms.py:54 apps/food/forms.py:82
+msgid "Specific order given to GCKs"
+msgstr ""
+
+#: apps/food/forms.py:78
msgid "Lasagna"
msgstr "Lasagnes"
-#: apps/food/models.py:18
-msgid "QR-code number"
-msgstr "numéro de QR-code"
+#: apps/food/forms.py:117
+msgid "Shelf life (in hours)"
+msgstr "Durée de vie (en heure)"
-#: apps/food/models.py:26
-msgid "food container"
-msgstr "récipient"
+#: apps/food/forms.py:139
+msgid "Fully used"
+msgstr "Entièrement utilisé"
-#: apps/food/models.py:30
-msgid "QR-code"
-msgstr "QR-code"
-
-#: apps/food/models.py:31
-msgid "QR-codes"
-msgstr "QR-codes"
-
-#: apps/food/models.py:34
-#, python-brace-format
-msgid "QR-code number {qr_code_number}"
-msgstr "numéro du QR-code {qr_code_number}"
-
-#: apps/food/models.py:47
+#: apps/food/models.py:23
msgid "Allergen"
msgstr "Allergène"
-#: apps/food/models.py:48 apps/food/templates/food/basicfood_detail.html:17
-#: apps/food/templates/food/transformedfood_detail.html:20
+#: apps/food/models.py:24
msgid "Allergens"
msgstr "Allergènes"
-#: apps/food/models.py:64
+#: apps/food/models.py:43
msgid "owner"
msgstr "propriétaire"
-#: apps/food/models.py:70
-msgid "allergen"
-msgstr "allergène"
+#: apps/food/models.py:49
+msgid "allergens"
+msgstr "allergènes"
-#: apps/food/models.py:74
+#: apps/food/models.py:53
msgid "expiry date"
msgstr "date de péremption"
-#: apps/food/models.py:80
-msgid "was eaten"
-msgstr "a été mangé"
+#: apps/food/models.py:59
+msgid "end of life"
+msgstr "fin de vie"
-#: apps/food/models.py:89
+#: apps/food/models.py:64
msgid "is ready"
msgstr "est prêt"
-#: apps/food/models.py:94
-msgid "is active"
-msgstr "est en cours"
+#: apps/food/models.py:70
+msgid "order"
+msgstr "consigne"
-#: apps/food/models.py:106
-msgid "foods"
-msgstr "bouffes"
+#: apps/food/models.py:107 apps/food/views.py:32
+#: note_kfet/templates/base.html:72
+msgid "Food"
+msgstr "Bouffe"
-#: apps/food/models.py:122
+#: apps/food/models.py:108
+msgid "Foods"
+msgstr "Bouffes"
+
+#: apps/food/models.py:117
msgid "arrival date"
msgstr "date d'arrivée"
-#: apps/food/models.py:152
+#: apps/food/models.py:169
msgid "Basic food"
msgstr "Aliment basique"
-#: apps/food/models.py:153
+#: apps/food/models.py:170
msgid "Basic foods"
msgstr "Aliments basiques"
-#: apps/food/models.py:161
+#: apps/food/models.py:182
msgid "creation date"
msgstr "date de création"
-#: apps/food/models.py:169
-msgid "transformed ingredient"
-msgstr "ingrédients tranformées"
-
-#: apps/food/models.py:174
+#: apps/food/models.py:188
msgid "shelf life"
msgstr "durée de vie"
-#: apps/food/models.py:225 apps/food/views.py:375
+#: apps/food/models.py:196
+msgid "transformed ingredient"
+msgstr "ingrédients tranformées"
+
+#: apps/food/models.py:258
msgid "Transformed food"
msgstr "Aliment transformé"
-#: apps/food/models.py:226
+#: apps/food/models.py:259
msgid "Transformed foods"
msgstr "Aliments transformés"
-#: apps/food/templates/food/basicfood_detail.html:14
-#: apps/food/templates/food/create_qrcode_form.html:31
-#: apps/food/templates/food/qrcode_detail.html:15
-#: apps/food/templates/food/transformedfood_detail.html:14
-msgid "Owner"
-msgstr "Propriétaire"
+#: apps/food/models.py:271
+msgid "qr code number"
+msgstr "numéro de QR-code"
-#: apps/food/templates/food/basicfood_detail.html:15
-#: apps/food/templates/food/create_qrcode_form.html:34
-msgid "Arrival date"
-msgstr "Date d'arrivée"
+#: apps/food/models.py:278
+msgid "food container"
+msgstr "récipient"
-#: apps/food/templates/food/basicfood_detail.html:16
-#: apps/food/templates/food/create_qrcode_form.html:37
-#: apps/food/templates/food/qrcode_detail.html:16
-#: apps/food/templates/food/transformedfood_detail.html:19
-msgid "Expiry date"
-msgstr "Date de péremption"
+#: apps/food/models.py:282
+msgid "QR-code"
+msgstr "QR-code"
-#: apps/food/templates/food/basicfood_detail.html:24
-#: apps/food/templates/food/transformedfood_detail.html:36
-msgid "Active"
-msgstr "Actif"
+#: apps/food/models.py:283
+msgid "QR-codes"
+msgstr "QR-codes"
-#: apps/food/templates/food/basicfood_detail.html:25
-#: apps/food/templates/food/transformedfood_detail.html:37
-msgid "Eaten"
-msgstr "Mangé"
+#: apps/food/models.py:286
+msgid "QR-code number"
+msgstr "numéro de QR-code"
-#: apps/food/templates/food/basicfood_detail.html:28
-#: apps/food/templates/food/qrcode_detail.html:20
-#: apps/food/templates/food/qrcode_detail.html:24
-#: apps/food/templates/food/transformedfood_detail.html:41
+#: apps/food/templates/food/food_detail.html:19
+msgid "Contained in"
+msgstr "Contenu dans"
+
+#: apps/food/templates/food/food_detail.html:26
+msgid "Contain"
+msgstr "Contient"
+
+#: apps/food/templates/food/food_detail.html:35
msgid "Update"
msgstr "Modifier"
-#: apps/food/templates/food/basicfood_detail.html:32
-#: apps/food/templates/food/qrcode_detail.html:34
-#: apps/food/templates/food/transformedfood_detail.html:46
+#: apps/food/templates/food/food_detail.html:40
msgid "Add to a meal"
msgstr "Ajouter à un plat"
-#: apps/food/templates/food/create_qrcode_form.html:15
-msgid "New basic food"
-msgstr "Nouvel aliment basique"
+#: apps/food/templates/food/food_detail.html:44
+msgid "Return to the food list"
+msgstr "Retour à la liste de nourriture"
-#: apps/food/templates/food/create_qrcode_form.html:23
-msgid "Copy constructor"
-msgstr "Constructeur de copie"
-
-#: apps/food/templates/food/create_qrcode_form.html:28
-#: apps/food/templates/food/qrcode_detail.html:14
-#: apps/note/templates/note/transaction_form.html:132
-#: apps/treasury/models.py:60
-msgid "Name"
-msgstr "Nom"
-
-#: apps/food/templates/food/qrcode_detail.html:10
-msgid "number"
-msgstr "numéro"
-
-#: apps/food/templates/food/qrcode_detail.html:29
-msgid "View details"
-msgstr "Voir plus"
-
-#: apps/food/templates/food/transformedfood_detail.html:16
-#: apps/food/templates/food/transformedfood_detail.html:35
-msgid "Ready"
-msgstr "Prêt"
-
-#: apps/food/templates/food/transformedfood_detail.html:18
-msgid "Creation date"
-msgstr "Date de création"
-
-#: apps/food/templates/food/transformedfood_detail.html:27
-msgid "Ingredients"
-msgstr "Ingrédients"
-
-#: apps/food/templates/food/transformedfood_detail.html:34
-msgid "Shelf life"
-msgstr "Durée de vie"
-
-#: apps/food/templates/food/transformedfood_list.html:11
+#: apps/food/templates/food/food_list.html:14
msgid "Meal served"
msgstr "Plat servis"
-#: apps/food/templates/food/transformedfood_list.html:16
+#: apps/food/templates/food/food_list.html:19
msgid "New meal"
msgstr "Nouveau plat"
-#: apps/food/templates/food/transformedfood_list.html:25
+#: apps/food/templates/food/food_list.html:28
msgid "There is no meal served."
msgstr "Il n'y a pas de plat servi."
-#: apps/food/templates/food/transformedfood_list.html:33
-msgid "Open"
+#: apps/food/templates/food/food_list.html:35
+msgid "Free food"
msgstr "Open"
-#: apps/food/templates/food/transformedfood_list.html:40
-msgid "There is no free meal."
-msgstr "Il n'y a pas de plat en open"
+#: apps/food/templates/food/food_list.html:42
+msgid "There is no free food."
+msgstr "Il n'y a pas de bouffe en open"
-#: apps/food/templates/food/transformedfood_list.html:48
-msgid "All meals"
-msgstr "Tout les plats"
+#: apps/food/templates/food/food_list.html:50
+msgid "Food of your clubs"
+msgstr "Bouffe de tes clubs"
-#: apps/food/templates/food/transformedfood_list.html:55
-msgid "There is no meal."
-msgstr "Il n'y a pas de plat"
+#: apps/food/templates/food/food_list.html:56
+msgid "Food of club"
+msgstr "Bouffe du club"
-#: apps/food/views.py:28
-msgid "Add the ingredient"
-msgstr "Ajouter un ingrédient"
+#: apps/food/templates/food/food_list.html:63
+msgid "Yours club has not food yet."
+msgstr "Ton club n'a pas de bouffe pour l'instant"
-#: apps/food/views.py:42
-msgid "The product is already prepared"
-msgstr "Le produit est déjà prêt"
+#: apps/food/templates/food/qrcode.html:22
+msgid "Copy constructor"
+msgstr "Constructeur de copie"
-#: apps/food/views.py:70
-msgid "Update an aliment"
-msgstr "Modifier un aliment"
+#: apps/food/templates/food/qrcode.html:23
+msgid "New food"
+msgstr "Nouvel aliment"
-#: apps/food/views.py:97
-msgid "Details of:"
-msgstr "Détails de:"
+#: apps/food/templates/food/qrcode.html:29
+#: apps/note/templates/note/transaction_form.html:132
+#: apps/treasury/models.py:61
+msgid "Name"
+msgstr "Nom"
-#: apps/food/views.py:121
-msgid "Add a new basic food with QRCode"
-msgstr "Ajouter un nouvel ingrédient avec un QR-code"
+#: apps/food/templates/food/qrcode.html:32
+msgid "Owner"
+msgstr "Propriétaire"
-#: apps/food/views.py:194
+#: apps/food/templates/food/qrcode.html:35
+msgid "Expiry date"
+msgstr "Date de péremption"
+
+#: apps/food/utils.py:6
+msgid "second"
+msgstr "seconde"
+
+#: apps/food/utils.py:6
+msgid "seconds"
+msgstr "secondes"
+
+#: apps/food/utils.py:7
+msgid "minute"
+msgstr "minute"
+
+#: apps/food/utils.py:7
+msgid "minutes"
+msgstr "minutes"
+
+#: apps/food/utils.py:8
+msgid "hour"
+msgstr "heure"
+
+#: apps/food/utils.py:8
+msgid "hours"
+msgstr "heures"
+
+#: apps/food/utils.py:9
+msgid "day"
+msgstr "jour"
+
+#: apps/food/utils.py:9 apps/member/templates/member/includes/club_info.html:27
+msgid "days"
+msgstr "jours"
+
+#: apps/food/utils.py:10
+msgid "week"
+msgstr "semaine"
+
+#: apps/food/utils.py:10
+msgid "weeks"
+msgstr "semaines"
+
+#: apps/food/utils.py:53
+msgid "and"
+msgstr "et"
+
+#: apps/food/views.py:116
msgid "Add a new QRCode"
msgstr "Ajouter un nouveau QR-code"
-#: apps/food/views.py:245
-msgid "QRCode"
-msgstr "QR-code"
+#: apps/food/views.py:165
+msgid "Add an aliment"
+msgstr "Ajouter un nouvel aliment"
-#: apps/food/views.py:281
-msgid "Add a new meal"
-msgstr "Ajouter un nouveau plat"
+#: apps/food/views.py:224
+msgid "Add a meal"
+msgstr "Ajouter un plat"
-#: apps/food/views.py:347
-msgid "Update a meal"
-msgstr "Modifier le plat"
+#: apps/food/views.py:251
+msgid "Add the ingredient:"
+msgstr "Ajouter l'ingrédient"
+
+#: apps/food/views.py:275
+#, python-brace-format
+msgid "Food fully used in : {meal.name}"
+msgstr "Aliment entièrement utilisé dans : {meal.name}"
+
+#: apps/food/views.py:294
+msgid "Update an aliment"
+msgstr "Modifier un aliment"
+
+#: apps/food/views.py:342
+msgid "Details of:"
+msgstr "Détails de :"
+
+#: apps/food/views.py:352 apps/treasury/tables.py:149
+msgid "Yes"
+msgstr "Oui"
+
+#: apps/food/views.py:354 apps/member/models.py:99 apps/treasury/tables.py:149
+msgid "No"
+msgstr "Non"
#: apps/logs/apps.py:11
msgid "Logs"
@@ -1044,10 +1062,6 @@ msgstr "payé⋅e"
msgid "Tells if the user receive a salary."
msgstr "Indique si l'utilisateur⋅rice perçoit un salaire."
-#: apps/member/models.py:99 apps/treasury/tables.py:149
-msgid "No"
-msgstr "Non"
-
#: apps/member/models.py:100
msgid "Yes (receive them in french)"
msgstr "Oui (les recevoir en français)"
@@ -1357,10 +1371,6 @@ msgstr "Il n'y a pas d'adhésion trouvée avec cette entrée."
msgid "Club Parent"
msgstr "Club parent"
-#: apps/member/templates/member/includes/club_info.html:27
-msgid "days"
-msgstr "jours"
-
#: apps/member/templates/member/includes/club_info.html:31
#: apps/wei/templates/wei/base.html:40
msgid "membership fee"
@@ -1456,11 +1466,11 @@ msgstr "Introspection :"
msgid "Show my applications"
msgstr "Voir mes applications"
-#: apps/member/templates/member/picture_update.html:38
+#: apps/member/templates/member/picture_update.html:40
msgid "Nevermind"
msgstr "Annuler"
-#: apps/member/templates/member/picture_update.html:39
+#: apps/member/templates/member/picture_update.html:41
msgid "Crop and upload"
msgstr "Recadrer et envoyer"
@@ -1899,7 +1909,7 @@ msgstr "Ce champ est requis."
msgid "membership transaction"
msgstr "transaction d'adhésion"
-#: apps/note/models/transactions.py:381 apps/treasury/models.py:300
+#: apps/note/models/transactions.py:381 apps/treasury/models.py:301
msgid "membership transactions"
msgstr "transactions d'adhésion"
@@ -2512,7 +2522,7 @@ msgstr "Invalider l'inscription"
msgid "Treasury"
msgstr "Trésorerie"
-#: apps/treasury/forms.py:26 apps/treasury/models.py:112
+#: apps/treasury/forms.py:26 apps/treasury/models.py:113
#: apps/treasury/templates/treasury/invoice_form.html:22
msgid "This invoice is locked and can no longer be edited."
msgstr "Cette facture est verrouillée et ne peut plus être éditée."
@@ -2525,7 +2535,7 @@ msgstr "La remise est déjà fermée."
msgid "You can't change the type of the remittance."
msgstr "Vous ne pouvez pas changer le type de la remise."
-#: apps/treasury/forms.py:125 apps/treasury/models.py:275
+#: apps/treasury/forms.py:125 apps/treasury/models.py:276
#: apps/treasury/tables.py:99 apps/treasury/tables.py:108
#: apps/treasury/templates/treasury/invoice_list.html:16
#: apps/treasury/templates/treasury/remittance_list.html:16
@@ -2541,139 +2551,139 @@ msgstr "Pas de remise associée"
msgid "Invoice identifier"
msgstr "Numéro de facture"
-#: apps/treasury/models.py:42 apps/wrapped/models.py:28
+#: apps/treasury/models.py:43 apps/wrapped/models.py:28
#: apps/wrapped/models.py:29
msgid "BDE"
msgstr "BDE"
-#: apps/treasury/models.py:46
+#: apps/treasury/models.py:47
msgid "Quotation"
msgstr "Devis"
-#: apps/treasury/models.py:51
+#: apps/treasury/models.py:52
msgid "Object"
msgstr "Objet"
-#: apps/treasury/models.py:55
+#: apps/treasury/models.py:56
msgid "Description"
msgstr "Description"
-#: apps/treasury/models.py:64
+#: apps/treasury/models.py:65
msgid "Address"
msgstr "Adresse"
-#: apps/treasury/models.py:69 apps/treasury/models.py:202
+#: apps/treasury/models.py:70 apps/treasury/models.py:203
msgid "Date"
msgstr "Date"
-#: apps/treasury/models.py:75
+#: apps/treasury/models.py:76
msgid "Payment date"
msgstr "Date de paiement"
-#: apps/treasury/models.py:79
+#: apps/treasury/models.py:80
msgid "Acquitted"
msgstr "Acquittée"
-#: apps/treasury/models.py:84
+#: apps/treasury/models.py:85
msgid "Locked"
msgstr "Verrouillée"
-#: apps/treasury/models.py:85
+#: apps/treasury/models.py:86
msgid "An invoice can't be edited when it is locked."
msgstr "Une facture ne peut plus être modifiée si elle est verrouillée."
-#: apps/treasury/models.py:91
+#: apps/treasury/models.py:92
msgid "tex source"
msgstr "fichier TeX source"
-#: apps/treasury/models.py:95 apps/treasury/models.py:140
+#: apps/treasury/models.py:96 apps/treasury/models.py:141
msgid "invoice"
msgstr "facture"
-#: apps/treasury/models.py:96
+#: apps/treasury/models.py:97
msgid "invoices"
msgstr "factures"
-#: apps/treasury/models.py:99
+#: apps/treasury/models.py:100
#, python-brace-format
msgid "Invoice #{id}"
msgstr "Facture n°{id}"
-#: apps/treasury/models.py:145
+#: apps/treasury/models.py:146
msgid "Designation"
msgstr "Désignation"
-#: apps/treasury/models.py:151
+#: apps/treasury/models.py:152
msgid "Quantity"
msgstr "Quantité"
-#: apps/treasury/models.py:156
+#: apps/treasury/models.py:157
msgid "Unit price"
msgstr "Prix unitaire"
-#: apps/treasury/models.py:160
+#: apps/treasury/models.py:161
msgid "product"
msgstr "produit"
-#: apps/treasury/models.py:161
+#: apps/treasury/models.py:162
msgid "products"
msgstr "produits"
-#: apps/treasury/models.py:189
+#: apps/treasury/models.py:190
msgid "remittance type"
msgstr "type de remise"
-#: apps/treasury/models.py:190
+#: apps/treasury/models.py:191
msgid "remittance types"
msgstr "types de remises"
-#: apps/treasury/models.py:213
+#: apps/treasury/models.py:214
msgid "Comment"
msgstr "Commentaire"
-#: apps/treasury/models.py:218
+#: apps/treasury/models.py:219
msgid "Closed"
msgstr "Fermée"
-#: apps/treasury/models.py:222
+#: apps/treasury/models.py:223
msgid "remittance"
msgstr "remise"
-#: apps/treasury/models.py:223
+#: apps/treasury/models.py:224
msgid "remittances"
msgstr "remises"
-#: apps/treasury/models.py:226
+#: apps/treasury/models.py:227
msgid "Remittance #{:d}: {}"
msgstr "Remise n°{:d} : {}"
-#: apps/treasury/models.py:279
+#: apps/treasury/models.py:280
msgid "special transaction proxy"
msgstr "proxy de transaction spéciale"
-#: apps/treasury/models.py:280
+#: apps/treasury/models.py:281
msgid "special transaction proxies"
msgstr "proxys de transactions spéciales"
-#: apps/treasury/models.py:306
+#: apps/treasury/models.py:307
msgid "credit transaction"
msgstr "transaction de crédit"
-#: apps/treasury/models.py:311
+#: apps/treasury/models.py:312
#: apps/treasury/templates/treasury/sogecredit_detail.html:10
msgid "Credit from the Société générale"
msgstr "Crédit de la Société générale"
-#: apps/treasury/models.py:312
+#: apps/treasury/models.py:313
msgid "Credits from the Société générale"
msgstr "Crédits de la Société générale"
-#: apps/treasury/models.py:315
+#: apps/treasury/models.py:316
#, python-brace-format
msgid "Soge credit for {user}"
msgstr "Crédit de la société générale pour l'utilisateur·rice {user}"
-#: apps/treasury/models.py:445
+#: apps/treasury/models.py:446
msgid ""
"This user doesn't have enough money to pay the memberships with its note. "
"Please ask her/him to credit the note before invalidating this credit."
@@ -2702,10 +2712,6 @@ msgstr "Nombre de transactions"
msgid "View"
msgstr "Voir"
-#: apps/treasury/tables.py:149
-msgid "Yes"
-msgstr "Oui"
-
#: apps/treasury/templates/treasury/invoice_confirm_delete.html:10
#: apps/treasury/views.py:174
msgid "Delete invoice"
@@ -3812,10 +3818,6 @@ msgstr ""
msgid "Reset"
msgstr "Réinitialiser"
-#: note_kfet/templates/base.html:72
-msgid "Food"
-msgstr "Bouffe"
-
#: note_kfet/templates/base.html:84
msgid "Users"
msgstr "Utilisateur·rices"
@@ -4125,6 +4127,67 @@ msgstr ""
"d'adhésion. Vous devez également valider votre adresse email en suivant le "
"lien que vous avez reçu."
+#, python-brace-format
+#~ msgid "QR-code number {qr_code_number}"
+#~ msgstr "numéro du QR-code {qr_code_number}"
+
+#~ msgid "was eaten"
+#~ msgstr "a été mangé"
+
+#~ msgid "is active"
+#~ msgstr "est en cours"
+
+#~ msgid "foods"
+#~ msgstr "bouffes"
+
+#~ msgid "Arrival date"
+#~ msgstr "Date d'arrivée"
+
+#~ msgid "Active"
+#~ msgstr "Actif"
+
+#~ msgid "Eaten"
+#~ msgstr "Mangé"
+
+#~ msgid "number"
+#~ msgstr "numéro"
+
+#~ msgid "View details"
+#~ msgstr "Voir plus"
+
+#~ msgid "Ready"
+#~ msgstr "Prêt"
+
+#~ msgid "Creation date"
+#~ msgstr "Date de création"
+
+#~ msgid "Ingredients"
+#~ msgstr "Ingrédients"
+
+#~ msgid "Open"
+#~ msgstr "Open"
+
+#~ msgid "All meals"
+#~ msgstr "Tout les plats"
+
+#~ msgid "There is no meal."
+#~ msgstr "Il n'y a pas de plat"
+
+#~ msgid "The product is already prepared"
+#~ msgstr "Le produit est déjà prêt"
+
+#~ msgid "Add a new basic food with QRCode"
+#~ msgstr "Ajouter un nouvel ingrédient avec un QR-code"
+
+#~ msgid "QRCode"
+#~ msgstr "QR-code"
+
+#~ msgid "Add a new meal"
+#~ msgstr "Ajouter un nouveau plat"
+
+#~ msgid "Update a meal"
+#~ msgstr "Modifier le plat"
+
#, fuzzy
#~| msgid "invalidate"
#~ msgid "Enter a valid color."
@@ -4180,11 +4243,6 @@ msgstr ""
#~ msgid "Enter a number."
#~ msgstr "numéro de téléphone"
-#, fuzzy
-#~| msgid "add"
-#~ msgid "and"
-#~ msgstr "ajouter"
-
#, fuzzy, python-format
#~| msgid "A template with this name already exist"
#~ msgid "%(model_name)s with this %(field_labels)s already exists."
@@ -4446,9 +4504,6 @@ msgstr ""
#~ msgid "Free"
#~ msgstr "Open"
-#~ msgid "Add a new aliment"
-#~ msgstr "Ajouter un nouvel aliment"
-
#, fuzzy
#~| msgid "Transformed food"
#~ msgid "New transformed food"