mirror of
				https://gitlab.crans.org/bde/nk20
				synced 2025-11-04 01:12:08 +01:00 
			
		
		
		
	Compare commits
	
		
			6 Commits
		
	
	
		
			main
			...
			note_sheet
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					6cffe94bae | ||
| 
						 | 
					78372807f8 | ||
| 
						 | 
					b9bf01f2e3 | ||
| 
						 | 
					624f94823c | ||
| 
						 | 
					30a598c0b7 | ||
| 
						 | 
					6bf21b103f | 
@@ -48,5 +48,15 @@
 | 
			
		||||
            "can_invite": true,
 | 
			
		||||
            "guest_entry_fee": 0
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        "model": "activity.activitytype",
 | 
			
		||||
        "pk": 8,
 | 
			
		||||
        "fields": {
 | 
			
		||||
            "name": "Perm bouffe",
 | 
			
		||||
            "manage_entries": false,
 | 
			
		||||
            "can_invite": false,
 | 
			
		||||
            "guest_entry_fee": 0
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
]
 | 
			
		||||
]
 | 
			
		||||
@@ -66,6 +66,10 @@ SPDX-License-Identifier: GPL-3.0-or-later
 | 
			
		||||
            <a class="btn btn-warning btn-sm my-1" href="{% url 'activity:activity_entry' pk=activity.pk %}"> {% trans "Entry page" %}</a>
 | 
			
		||||
        {% endif %}
 | 
			
		||||
 | 
			
		||||
        {% if activity.activity_type.name == "Perm bouffe" %}
 | 
			
		||||
            <a class="btn btn-warning btn-sm my-1" href="{% url 'food:dish_list' activity_pk=activity.pk %}"> {% trans "Dish page" %}</a>
 | 
			
		||||
        {% endif %}
 | 
			
		||||
 | 
			
		||||
        {% if request.path_info == activity_detail_url %}
 | 
			
		||||
            {% if activity.valid and ".change__open"|has_perm:activity %}
 | 
			
		||||
                <a class="btn btn-warning btn-sm my-1" id="open_activity"> {% if activity.open %}{% trans "close"|capfirst %}{% else %}{% trans "open"|capfirst %}{% endif %}</a>
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,7 @@
 | 
			
		||||
 | 
			
		||||
from rest_framework import serializers
 | 
			
		||||
 | 
			
		||||
from ..models import Allergen, Food, BasicFood, TransformedFood, QRCode
 | 
			
		||||
from ..models import Allergen, Food, BasicFood, TransformedFood, QRCode, Dish, Supplement, Order, FoodTransaction
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AllergenSerializer(serializers.ModelSerializer):
 | 
			
		||||
@@ -21,9 +21,13 @@ class FoodSerializer(serializers.ModelSerializer):
 | 
			
		||||
    REST API Serializer for Food.
 | 
			
		||||
    The djangorestframework plugin will analyse the model `Food` and parse all fields in the API.
 | 
			
		||||
    """
 | 
			
		||||
    # This fields is used for autocompleting food in ManageIngredientsView
 | 
			
		||||
    # TODO Find a better way to do it
 | 
			
		||||
    owner_name = serializers.CharField(source='owner.name', read_only=True)
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Food
 | 
			
		||||
        fields = '__all__'
 | 
			
		||||
        fields = ['name', 'owner', 'allergens', 'expiry_date', 'end_of_life', 'is_ready', 'order', 'owner_name']
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class BasicFoodSerializer(serializers.ModelSerializer):
 | 
			
		||||
@@ -54,3 +58,43 @@ class QRCodeSerializer(serializers.ModelSerializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = QRCode
 | 
			
		||||
        fields = '__all__'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DishSerializer(serializers.ModelSerializer):
 | 
			
		||||
    """
 | 
			
		||||
    REST API Serializer for Dish.
 | 
			
		||||
    The djangorestframework plugin will analyse the model `Dish` and parse all fields in the API.
 | 
			
		||||
    """
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Dish
 | 
			
		||||
        fields = '__all__'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SupplementSerializer(serializers.ModelSerializer):
 | 
			
		||||
    """
 | 
			
		||||
    REST API Serializer for Supplement.
 | 
			
		||||
    The djangorestframework plugin will analyse the model `Supplement` and parse all fields in the API.
 | 
			
		||||
    """
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Supplement
 | 
			
		||||
        fields = '__all__'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class OrderSerializer(serializers.ModelSerializer):
 | 
			
		||||
    """
 | 
			
		||||
    REST API Serializer for Order.
 | 
			
		||||
    The djangorestframework plugin will analyse the model `Order` and parse all fields in the API.
 | 
			
		||||
    """
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Order
 | 
			
		||||
        fields = '__all__'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class FoodTransactionSerializer(serializers.ModelSerializer):
 | 
			
		||||
    """
 | 
			
		||||
    REST API Serializer for FoodTransaction.
 | 
			
		||||
    The djangorestframework plugin will analyse the model `FoodTransaction` and parse all fields in the API.
 | 
			
		||||
    """
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = FoodTransaction
 | 
			
		||||
        fields = '__all__'
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,8 @@
 | 
			
		||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
 | 
			
		||||
# SPDX-License-Identifier: GPL-3.0-or-later
 | 
			
		||||
 | 
			
		||||
from .views import AllergenViewSet, FoodViewSet, BasicFoodViewSet, TransformedFoodViewSet, QRCodeViewSet
 | 
			
		||||
from .views import AllergenViewSet, FoodViewSet, BasicFoodViewSet, TransformedFoodViewSet, QRCodeViewSet, \
 | 
			
		||||
    DishViewSet, SupplementViewSet, OrderViewSet, FoodTransactionViewSet
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def register_food_urls(router, path):
 | 
			
		||||
@@ -13,3 +14,7 @@ def register_food_urls(router, path):
 | 
			
		||||
    router.register(path + '/basicfood', BasicFoodViewSet)
 | 
			
		||||
    router.register(path + '/transformedfood', TransformedFoodViewSet)
 | 
			
		||||
    router.register(path + '/qrcode', QRCodeViewSet)
 | 
			
		||||
    router.register(path + '/dish', DishViewSet)
 | 
			
		||||
    router.register(path + '/supplement', SupplementViewSet)
 | 
			
		||||
    router.register(path + '/order', OrderViewSet)
 | 
			
		||||
    router.register(path + '/foodtransaction', FoodTransactionViewSet)
 | 
			
		||||
 
 | 
			
		||||
@@ -5,8 +5,9 @@ from api.viewsets import ReadProtectedModelViewSet
 | 
			
		||||
from django_filters.rest_framework import DjangoFilterBackend
 | 
			
		||||
from rest_framework.filters import SearchFilter
 | 
			
		||||
 | 
			
		||||
from .serializers import AllergenSerializer, FoodSerializer, BasicFoodSerializer, TransformedFoodSerializer, QRCodeSerializer
 | 
			
		||||
from ..models import Allergen, Food, BasicFood, TransformedFood, QRCode
 | 
			
		||||
from .serializers import AllergenSerializer, FoodSerializer, BasicFoodSerializer, TransformedFoodSerializer, QRCodeSerializer, \
 | 
			
		||||
    DishSerializer, SupplementSerializer, OrderSerializer, FoodTransactionSerializer
 | 
			
		||||
from ..models import Allergen, Food, BasicFood, TransformedFood, QRCode, Dish, Supplement, Order, FoodTransaction
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AllergenViewSet(ReadProtectedModelViewSet):
 | 
			
		||||
@@ -72,3 +73,55 @@ class QRCodeViewSet(ReadProtectedModelViewSet):
 | 
			
		||||
    filter_backends = [DjangoFilterBackend, SearchFilter]
 | 
			
		||||
    filterset_fields = ['qr_code_number', ]
 | 
			
		||||
    search_fields = ['$qr_code_number', ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DishViewSet(ReadProtectedModelViewSet):
 | 
			
		||||
    """
 | 
			
		||||
    REST API View set.
 | 
			
		||||
    The djangorestframework plugin will get all `Dish` objects, serialize it to JSON with the given serializer,
 | 
			
		||||
    then render it on /api/food/dish/
 | 
			
		||||
    """
 | 
			
		||||
    queryset = Dish.objects.order_by('id')
 | 
			
		||||
    serializer_class = DishSerializer
 | 
			
		||||
    filter_backends = [DjangoFilterBackend, SearchFilter]
 | 
			
		||||
    filterset_fields = ['main__name', 'activity', ]
 | 
			
		||||
    search_fields = ['$main__name', '$activity', ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SupplementViewSet(ReadProtectedModelViewSet):
 | 
			
		||||
    """
 | 
			
		||||
    REST API View set.
 | 
			
		||||
    The djangorestframework plugin will get all `Supplement` objects, serialize it to JSON with the given serializer,
 | 
			
		||||
    then render it on /api/food/supplement/
 | 
			
		||||
    """
 | 
			
		||||
    queryset = Supplement.objects.order_by('id')
 | 
			
		||||
    serializer_class = SupplementSerializer
 | 
			
		||||
    filter_backends = [DjangoFilterBackend, SearchFilter]
 | 
			
		||||
    filterset_fields = ['food__name', 'dish__activity', ]
 | 
			
		||||
    search_fields = ['$food__name', '$dish__activity', ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class OrderViewSet(ReadProtectedModelViewSet):
 | 
			
		||||
    """
 | 
			
		||||
    REST API View set.
 | 
			
		||||
    The djangorestframework plugin will get all `Order` objects, serialize it to JSON with the given serializer,
 | 
			
		||||
    then render it on /api/food/order/
 | 
			
		||||
    """
 | 
			
		||||
    queryset = Order.objects.order_by('id')
 | 
			
		||||
    serializer_class = OrderSerializer
 | 
			
		||||
    filter_backends = [DjangoFilterBackend, SearchFilter]
 | 
			
		||||
    filterset_fields = ['user', 'activity', 'dish', 'supplements', 'number', ]
 | 
			
		||||
    search_fields = ['$user', '$activity', '$dish', '$supplements', '$number', ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class FoodTransactionViewSet(ReadProtectedModelViewSet):
 | 
			
		||||
    """
 | 
			
		||||
    REST API View set.
 | 
			
		||||
    The djangorestframework plugin will get all `FoodTransaction` objects, serialize it to JSON with the given serializer,
 | 
			
		||||
    then render it on /api/food/foodtransaction/
 | 
			
		||||
    """
 | 
			
		||||
    queryset = FoodTransaction.objects.order_by('id')
 | 
			
		||||
    serializer_class = FoodTransactionSerializer
 | 
			
		||||
    filter_backends = [DjangoFilterBackend, SearchFilter]
 | 
			
		||||
    filterset_fields = ['order', ]
 | 
			
		||||
    search_fields = ['$order', ]
 | 
			
		||||
 
 | 
			
		||||
@@ -4,15 +4,16 @@
 | 
			
		||||
from random import shuffle
 | 
			
		||||
 | 
			
		||||
from bootstrap_datepicker_plus.widgets import DateTimePickerInput
 | 
			
		||||
from crispy_forms.helper import FormHelper
 | 
			
		||||
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.inputs import Autocomplete, AmountInput
 | 
			
		||||
from note_kfet.middlewares import get_current_request
 | 
			
		||||
from permission.backends import PermissionBackend
 | 
			
		||||
 | 
			
		||||
from .models import Food, BasicFood, TransformedFood, QRCode
 | 
			
		||||
from .models import Food, BasicFood, TransformedFood, QRCode, Dish, Supplement, Order
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class QRCodeForms(forms.ModelForm):
 | 
			
		||||
@@ -166,7 +167,7 @@ class ManageIngredientsForm(forms.Form):
 | 
			
		||||
        model=Food,
 | 
			
		||||
        resetable=True,
 | 
			
		||||
        attrs={"api_url": "/api/food/food",
 | 
			
		||||
               "class": "autocomplete"},
 | 
			
		||||
               "class": "autocomplete manageingredients-autocomplete"},
 | 
			
		||||
    )
 | 
			
		||||
    name.label = _('Name')
 | 
			
		||||
 | 
			
		||||
@@ -180,8 +181,70 @@ class ManageIngredientsForm(forms.Form):
 | 
			
		||||
    )
 | 
			
		||||
    qrcode.label = _('QR code number')
 | 
			
		||||
 | 
			
		||||
    add_all_same_name = forms.BooleanField(
 | 
			
		||||
        required=False,
 | 
			
		||||
        label=_("Add all identical food")
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
ManageIngredientsFormSet = forms.formset_factory(
 | 
			
		||||
    ManageIngredientsForm,
 | 
			
		||||
    extra=1,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DishForm(forms.ModelForm):
 | 
			
		||||
    """
 | 
			
		||||
    Form to create a dish
 | 
			
		||||
    """
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Dish
 | 
			
		||||
        fields = ('main', 'price', 'available')
 | 
			
		||||
        widgets = {
 | 
			
		||||
            "price": AmountInput(),
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SupplementForm(forms.ModelForm):
 | 
			
		||||
    """
 | 
			
		||||
    Form to create a dish
 | 
			
		||||
    """
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Supplement
 | 
			
		||||
        fields = '__all__'
 | 
			
		||||
        widgets = {
 | 
			
		||||
            "price": AmountInput(),
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# The 2 following classes are copied from treasury app
 | 
			
		||||
# Add a subform per supplement in the dish form, and manage correctly the link between the dish and
 | 
			
		||||
# its supplements. The FormSet will search automatically the ForeignKey in the Supplement model.
 | 
			
		||||
SupplementFormSet = forms.inlineformset_factory(
 | 
			
		||||
    Dish,
 | 
			
		||||
    Supplement,
 | 
			
		||||
    form=SupplementForm,
 | 
			
		||||
    extra=1,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SupplementFormSetHelper(FormHelper):
 | 
			
		||||
    """
 | 
			
		||||
    Specify some template information for the supplement form
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    def __init__(self, form=None):
 | 
			
		||||
        super().__init__(form)
 | 
			
		||||
        self.form_tag = False
 | 
			
		||||
        self.form_method = 'POST'
 | 
			
		||||
        self.form_class = 'form-inline'
 | 
			
		||||
        self.template = 'bootstrap4/table_inline_formset.html'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class OrderForm(forms.ModelForm):
 | 
			
		||||
    """
 | 
			
		||||
    Form to order food
 | 
			
		||||
    """
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Order
 | 
			
		||||
        exclude = ("activity", "number", "ordered_at", "served", "served_at")
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,23 @@
 | 
			
		||||
# Generated by Django 5.2.4 on 2025-08-30 00:16
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('food', '0001_initial'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name='food',
 | 
			
		||||
            name='end_of_life',
 | 
			
		||||
            field=models.CharField(blank=True, max_length=255, verbose_name='end of life'),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name='food',
 | 
			
		||||
            name='order',
 | 
			
		||||
            field=models.CharField(blank=True, max_length=255, verbose_name='order'),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -0,0 +1,86 @@
 | 
			
		||||
# Generated by Django 5.2.6 on 2025-10-30 22:46
 | 
			
		||||
 | 
			
		||||
import django.db.models.deletion
 | 
			
		||||
import django.utils.timezone
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('activity', '0007_alter_guest_activity'),
 | 
			
		||||
        ('food', '0002_alter_food_end_of_life_alter_food_order'),
 | 
			
		||||
        ('note', '0007_alter_note_polymorphic_ctype_and_more'),
 | 
			
		||||
        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name='Dish',
 | 
			
		||||
            fields=[
 | 
			
		||||
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
			
		||||
                ('price', models.PositiveIntegerField(verbose_name='price')),
 | 
			
		||||
                ('available', models.BooleanField(default=True, verbose_name='available')),
 | 
			
		||||
                ('activity', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='dishes', to='activity.activity', verbose_name='activity')),
 | 
			
		||||
                ('main', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='dishes_as_main', to='food.transformedfood', verbose_name='main food')),
 | 
			
		||||
            ],
 | 
			
		||||
            options={
 | 
			
		||||
                'verbose_name': 'Dish',
 | 
			
		||||
                'verbose_name_plural': 'Dishes',
 | 
			
		||||
                'unique_together': {('main', 'activity')},
 | 
			
		||||
            },
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name='Order',
 | 
			
		||||
            fields=[
 | 
			
		||||
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
			
		||||
                ('request', models.TextField(blank=True, help_text='A specific request (to remove an ingredient for example)', verbose_name='request')),
 | 
			
		||||
                ('number', models.PositiveIntegerField(default=1, verbose_name='number')),
 | 
			
		||||
                ('ordered_at', models.DateTimeField(default=django.utils.timezone.now, verbose_name='order date')),
 | 
			
		||||
                ('served', models.BooleanField(default=False, verbose_name='served')),
 | 
			
		||||
                ('served_at', models.DateTimeField(blank=True, null=True, verbose_name='served date')),
 | 
			
		||||
                ('activity', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='food_orders', to='activity.activity', verbose_name='activity')),
 | 
			
		||||
                ('dish', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='orders', to='food.dish', verbose_name='dish')),
 | 
			
		||||
                ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='food_orders', to=settings.AUTH_USER_MODEL, verbose_name='user')),
 | 
			
		||||
            ],
 | 
			
		||||
            options={
 | 
			
		||||
                'verbose_name': 'Order',
 | 
			
		||||
                'verbose_name_plural': 'Orders',
 | 
			
		||||
            },
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name='FoodTransaction',
 | 
			
		||||
            fields=[
 | 
			
		||||
                ('transaction_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='note.transaction')),
 | 
			
		||||
                ('order', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='transaction', to='food.order', verbose_name='order')),
 | 
			
		||||
            ],
 | 
			
		||||
            options={
 | 
			
		||||
                'verbose_name': 'food transaction',
 | 
			
		||||
                'verbose_name_plural': 'food transactions',
 | 
			
		||||
            },
 | 
			
		||||
            bases=('note.transaction',),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name='Supplement',
 | 
			
		||||
            fields=[
 | 
			
		||||
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
			
		||||
                ('price', models.PositiveIntegerField(verbose_name='price')),
 | 
			
		||||
                ('dish', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='supplements', to='food.dish', verbose_name='dish')),
 | 
			
		||||
                ('food', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='supplements', to='food.food', verbose_name='food')),
 | 
			
		||||
            ],
 | 
			
		||||
            options={
 | 
			
		||||
                'verbose_name': 'Supplement',
 | 
			
		||||
                'verbose_name_plural': 'Supplements',
 | 
			
		||||
            },
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='order',
 | 
			
		||||
            name='supplements',
 | 
			
		||||
            field=models.ManyToManyField(blank=True, related_name='orders', to='food.supplement', verbose_name='supplements'),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterUniqueTogether(
 | 
			
		||||
            name='order',
 | 
			
		||||
            unique_together={('activity', 'number')},
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
							
								
								
									
										19
									
								
								apps/food/migrations/0004_alter_foodtransaction_order.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								apps/food/migrations/0004_alter_foodtransaction_order.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,19 @@
 | 
			
		||||
# Generated by Django 5.2.6 on 2025-10-31 17:46
 | 
			
		||||
 | 
			
		||||
import django.db.models.deletion
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('food', '0003_dish_order_foodtransaction_supplement_and_more'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name='foodtransaction',
 | 
			
		||||
            name='order',
 | 
			
		||||
            field=models.OneToOneField(on_delete=django.db.models.deletion.PROTECT, related_name='transaction', to='food.order', verbose_name='order'),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -4,10 +4,14 @@
 | 
			
		||||
from datetime import timedelta
 | 
			
		||||
 | 
			
		||||
from django.db import models, transaction
 | 
			
		||||
from django.core.exceptions import ValidationError
 | 
			
		||||
from django.utils import timezone
 | 
			
		||||
from django.contrib.auth.models import User
 | 
			
		||||
from django.utils.translation import gettext_lazy as _
 | 
			
		||||
from polymorphic.models import PolymorphicModel
 | 
			
		||||
from member.models import Club
 | 
			
		||||
from activity.models import Activity
 | 
			
		||||
from note.models import Transaction
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Allergen(models.Model):
 | 
			
		||||
@@ -252,7 +256,7 @@ class TransformedFood(Food):
 | 
			
		||||
                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)
 | 
			
		||||
        return super().save(force_insert=False, force_update=force_update, using=using, update_fields=update_fields)
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        verbose_name = _('Transformed food')
 | 
			
		||||
@@ -284,3 +288,196 @@ class QRCode(models.Model):
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return _('QR-code number') + ' ' + str(self.qr_code_number)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Dish(models.Model):
 | 
			
		||||
    """
 | 
			
		||||
    A dish is a food proposed during a meal
 | 
			
		||||
    """
 | 
			
		||||
    main = models.ForeignKey(
 | 
			
		||||
        TransformedFood,
 | 
			
		||||
        on_delete=models.PROTECT,
 | 
			
		||||
        related_name='dishes_as_main',
 | 
			
		||||
        verbose_name=_('main food'),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    price = models.PositiveIntegerField(
 | 
			
		||||
        verbose_name=_('price')
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    activity = models.ForeignKey(
 | 
			
		||||
        Activity,
 | 
			
		||||
        on_delete=models.CASCADE,
 | 
			
		||||
        related_name='dishes',
 | 
			
		||||
        verbose_name=_('activity'),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    available = models.BooleanField(
 | 
			
		||||
        default=True,
 | 
			
		||||
        verbose_name=_('available'),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        verbose_name = _('Dish')
 | 
			
		||||
        verbose_name_plural = _('Dishes')
 | 
			
		||||
        unique_together = ('main', 'activity')
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return self.main.name + ' (' + str(self.activity) + ')'
 | 
			
		||||
 | 
			
		||||
    def save(self, *args, **kwargs):
 | 
			
		||||
        "Check the type of activity"
 | 
			
		||||
        if self.activity.activity_type.name != 'Perm bouffe':
 | 
			
		||||
            raise ValidationError(_('(You cannot select this type of activity.'))
 | 
			
		||||
 | 
			
		||||
        return super().save(*args, **kwargs)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Supplement(models.Model):
 | 
			
		||||
    """
 | 
			
		||||
    A supplement is a food added to a dish
 | 
			
		||||
    """
 | 
			
		||||
    dish = models.ForeignKey(
 | 
			
		||||
        Dish,
 | 
			
		||||
        on_delete=models.CASCADE,
 | 
			
		||||
        related_name='supplements',
 | 
			
		||||
        verbose_name=_('dish'),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    food = models.ForeignKey(
 | 
			
		||||
        Food,
 | 
			
		||||
        on_delete=models.PROTECT,
 | 
			
		||||
        related_name='supplements',
 | 
			
		||||
        verbose_name=_('food'),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    price = models.PositiveIntegerField(
 | 
			
		||||
        verbose_name=_('price')
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        verbose_name = _('Supplement')
 | 
			
		||||
        verbose_name_plural = _('Supplements')
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return _("Supplement {food} for {dish}").format(
 | 
			
		||||
            food=str(self.food), dish=str(self.dish))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Order(models.Model):
 | 
			
		||||
    """
 | 
			
		||||
    An order is a dish ordered by a member during an activity
 | 
			
		||||
    """
 | 
			
		||||
    user = models.ForeignKey(
 | 
			
		||||
        User,
 | 
			
		||||
        on_delete=models.CASCADE,
 | 
			
		||||
        related_name='food_orders',
 | 
			
		||||
        verbose_name=_('user'),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    activity = models.ForeignKey(
 | 
			
		||||
        Activity,
 | 
			
		||||
        on_delete=models.CASCADE,
 | 
			
		||||
        related_name='food_orders',
 | 
			
		||||
        verbose_name=_('activity'),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    dish = models.ForeignKey(
 | 
			
		||||
        Dish,
 | 
			
		||||
        on_delete=models.CASCADE,
 | 
			
		||||
        related_name='orders',
 | 
			
		||||
        verbose_name=_('dish'),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    supplements = models.ManyToManyField(
 | 
			
		||||
        Supplement,
 | 
			
		||||
        related_name='orders',
 | 
			
		||||
        verbose_name=_('supplements'),
 | 
			
		||||
        blank=True,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    request = models.TextField(
 | 
			
		||||
        blank=True,
 | 
			
		||||
        verbose_name=_('request'),
 | 
			
		||||
        help_text=_('A specific request (to remove an ingredient for example)')
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    number = models.PositiveIntegerField(
 | 
			
		||||
        verbose_name=_('number'),
 | 
			
		||||
        default=1,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    ordered_at = models.DateTimeField(
 | 
			
		||||
        default=timezone.now,
 | 
			
		||||
        verbose_name=_('order date'),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    served = models.BooleanField(
 | 
			
		||||
        default=False,
 | 
			
		||||
        verbose_name=_('served'),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    served_at = models.DateTimeField(
 | 
			
		||||
        null=True,
 | 
			
		||||
        blank=True,
 | 
			
		||||
        verbose_name=_('served date'),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        verbose_name = _('Order')
 | 
			
		||||
        verbose_name_plural = _('Orders')
 | 
			
		||||
        unique_together = ('activity', 'number', )
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def amount(self):
 | 
			
		||||
        return self.dish.price + sum(s.price for s in self.supplements.all())
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return _("Order of {dish} by {user}").format(
 | 
			
		||||
            dish=str(self.dish),
 | 
			
		||||
            user=str(self.user))
 | 
			
		||||
 | 
			
		||||
    def save(self, *args, **kwargs):
 | 
			
		||||
        created = self.pk is None
 | 
			
		||||
        if created:
 | 
			
		||||
            last_order = Order.objects.filter(activity=self.activity).last()
 | 
			
		||||
            if last_order is None:
 | 
			
		||||
                self.number = 1
 | 
			
		||||
            else:
 | 
			
		||||
                self.number = last_order.number + 1
 | 
			
		||||
            super().save(*args, **kwargs)
 | 
			
		||||
 | 
			
		||||
            transaction = FoodTransaction(
 | 
			
		||||
                order=self,
 | 
			
		||||
                source=self.user.note,
 | 
			
		||||
                destination=self.activity.organizer.note,
 | 
			
		||||
                amount=self.amount,
 | 
			
		||||
                quantity=1,
 | 
			
		||||
            )
 | 
			
		||||
            transaction.save()
 | 
			
		||||
        else:
 | 
			
		||||
            old_object = Order.objects.get(pk=self.pk)
 | 
			
		||||
            if not old_object.served and self.served:
 | 
			
		||||
                self.served_at = timezone.now()
 | 
			
		||||
            self.transaction.save()
 | 
			
		||||
            super().save(*args, **kwargs)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class FoodTransaction(Transaction):
 | 
			
		||||
    """
 | 
			
		||||
    Special type of :model:`note.Transaction` associated to a :model:`food.Order`.
 | 
			
		||||
    """
 | 
			
		||||
    order = models.OneToOneField(
 | 
			
		||||
        Order,
 | 
			
		||||
        on_delete=models.PROTECT,
 | 
			
		||||
        related_name='transaction',
 | 
			
		||||
        verbose_name=_('order')
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        verbose_name = _("food transaction")
 | 
			
		||||
        verbose_name_plural = _("food transactions")
 | 
			
		||||
 | 
			
		||||
    def save(self, *args, **kwargs):
 | 
			
		||||
        self.valid = self.order.served
 | 
			
		||||
        super().save(*args, **kwargs)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										45
									
								
								apps/food/static/food/js/order.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								apps/food/static/food/js/order.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,45 @@
 | 
			
		||||
/**
 | 
			
		||||
 * On click of "delete", delete the order
 | 
			
		||||
 * @param button_id:Integer Order id to remove
 | 
			
		||||
 * @param table_id: Id of the table to reload
 | 
			
		||||
 */
 | 
			
		||||
function delete_button (button_id, table_id) {
 | 
			
		||||
  $.ajax({
 | 
			
		||||
    url: '/api/food/order/' + button_id + '/',
 | 
			
		||||
    method: 'DELETE',
 | 
			
		||||
    headers: { 'X-CSRFTOKEN': CSRF_TOKEN }
 | 
			
		||||
  }).done(function () {
 | 
			
		||||
    $('#' + table_id).load(location.pathname + ' #' + table_id + ' > *')
 | 
			
		||||
  }).fail(function (xhr, _textStatus, _error) {
 | 
			
		||||
    errMsg(xhr.responseJSON, 10000)
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * On click of "Serve", mark the order as served
 | 
			
		||||
 * @param button_id: Order id
 | 
			
		||||
 * @param table_id:  Id of the table to reload
 | 
			
		||||
 */
 | 
			
		||||
function serve_button(button_id, table_id, current_state) {
 | 
			
		||||
  const new_state = !current_state;
 | 
			
		||||
  $.ajax({
 | 
			
		||||
    url: '/api/food/order/' + button_id + '/',
 | 
			
		||||
    method: 'PATCH',
 | 
			
		||||
    headers: { 'X-CSRFTOKEN': CSRF_TOKEN },
 | 
			
		||||
    contentType: 'application/json',
 | 
			
		||||
    data: JSON.stringify({
 | 
			
		||||
      served: new_state
 | 
			
		||||
    })
 | 
			
		||||
  })
 | 
			
		||||
  .done(function () {
 | 
			
		||||
    if (current_state) {
 | 
			
		||||
    $('table').load(location.pathname + ' table')
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
    $('#' + table_id).load(location.pathname + ' #' + table_id + ' > *');
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
  .fail(function (xhr) {
 | 
			
		||||
    errMsg(xhr.responseJSON, 10000);
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
@@ -3,8 +3,11 @@
 | 
			
		||||
 | 
			
		||||
import django_tables2 as tables
 | 
			
		||||
from django.utils.translation import gettext_lazy as _
 | 
			
		||||
from note_kfet.middlewares import get_current_request
 | 
			
		||||
from note.templatetags.pretty_money import pretty_money
 | 
			
		||||
from permission.backends import PermissionBackend
 | 
			
		||||
 | 
			
		||||
from .models import Food
 | 
			
		||||
from .models import Food, Dish, Order
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class FoodTable(tables.Table):
 | 
			
		||||
@@ -35,3 +38,80 @@ class FoodTable(tables.Table):
 | 
			
		||||
            'data-href': lambda record: 'detail/' + str(record.pk),
 | 
			
		||||
            'style': 'cursor:pointer',
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DishTable(tables.Table):
 | 
			
		||||
    """
 | 
			
		||||
    List dishes
 | 
			
		||||
    """
 | 
			
		||||
    supplements = tables.Column(empty_values=(), verbose_name=_('Available supplements'), orderable=False)
 | 
			
		||||
 | 
			
		||||
    def render_supplements(self, record):
 | 
			
		||||
        return ", ".join(str(q.food) for q in record.supplements.all())
 | 
			
		||||
 | 
			
		||||
    def render_price(self, value):
 | 
			
		||||
        return pretty_money(value)
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Dish
 | 
			
		||||
        template_name = 'django_tables2/bootstrap4.html'
 | 
			
		||||
        fields = ('main', 'supplements', 'price', 'available')
 | 
			
		||||
        row_attrs = {
 | 
			
		||||
            'class': 'table-row',
 | 
			
		||||
            'data-href': lambda record: str(record.pk),
 | 
			
		||||
            'style': 'cursor:pointer',
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
DELETE_TEMPLATE = """
 | 
			
		||||
<button id="{{ record.pk }}"
 | 
			
		||||
        class="btn btn-danger btn-sm"
 | 
			
		||||
        onclick="delete_button(this.id, 'orders_table_{{ table.prefix }}')">
 | 
			
		||||
    {{ delete_trans }}
 | 
			
		||||
</button>
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
SERVE_TEMPLATE = """
 | 
			
		||||
<button id="{{ record.pk }}"
 | 
			
		||||
        class="btn btn-sm {% if record.served %}btn-secondary{% else %}btn-success{% endif %}"
 | 
			
		||||
        onclick="serve_button(this.id, 'orders_table_{{ table.prefix }}', {{ record.served|yesno:'true,false' }})">
 | 
			
		||||
    {% if record.served %}
 | 
			
		||||
        {{ record.served_at|date:"d/m/Y H:i" }}
 | 
			
		||||
    {% else %}""" + _('Serve') + """
 | 
			
		||||
    {% endif %}
 | 
			
		||||
</button>
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class OrderTable(tables.Table):
 | 
			
		||||
    """
 | 
			
		||||
    Lis all orders.
 | 
			
		||||
    """
 | 
			
		||||
    delete = tables.TemplateColumn(
 | 
			
		||||
        template_code=DELETE_TEMPLATE,
 | 
			
		||||
        extra_context={"delete_trans": _('Delete')},
 | 
			
		||||
        orderable=False,
 | 
			
		||||
        attrs={'td': {'class': lambda record: 'col-sm-1' + (
 | 
			
		||||
            ' d-none' if not PermissionBackend.check_perm(
 | 
			
		||||
                get_current_request(), "food.delete_order",
 | 
			
		||||
                record) else '')}}, verbose_name=_("Delete"), )
 | 
			
		||||
 | 
			
		||||
    serve = tables.TemplateColumn(
 | 
			
		||||
        template_code=SERVE_TEMPLATE,
 | 
			
		||||
        extra_context={"serve_trans": _('Serve')},
 | 
			
		||||
        orderable=False,
 | 
			
		||||
        attrs={'td': {'class': lambda record: 'col-sm-1' + (
 | 
			
		||||
            ' d-none' if not PermissionBackend.check_perm(
 | 
			
		||||
                get_current_request(), "food.change_order_saved",
 | 
			
		||||
                record) else '')}}, verbose_name=_("Serve"), )
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Order
 | 
			
		||||
        template_name = 'django_tables2/bootstrap4.html'
 | 
			
		||||
        fields = ('number', 'ordered_at', 'user', 'dish', 'supplements', 'request', 'serve', 'delete')
 | 
			
		||||
        order_by = ('ordered_at', )
 | 
			
		||||
        row_attrs = {
 | 
			
		||||
            'class': 'table-row',
 | 
			
		||||
            'style': 'cursor:pointer',
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										25
									
								
								apps/food/templates/food/dish_confirm_delete.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								apps/food/templates/food/dish_confirm_delete.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,25 @@
 | 
			
		||||
{% extends "base.html" %}
 | 
			
		||||
{% comment %}
 | 
			
		||||
SPDX-License-Identifier: GPL-3.0-or-later
 | 
			
		||||
{% endcomment %}
 | 
			
		||||
{% load i18n crispy_forms_tags %}
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
    <div class="card bg-light">
 | 
			
		||||
        <div class="card-header text-center">
 | 
			
		||||
            <h4>{% trans "Delete dish" %}</h4>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="card-body">
 | 
			
		||||
            <div class="alert alert-warning">
 | 
			
		||||
                {% blocktrans %}Are you sure you want to delete this dish? This action can't be undone.{% endblocktrans %}
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="card-footer text-center">
 | 
			
		||||
            <form method="post">
 | 
			
		||||
                {% csrf_token %}
 | 
			
		||||
                <a class="btn btn-primary" href="{% url 'food:dish_detail' activity_pk=object.activity.pk pk=object.pk%}">{% trans "Return to dish detail" %}</a>
 | 
			
		||||
                <button class="btn btn-danger" type="submit">{% trans "Delete" %}</button>
 | 
			
		||||
            </form>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
							
								
								
									
										44
									
								
								apps/food/templates/food/dish_detail.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								apps/food/templates/food/dish_detail.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,44 @@
 | 
			
		||||
{% extends "base.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 pretty_money %}
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
<div class="card bg-white mb-3">
 | 
			
		||||
  <h3 class="card-header text-center">
 | 
			
		||||
    {{ title }} {{ food.name }}
 | 
			
		||||
  </h3>
 | 
			
		||||
  <div class="card-body">
 | 
			
		||||
    <ul>
 | 
			
		||||
      <li> {% trans "Associated food" %} : 
 | 
			
		||||
        <a href="{% url "food:transformedfood_view" pk=food.pk %}">
 | 
			
		||||
          {{ food.name }}
 | 
			
		||||
        </a>
 | 
			
		||||
      </li>
 | 
			
		||||
      <li> {% trans "Sell price" %} : {{ dish.price|pretty_money }}</li>
 | 
			
		||||
      <li> {% trans "Available" %} : {{ dish.available|yesno }}</li>
 | 
			
		||||
      <li> {% trans "Possible supplements" %} : 
 | 
			
		||||
        {% for supp in supplements %}
 | 
			
		||||
          <a href="{% url "food:food_view" pk=supp.food.pk %}">{{ supp.food.name }} ({{ supp.price|pretty_money }})</a>{% if not forloop.last %},{% endif %}
 | 
			
		||||
        {% endfor %}
 | 
			
		||||
      </li>
 | 
			
		||||
    </ul>
 | 
			
		||||
    {% if update %}
 | 
			
		||||
        <a class="btn btn-sm btn-secondary" href="{% url "food:dish_update" activity_pk=dish.activity.pk pk=dish.pk %}">
 | 
			
		||||
          {% trans "Update" %}
 | 
			
		||||
        </a>
 | 
			
		||||
    {% endif %}
 | 
			
		||||
    <a class="btn btn-sm btn-primary" href="{% url "food:dish_list" activity_pk=dish.activity.pk %}">
 | 
			
		||||
          {% trans "Return to dish list" %}
 | 
			
		||||
        </a>
 | 
			
		||||
    {% if delete %}
 | 
			
		||||
    <a class="btn btn-sm btn-danger" href="{% url "food:dish_delete" activity_pk=dish.activity.pk pk=dish.pk %}">
 | 
			
		||||
          {% trans "Delete" %}
 | 
			
		||||
        </a>
 | 
			
		||||
    {% endif %}
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
							
								
								
									
										94
									
								
								apps/food/templates/food/dish_form.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								apps/food/templates/food/dish_form.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,94 @@
 | 
			
		||||
{% extends "base.html" %}
 | 
			
		||||
{% comment %}
 | 
			
		||||
Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
 | 
			
		||||
SPDX-License-Identifier: GPL-3.0-or-later
 | 
			
		||||
{% endcomment %}
 | 
			
		||||
{% load i18n crispy_forms_tags %}
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
<div class="card bg-white mb-3">
 | 
			
		||||
  <h3 class="card-header text-center">
 | 
			
		||||
    {{ title }}
 | 
			
		||||
  </h3>
 | 
			
		||||
  <form method="post" action="">
 | 
			
		||||
    {% csrf_token %}
 | 
			
		||||
    <div class="card-body">
 | 
			
		||||
      {% crispy form %}
 | 
			
		||||
    </div>
 | 
			
		||||
  <h3 class="card-header text-center">
 | 
			
		||||
    {% trans "Add supplements (optional)" %}
 | 
			
		||||
  </h3>
 | 
			
		||||
  {{ formset.management_form }}
 | 
			
		||||
  <table class="table table-condensed table-striped">
 | 
			
		||||
    {% for form in formset %}
 | 
			
		||||
    {% if forloop.first %}
 | 
			
		||||
    <thead>
 | 
			
		||||
      <tr>
 | 
			
		||||
        <th>{{ form.food.label }}<span class="asteriskField">*</span></th>
 | 
			
		||||
        <th>{{ form.price.label }}<span class="asteriskField">*</span></th>
 | 
			
		||||
      </tr>
 | 
			
		||||
    </thead>
 | 
			
		||||
    <tbody id="form_body">
 | 
			
		||||
      {% endif %}
 | 
			
		||||
      <tr class="row-formset">
 | 
			
		||||
        <td>{{ form.food }}</td>
 | 
			
		||||
        <td>{{ form.price }}</td>
 | 
			
		||||
        {# These fields are hidden but handled by the formset to link the id and the invoice id #}
 | 
			
		||||
        {{ form.dish }}
 | 
			
		||||
        {{ form.id }}
 | 
			
		||||
      </tr>
 | 
			
		||||
      {% endfor %}
 | 
			
		||||
    </tbody>
 | 
			
		||||
  </table>
 | 
			
		||||
 | 
			
		||||
  {# Display buttons to add and remove supplements #}
 | 
			
		||||
    <div class="card-body">
 | 
			
		||||
        <div class="btn-group btn-block" role="group">
 | 
			
		||||
            <button type="button" id="add_more" class="btn btn-success">{% trans "Add supplement" %}</button>
 | 
			
		||||
            <button type="button" id="remove_one" class="btn btn-danger">{% trans "Remove supplement" %}</button>
 | 
			
		||||
        </div>
 | 
			
		||||
        <button type="submit" class="btn btn-block btn-primary">{% trans "Submit" %}</button>
 | 
			
		||||
    </div>
 | 
			
		||||
  </form>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
{# Hidden div that store an empty supplement form, to be copied into new forms #}
 | 
			
		||||
<div id="empty_form" style="display: none;">
 | 
			
		||||
    <table class='no_error'>
 | 
			
		||||
        <tbody id="for_real">
 | 
			
		||||
            <tr class="row-formset">
 | 
			
		||||
                <td>{{ formset.empty_form.food }}</td>
 | 
			
		||||
                <td>{{ formset.empty_form.price }} </td>
 | 
			
		||||
                {{ formset.empty_form.dish }}
 | 
			
		||||
                {{ formset.empty_form.id }}
 | 
			
		||||
            </tr>
 | 
			
		||||
        </tbody>
 | 
			
		||||
    </table>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block extrajavascript %}
 | 
			
		||||
<script>
 | 
			
		||||
    /* script that handles add and remove lines */
 | 
			
		||||
    IDS = {};
 | 
			
		||||
 | 
			
		||||
    $("#id_supplements-TOTAL_FORMS").val($(".row-formset").length - 1);
 | 
			
		||||
 | 
			
		||||
    $('#add_more').click(function () {
 | 
			
		||||
        let form_idx = $('#id_supplements-TOTAL_FORMS').val();
 | 
			
		||||
        $('#form_body').append($('#for_real').html().replace(/__prefix__/g, form_idx));
 | 
			
		||||
        $('#id_supplements-TOTAL_FORMS').val(parseInt(form_idx) + 1);
 | 
			
		||||
        $('#id_supplements-' + parseInt(form_idx) + '-id').val(IDS[parseInt(form_idx)]);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    $('#remove_one').click(function () {
 | 
			
		||||
        let form_idx = $('#id_supplements-TOTAL_FORMS').val();
 | 
			
		||||
        if (form_idx > 0) {
 | 
			
		||||
            IDS[parseInt(form_idx) - 1] = $('#id_supplements-' + (parseInt(form_idx) - 1) + '-id').val();
 | 
			
		||||
            $('#form_body tr:last-child').remove();
 | 
			
		||||
            $('#id_supplements-TOTAL_FORMS').val(parseInt(form_idx) - 1);
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
</script>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
							
								
								
									
										33
									
								
								apps/food/templates/food/dish_list.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								apps/food/templates/food/dish_list.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,33 @@
 | 
			
		||||
{% extends "base.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 %}
 | 
			
		||||
<div class="card bg-white mb-3">
 | 
			
		||||
    <h3 class="card-header text-center">
 | 
			
		||||
        {{ title }} {{activity.name}}
 | 
			
		||||
    </h3>
 | 
			
		||||
    {% render_table table %}
 | 
			
		||||
    <div class="card-footer">
 | 
			
		||||
        {% if can_add_dish %}
 | 
			
		||||
        <a class="btn btn-sm btn-success" href="{% url 'food:dish_create' activity_pk=activity.pk %}">{% trans "New dish" %}</a>
 | 
			
		||||
        {% endif %}
 | 
			
		||||
        <a class="btn btn-sm btn-secondary" href="{% url 'activity:activity_detail' pk=activity.pk %}">{% trans "Activity page" %}</a>
 | 
			
		||||
        <a class="btn btn-sm btn-primary" href="{% url "food:food_list" %}">
 | 
			
		||||
          {% trans "Return to the food list" %}
 | 
			
		||||
        </a>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block extrajavascript %}
 | 
			
		||||
<script type="text/javascript">
 | 
			
		||||
    $(".table-row").click(function () {
 | 
			
		||||
            window.document.location = $(this).data("href");
 | 
			
		||||
        });
 | 
			
		||||
</script>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
@@ -64,13 +64,19 @@ SPDX-License-Identifier: GPL-3.0-or-later
 | 
			
		||||
  <h3 class="card-header text-center">
 | 
			
		||||
    {% trans "Meal served" %}
 | 
			
		||||
  </h3>
 | 
			
		||||
  {% if can_add_meal %}
 | 
			
		||||
  <div class="card-footer">
 | 
			
		||||
    {% if can_add_meal %}
 | 
			
		||||
    <a class="btn btn-sm btn-primary" href="{% url 'food:transformedfood_create' %}">
 | 
			
		||||
      {% trans "New meal" %}
 | 
			
		||||
    </a>
 | 
			
		||||
    {% endif %}
 | 
			
		||||
    {% for activity in open_activities %}
 | 
			
		||||
      <a class="btn btn-sm btn-secondary" href="{% url 'food:dish_list' activity_pk=activity.pk %}">
 | 
			
		||||
      {% trans "View" %} {{ activity.name }}
 | 
			
		||||
    </a>
 | 
			
		||||
    {% endfor %}
 | 
			
		||||
  </div>
 | 
			
		||||
  {% endif %}
 | 
			
		||||
 | 
			
		||||
  {% if served.data %}
 | 
			
		||||
  {% render_table served %}
 | 
			
		||||
  {% else %}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										41
									
								
								apps/food/templates/food/kitchen.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								apps/food/templates/food/kitchen.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,41 @@
 | 
			
		||||
{% extends "base.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 %}
 | 
			
		||||
 | 
			
		||||
  <!-- Colonne de plats -->
 | 
			
		||||
  <div style="display: flex; flex-wrap: wrap; gap: 1rem; margin-bottom: 2rem;">
 | 
			
		||||
  {% for food, quantity in orders.items %}
 | 
			
		||||
    <div class="card bg-white mb-3" style="flex: 1 1 calc(33.333% - 1rem); border: 1px solid #ccc; padding: 1rem; border-radius: 0.5rem; box-sizing: border-box;">
 | 
			
		||||
        
 | 
			
		||||
        <h3 class="card-header text-center">
 | 
			
		||||
      <strong>{{ food }}</strong><br>
 | 
			
		||||
      </h3>
 | 
			
		||||
      <h1 class="card-body text-center">
 | 
			
		||||
      {{ quantity }}</h1>
 | 
			
		||||
    </div>
 | 
			
		||||
  {% endfor %}
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
  <!-- Colonne de la table -->
 | 
			
		||||
        <div class="card bg-white mb-3">
 | 
			
		||||
            <h3 class="card-header text-center">
 | 
			
		||||
                {% trans "Special orders" %}
 | 
			
		||||
            </h3>
 | 
			
		||||
            {% if table.data %}
 | 
			
		||||
            {% render_table table %}
 | 
			
		||||
            {% else %}
 | 
			
		||||
            <div class="card-body">
 | 
			
		||||
                <div class="alert alert-warning">
 | 
			
		||||
                    {% trans "There are no special orders." %}
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            {% endif %}
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
{% endblock %}
 | 
			
		||||
@@ -22,6 +22,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
 | 
			
		||||
	  <th>{{ form.name.label }}</th>
 | 
			
		||||
	  <th>{{ form.qrcode.label }}</th>
 | 
			
		||||
	  <th>{{ form.fully_used.label }}</th>
 | 
			
		||||
	  <th>{{ form.add_all_same_name.label }}</th>
 | 
			
		||||
	</tr>
 | 
			
		||||
      </thead>
 | 
			
		||||
      <tbody id="form_body">
 | 
			
		||||
@@ -34,6 +35,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
 | 
			
		||||
	  <td>{{ form.name }}</td>
 | 
			
		||||
	  <td>{{ form.qrcode }}</td>
 | 
			
		||||
	  <td>{{ form.fully_used }}</td>
 | 
			
		||||
	  <td>{{ form.add_all_same_name }}</td>
 | 
			
		||||
	</tr>
 | 
			
		||||
      {% endfor %}
 | 
			
		||||
      </tbody>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										25
									
								
								apps/food/templates/food/order_confirm_delete.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								apps/food/templates/food/order_confirm_delete.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,25 @@
 | 
			
		||||
{% extends "base.html" %}
 | 
			
		||||
{% comment %}
 | 
			
		||||
SPDX-License-Identifier: GPL-3.0-or-later
 | 
			
		||||
{% endcomment %}
 | 
			
		||||
{% load i18n crispy_forms_tags %}
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
    <div class="card bg-light">
 | 
			
		||||
        <div class="card-header text-center">
 | 
			
		||||
            <h4>{% trans "Delete order" %}</h4>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="card-body">
 | 
			
		||||
            <div class="alert alert-warning">
 | 
			
		||||
                {% blocktrans %}Are you sure you want to delete this order? This action can't be undone.{% endblocktrans %}
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="card-footer text-center">
 | 
			
		||||
            <form method="post">
 | 
			
		||||
                {% csrf_token %}
 | 
			
		||||
                <a class="btn btn-primary" href="{% url 'food:order_list' activity_pk=object.activity.pk%}">{% trans "Return to order list" %}</a>
 | 
			
		||||
                <button class="btn btn-danger" type="submit">{% trans "Delete" %}</button>
 | 
			
		||||
            </form>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
							
								
								
									
										21
									
								
								apps/food/templates/food/order_form.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								apps/food/templates/food/order_form.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,21 @@
 | 
			
		||||
{% extends "base.html" %}
 | 
			
		||||
{% comment %}
 | 
			
		||||
Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
 | 
			
		||||
SPDX-License-Identifier: GPL-3.0-or-later
 | 
			
		||||
{% endcomment %}
 | 
			
		||||
{% load i18n crispy_forms_tags %}
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
<div class="card bg-white mb-3">
 | 
			
		||||
  <h3 class="card-header text-center">
 | 
			
		||||
    {{ title }}
 | 
			
		||||
  </h3>
 | 
			
		||||
  <div class="card-body" id="form">
 | 
			
		||||
    <form method="post">
 | 
			
		||||
      {% csrf_token %}
 | 
			
		||||
      {{ form | crispy }}
 | 
			
		||||
      <button class="btn btn-primary" type="submit">{% trans "Submit"%}</button>
 | 
			
		||||
    </form>
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
							
								
								
									
										30
									
								
								apps/food/templates/food/order_list.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								apps/food/templates/food/order_list.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,30 @@
 | 
			
		||||
{% extends "base.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 static i18n %}
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
<div class="card bg-white mb-3">
 | 
			
		||||
    <h3 class="card-header text-center">
 | 
			
		||||
        {{ title }}
 | 
			
		||||
    </h3>
 | 
			
		||||
    <a class="btn btn-primary" href="{% url 'food:served_order_list' activity_pk=activity.pk %}">{% trans "View served orders" %}</a>
 | 
			
		||||
      {% for table in tables %}
 | 
			
		||||
        <div class="card bg-light mb-3" id="orders_table_{{ table.prefix }}">
 | 
			
		||||
        <h3 class="card-header text-center">
 | 
			
		||||
            {% trans "Orders of " %} {{ table.prefix }} 
 | 
			
		||||
        </h3>
 | 
			
		||||
        {% if table.data %}
 | 
			
		||||
            {% render_table table %}
 | 
			
		||||
        {% endif %}
 | 
			
		||||
        </div>
 | 
			
		||||
      {% endfor %}
 | 
			
		||||
</div>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block extrajavascript %}
 | 
			
		||||
<script src="{% static "food/js/order.js" %}"></script>
 | 
			
		||||
{% endblock%}
 | 
			
		||||
							
								
								
									
										21
									
								
								apps/food/templates/food/served_order_list.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								apps/food/templates/food/served_order_list.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,21 @@
 | 
			
		||||
{% extends "base.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 static i18n %}
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
<div class="card bg-white mb-3">
 | 
			
		||||
    <h3 class="card-header text-center">
 | 
			
		||||
        {{ title }} {{activity.name}}
 | 
			
		||||
    </h3>
 | 
			
		||||
    <a class="btn btn-primary" href="{% url 'food:order_list' activity_pk=activity.pk %}">{% trans "View unserved orders" %}</a>
 | 
			
		||||
    {% render_table table %}
 | 
			
		||||
</div>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block extrajavascript %}
 | 
			
		||||
<script src="{% static "food/js/order.js" %}"></script>
 | 
			
		||||
{% endblock%}
 | 
			
		||||
							
								
								
									
										17
									
								
								apps/food/templates/food/supplement_detail.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								apps/food/templates/food/supplement_detail.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,17 @@
 | 
			
		||||
{% extends "base.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 pretty_money %}
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
<div class="card bg-white mb-3">
 | 
			
		||||
  <h3 class="card-header text-center">
 | 
			
		||||
    {{ title }} {{ supplement.name }}
 | 
			
		||||
  </h3>
 | 
			
		||||
  <div class="card-body">
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
							
								
								
									
										0
									
								
								apps/food/tests/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								apps/food/tests/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -6,9 +6,12 @@ from django.contrib.auth.models import User
 | 
			
		||||
from django.test import TestCase
 | 
			
		||||
from django.urls import reverse
 | 
			
		||||
from django.utils import timezone
 | 
			
		||||
from activity.models import Activity, ActivityType
 | 
			
		||||
from member.models import Club
 | 
			
		||||
 | 
			
		||||
from ..api.views import AllergenViewSet, BasicFoodViewSet, TransformedFoodViewSet, QRCodeViewSet
 | 
			
		||||
from ..models import Allergen, BasicFood, TransformedFood, QRCode
 | 
			
		||||
from ..api.views import AllergenViewSet, BasicFoodViewSet, TransformedFoodViewSet, QRCodeViewSet, \
 | 
			
		||||
    DishViewSet, SupplementViewSet, OrderViewSet, FoodTransactionViewSet
 | 
			
		||||
from ..models import Allergen, BasicFood, TransformedFood, QRCode, Dish, Supplement, Order, FoodTransaction
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestFood(TestCase):
 | 
			
		||||
@@ -53,73 +56,293 @@ class TestFood(TestCase):
 | 
			
		||||
            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_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_qrcode_create(self):
 | 
			
		||||
        """
 | 
			
		||||
        Display QRCode creation
 | 
			
		||||
        """
 | 
			
		||||
        response = self.client.get(reverse('food:qrcode_create', kwargs={"slug": 2}))
 | 
			
		||||
        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_basicfood_create(self):
 | 
			
		||||
        """
 | 
			
		||||
        Display BasicFood creation
 | 
			
		||||
        """
 | 
			
		||||
        response = self.client.get(reverse('food:basicfood_create', kwargs={"slug": 2}))
 | 
			
		||||
        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_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_update(self):
 | 
			
		||||
        """
 | 
			
		||||
        Display Food update
 | 
			
		||||
        """
 | 
			
		||||
        response = self.client.get(reverse('food:food_update', args=(self.basicfood.pk,)))
 | 
			
		||||
        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_food_view(self):
 | 
			
		||||
        """
 | 
			
		||||
        Display Food detail
 | 
			
		||||
        """
 | 
			
		||||
        response = self.client.get(reverse('food:food_view', args=(self.basicfood.pk,)))
 | 
			
		||||
        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_basicfood_view(self):
 | 
			
		||||
        """
 | 
			
		||||
        Display BasicFood detail
 | 
			
		||||
        """
 | 
			
		||||
        response = self.client.get(reverse('food:basicfood_view', args=(self.basicfood.pk,)))
 | 
			
		||||
        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_transformedfood_view(self):
 | 
			
		||||
        """
 | 
			
		||||
        Display TransformedFood detail
 | 
			
		||||
        """
 | 
			
		||||
        response = self.client.get(reverse('food:transformedfood_view', args=(self.transformedfood.pk,)))
 | 
			
		||||
        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)
 | 
			
		||||
    def test_add_ingredient(self):
 | 
			
		||||
        """
 | 
			
		||||
        Display add ingredient view
 | 
			
		||||
        """
 | 
			
		||||
        response = self.client.get(reverse('food:add_ingredient', args=(self.transformedfood.pk,)))
 | 
			
		||||
        self.assertEqual(response.status_code, 200)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestFoodOrder(TestCase):
 | 
			
		||||
    """
 | 
			
		||||
    Test Food Order
 | 
			
		||||
    """
 | 
			
		||||
    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.basicfood = BasicFood.objects.create(
 | 
			
		||||
            id=1,
 | 
			
		||||
            name='basicfood',
 | 
			
		||||
            owner=Club.objects.get(name="BDE"),
 | 
			
		||||
            expiry_date=timezone.now(),
 | 
			
		||||
            is_ready=True,
 | 
			
		||||
            date_type='DLC',
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        self.transformedfood = TransformedFood.objects.create(
 | 
			
		||||
            id=2,
 | 
			
		||||
            name='transformedfood',
 | 
			
		||||
            owner=Club.objects.get(name="BDE"),
 | 
			
		||||
            expiry_date=timezone.now(),
 | 
			
		||||
            is_ready=True,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        self.second_transformedfood = TransformedFood.objects.create(
 | 
			
		||||
            id=3,
 | 
			
		||||
            name='second transformedfood',
 | 
			
		||||
            owner=Club.objects.get(name="BDE"),
 | 
			
		||||
            expiry_date=timezone.now(),
 | 
			
		||||
            is_ready=True,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        self.third_transformedfood = TransformedFood.objects.create(
 | 
			
		||||
            id=4,
 | 
			
		||||
            name='third transformedfood',
 | 
			
		||||
            owner=Club.objects.get(name="BDE"),
 | 
			
		||||
            expiry_date=timezone.now(),
 | 
			
		||||
            is_ready=True,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        self.activity = Activity.objects.create(
 | 
			
		||||
            activity_type=ActivityType.objects.get(name="Perm bouffe"),
 | 
			
		||||
            organizer=Club.objects.get(name="BDE"),
 | 
			
		||||
            creater=self.user,
 | 
			
		||||
            attendees_club_id=1,
 | 
			
		||||
            date_start=timezone.now(),
 | 
			
		||||
            date_end=timezone.now(),
 | 
			
		||||
            name="Test activity",
 | 
			
		||||
            open=True,
 | 
			
		||||
            valid=True,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        self.dish = Dish.objects.create(
 | 
			
		||||
            main=self.transformedfood,
 | 
			
		||||
            price=500,
 | 
			
		||||
            activity=self.activity,
 | 
			
		||||
            available=True,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        self.second_dish = Dish.objects.create(
 | 
			
		||||
            main=self.second_transformedfood,
 | 
			
		||||
            price=1000,
 | 
			
		||||
            activity=self.activity,
 | 
			
		||||
            available=True,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        self.supplement = Supplement.objects.create(
 | 
			
		||||
            dish=self.dish,
 | 
			
		||||
            food=self.basicfood,
 | 
			
		||||
            price=100,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        self.order = Order.objects.create(
 | 
			
		||||
            user=self.user,
 | 
			
		||||
            activity=self.activity,
 | 
			
		||||
            dish=self.dish,
 | 
			
		||||
        )
 | 
			
		||||
        self.order.supplements.add(self.supplement)
 | 
			
		||||
        self.order.save()
 | 
			
		||||
 | 
			
		||||
    def test_dish_list(self):
 | 
			
		||||
        """
 | 
			
		||||
        Try to display dish list
 | 
			
		||||
        """
 | 
			
		||||
        response = self.client.get(reverse("food:dish_list", kwargs={"activity_pk": self.activity.pk}))
 | 
			
		||||
        self.assertEqual(response.status_code, 200)
 | 
			
		||||
 | 
			
		||||
    def test_dish_create(self):
 | 
			
		||||
        """
 | 
			
		||||
        Try to create a dish
 | 
			
		||||
        """
 | 
			
		||||
        response = self.client.get(reverse("food:dish_create", kwargs={"activity_pk": self.activity.pk}))
 | 
			
		||||
        self.assertEqual(response.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        response = self.client.post(reverse("food:dish_create", kwargs={"activity_pk": self.activity.pk}), data={
 | 
			
		||||
            "main": self.third_transformedfood.pk,
 | 
			
		||||
            "price": 4,
 | 
			
		||||
            "activity": self.activity.pk,
 | 
			
		||||
            "supplements-0-food": self.basicfood.pk,
 | 
			
		||||
            "supplements-0-price": 0.5,
 | 
			
		||||
            "supplements-TOTAL_FORMS": 1,
 | 
			
		||||
            "supplements-INITIAL_FORMS": 0,
 | 
			
		||||
            "supplements-MIN_NUM_FORMS": 0,
 | 
			
		||||
            "supplements-MAX_NUM_FORMS": 1000,
 | 
			
		||||
        })
 | 
			
		||||
        self.assertRedirects(response, reverse("food:dish_list", kwargs={"activity_pk": self.activity.pk}), 302, 200)
 | 
			
		||||
        self.assertTrue(Dish.objects.filter(main=self.third_transformedfood).exists())
 | 
			
		||||
        self.assertTrue(Supplement.objects.filter(food=self.basicfood, price=50).exists())
 | 
			
		||||
 | 
			
		||||
    def test_dish_update(self):
 | 
			
		||||
        """
 | 
			
		||||
        Try to update a dish
 | 
			
		||||
        """
 | 
			
		||||
        response = self.client.get(reverse("food:dish_update", kwargs={"activity_pk": self.activity.pk, "pk": self.dish.pk}))
 | 
			
		||||
        self.assertEqual(response.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        response = self.client.post(reverse("food:dish_update", kwargs={"activity_pk": self.activity.pk, "pk": self.dish.pk}), data={
 | 
			
		||||
            "price": 6,
 | 
			
		||||
            "supplements-0-food": self.basicfood.pk,
 | 
			
		||||
            "supplements-0-price": 1,
 | 
			
		||||
            "supplements-1-food": self.basicfood.pk,
 | 
			
		||||
            "supplements-1-price": 0.25,
 | 
			
		||||
            "supplements-TOTAL_FORMS": 2,
 | 
			
		||||
            "supplements-INITIAL_FORMS": 0,
 | 
			
		||||
            "supplements-MIN_NUM_FORMS": 0,
 | 
			
		||||
            "supplements-MAX_NUM_FORMS": 1000,
 | 
			
		||||
        })
 | 
			
		||||
        self.assertRedirects(response, reverse("food:dish_detail", kwargs={"activity_pk": self.activity.pk, "pk": self.dish.pk}), 302, 200)
 | 
			
		||||
        self.dish.refresh_from_db()
 | 
			
		||||
        self.assertTrue(Dish.objects.filter(main=self.transformedfood, price=600).exists())
 | 
			
		||||
        self.assertTrue(Supplement.objects.filter(dish=self.dish, food=self.basicfood, price=25).exists())
 | 
			
		||||
 | 
			
		||||
    def test_dish_detail(self):
 | 
			
		||||
        """
 | 
			
		||||
        Try to display dish details
 | 
			
		||||
        """
 | 
			
		||||
        response = self.client.get(reverse("food:dish_detail", kwargs={"activity_pk": self.activity.pk, "pk": self.dish.pk}))
 | 
			
		||||
        self.assertEqual(response.status_code, 200)
 | 
			
		||||
 | 
			
		||||
    def test_dish_delete(self):
 | 
			
		||||
        """
 | 
			
		||||
        Try to delete a dish
 | 
			
		||||
        """
 | 
			
		||||
        response = self.client.get(reverse("food:dish_delete", kwargs={"activity_pk": self.activity.pk, "pk": self.dish.pk}))
 | 
			
		||||
        self.assertEqual(response.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        # Cannot delete already ordered Dish
 | 
			
		||||
        response = self.client.delete(reverse("food:dish_delete", kwargs={"activity_pk": self.activity.pk, "pk": self.dish.pk}))
 | 
			
		||||
        self.assertEqual(response.status_code, 403)
 | 
			
		||||
        self.assertTrue(Dish.objects.filter(pk=self.dish.pk).exists())
 | 
			
		||||
 | 
			
		||||
        # Can delete a Dish with no order
 | 
			
		||||
        response = self.client.delete(reverse("food:dish_delete", kwargs={"activity_pk": self.activity.pk, "pk": self.second_dish.pk}))
 | 
			
		||||
        self.assertRedirects(response, reverse("food:dish_list", kwargs={"activity_pk": self.activity.pk}))
 | 
			
		||||
        self.assertFalse(Dish.objects.filter(pk=self.second_dish.pk).exists())
 | 
			
		||||
 | 
			
		||||
    def test_order_food(self):
 | 
			
		||||
        """
 | 
			
		||||
        Try to make an order
 | 
			
		||||
        """
 | 
			
		||||
        response = self.client.get(reverse("food:order_create", kwargs={"activity_pk": self.activity.pk}))
 | 
			
		||||
        self.assertEqual(response.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        response = self.client.post(reverse("food:order_create", kwargs={"activity_pk": self.activity.pk}), data=dict(
 | 
			
		||||
            user=self.user.pk,
 | 
			
		||||
            activity=self.activity.pk,
 | 
			
		||||
            dish=self.second_dish.pk,
 | 
			
		||||
            supplements=self.supplement.pk
 | 
			
		||||
        ))
 | 
			
		||||
        self.assertRedirects(response, reverse("food:food_list"))
 | 
			
		||||
        self.assertTrue(Order.objects.filter(user=self.user, dish=self.second_dish, activity=self.activity).exists())
 | 
			
		||||
 | 
			
		||||
    def test_order_list(self):
 | 
			
		||||
        """
 | 
			
		||||
        Try to display order list
 | 
			
		||||
        """
 | 
			
		||||
        response = self.client.get(reverse("food:order_list", kwargs={"activity_pk": self.activity.pk}))
 | 
			
		||||
        self.assertEqual(response.status_code, 200)
 | 
			
		||||
 | 
			
		||||
    def test_served_order_list(self):
 | 
			
		||||
        """
 | 
			
		||||
        Try to display served order list
 | 
			
		||||
        """
 | 
			
		||||
        response = self.client.get(reverse("food:served_order_list", kwargs={"activity_pk": self.activity.pk}))
 | 
			
		||||
        self.assertEqual(response.status_code, 200)
 | 
			
		||||
 | 
			
		||||
    def test_serve_order(self):
 | 
			
		||||
        """
 | 
			
		||||
        Try to serve an order, then to unserve it
 | 
			
		||||
        """
 | 
			
		||||
        response = self.client.patch("/api/food/order/" + str(self.order.pk) + "/", data=dict(
 | 
			
		||||
            served=True
 | 
			
		||||
        ), content_type="application/json")
 | 
			
		||||
        self.assertEqual(response.status_code, 200)
 | 
			
		||||
        self.order.refresh_from_db()
 | 
			
		||||
        self.assertTrue(Order.objects.filter(dish=self.dish, user=self.user, served=True).exists())
 | 
			
		||||
        self.assertIsNotNone(self.order.served_at)
 | 
			
		||||
 | 
			
		||||
        self.assertTrue(FoodTransaction.objects.filter(order=self.order, valid=True).exists())
 | 
			
		||||
 | 
			
		||||
        response = self.client.patch("/api/food/order/" + str(self.order.pk) + "/", data=dict(
 | 
			
		||||
            served=False
 | 
			
		||||
        ), content_type="application/json")
 | 
			
		||||
        self.assertEqual(response.status_code, 200)
 | 
			
		||||
        self.assertTrue(Order.objects.filter(dish=self.dish, user=self.user, served=False).exists())
 | 
			
		||||
 | 
			
		||||
        self.assertTrue(FoodTransaction.objects.filter(order=self.order, valid=False).exists())
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestFoodAPI(TestAPI):
 | 
			
		||||
    def setUp(self) -> None:
 | 
			
		||||
        super().setUP()
 | 
			
		||||
        super().setUp()
 | 
			
		||||
 | 
			
		||||
        self.allergen = Allergen.objects.create(
 | 
			
		||||
            name='name',
 | 
			
		||||
@@ -145,26 +368,84 @@ class TestFoodAPI(TestAPI):
 | 
			
		||||
            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/')
 | 
			
		||||
        self.activity = Activity.objects.create(
 | 
			
		||||
            activity_type=ActivityType.objects.get(name="Perm bouffe"),
 | 
			
		||||
            organizer=Club.objects.get(name="BDE"),
 | 
			
		||||
            creater=self.user,
 | 
			
		||||
            attendees_club_id=1,
 | 
			
		||||
            date_start=timezone.now(),
 | 
			
		||||
            date_end=timezone.now(),
 | 
			
		||||
            name="Test activity",
 | 
			
		||||
            open=True,
 | 
			
		||||
            valid=True,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        def test_basicfood_api(self):
 | 
			
		||||
            """
 | 
			
		||||
            Load BasicFood API page and test all filters and permissions
 | 
			
		||||
            """
 | 
			
		||||
            self.check_viewset(BasicFoodViewSet, '/api/food/basicfood/')
 | 
			
		||||
        self.dish = Dish.objects.create(
 | 
			
		||||
            main=self.transformedfood,
 | 
			
		||||
            price=500,
 | 
			
		||||
            activity=self.activity,
 | 
			
		||||
            available=True,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        self.supplement = Supplement.objects.create(
 | 
			
		||||
            dish=self.dish,
 | 
			
		||||
            food=self.basicfood,
 | 
			
		||||
            price=100,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        self.order = Order.objects.create(
 | 
			
		||||
            user=self.user,
 | 
			
		||||
            activity=self.activity,
 | 
			
		||||
            dish=self.dish,
 | 
			
		||||
        )
 | 
			
		||||
        self.order.supplements.add(self.supplement)
 | 
			
		||||
        self.order.save()
 | 
			
		||||
 | 
			
		||||
    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/')
 | 
			
		||||
 | 
			
		||||
        # TODO Repair and detabulate this test
 | 
			
		||||
        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/')
 | 
			
		||||
    def test_qrcode_api(self):
 | 
			
		||||
        """
 | 
			
		||||
        Load QRCode API page and test all filters and permissions
 | 
			
		||||
        """
 | 
			
		||||
        self.check_viewset(QRCodeViewSet, '/api/food/qrcode/')
 | 
			
		||||
 | 
			
		||||
    def test_dish_api(self):
 | 
			
		||||
        """
 | 
			
		||||
        Load Dish API page and test all filters and permissions
 | 
			
		||||
        """
 | 
			
		||||
        self.check_viewset(DishViewSet, '/api/food/dish/')
 | 
			
		||||
 | 
			
		||||
    def test_supplement_api(self):
 | 
			
		||||
        """
 | 
			
		||||
        Load Supplement API page and test all filters and permissions
 | 
			
		||||
        """
 | 
			
		||||
        self.check_viewset(SupplementViewSet, '/api/food/supplement/')
 | 
			
		||||
 | 
			
		||||
    def test_order_api(self):
 | 
			
		||||
        """
 | 
			
		||||
        Load Order API page and test all filters and permissions
 | 
			
		||||
        """
 | 
			
		||||
        self.check_viewset(OrderViewSet, '/api/food/order/')
 | 
			
		||||
 | 
			
		||||
    def test_foodtransaction_api(self):
 | 
			
		||||
        """
 | 
			
		||||
        Load FoodTransaction API page and test all filters and permissions
 | 
			
		||||
        """
 | 
			
		||||
        self.check_viewset(FoodTransactionViewSet, '/api/food/foodtransaction/')
 | 
			
		||||
 
 | 
			
		||||
@@ -19,4 +19,14 @@ urlpatterns = [
 | 
			
		||||
    path('detail/transformed/<int:pk>', views.TransformedFoodDetailView.as_view(), name='transformedfood_view'),
 | 
			
		||||
    path('add/ingredient/<int:pk>', views.AddIngredientView.as_view(), name='add_ingredient'),
 | 
			
		||||
    path('redirect/', views.QRCodeRedirectView.as_view(), name='redirect_view'),
 | 
			
		||||
    # TODO not always store activity_pk in url
 | 
			
		||||
    path('activity/<int:activity_pk>/dishes/add/', views.DishCreateView.as_view(), name='dish_create'),
 | 
			
		||||
    path('activity/<int:activity_pk>/dishes/', views.DishListView.as_view(), name='dish_list'),
 | 
			
		||||
    path('activity/<int:activity_pk>/dishes/<int:pk>/', views.DishDetailView.as_view(), name='dish_detail'),
 | 
			
		||||
    path('activity/<int:activity_pk>/dishes/<int:pk>/update/', views.DishUpdateView.as_view(), name='dish_update'),
 | 
			
		||||
    path('activity/<int:activity_pk>/dishes/<int:pk>/delete/', views.DishDeleteView.as_view(), name='dish_delete'),
 | 
			
		||||
    path('activity/<int:activity_pk>/order/', views.OrderCreateView.as_view(), name='order_create'),
 | 
			
		||||
    path('activity/<int:activity_pk>/orders/', views.OrderListView.as_view(), name='order_list'),
 | 
			
		||||
    path('activity/<int:activity_pk>/orders/served', views.ServedOrderListView.as_view(), name='served_order_list'),
 | 
			
		||||
    path('activity/<int:activity_pk>/kitchen/', views.KitchenView.as_view(), name='kitchen'),
 | 
			
		||||
]
 | 
			
		||||
 
 | 
			
		||||
@@ -4,25 +4,30 @@
 | 
			
		||||
from datetime import timedelta
 | 
			
		||||
 | 
			
		||||
from api.viewsets import is_regex
 | 
			
		||||
from django_tables2.views import MultiTableMixin
 | 
			
		||||
from crispy_forms.helper import FormHelper
 | 
			
		||||
from django_tables2.views import SingleTableView, MultiTableMixin
 | 
			
		||||
from django.core.exceptions import PermissionDenied
 | 
			
		||||
from django.db import transaction
 | 
			
		||||
from django.db.models import Q
 | 
			
		||||
from django.db.models import Q, Count
 | 
			
		||||
from django.http import HttpResponseRedirect, Http404
 | 
			
		||||
from django.views.generic import DetailView, UpdateView, CreateView
 | 
			
		||||
from django.views.generic.list import ListView
 | 
			
		||||
from django.views.generic.base import RedirectView
 | 
			
		||||
from django.views.generic.edit import DeleteView
 | 
			
		||||
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 activity.models import Activity
 | 
			
		||||
from permission.backends import PermissionBackend
 | 
			
		||||
from permission.views import ProtectQuerysetMixin, ProtectedCreateView, LoginRequiredMixin
 | 
			
		||||
 | 
			
		||||
from .models import Food, BasicFood, TransformedFood, QRCode
 | 
			
		||||
from .models import Food, BasicFood, TransformedFood, QRCode, Order, Dish, Supplement
 | 
			
		||||
from .forms import QRCodeForms, BasicFoodForms, TransformedFoodForms, \
 | 
			
		||||
    ManageIngredientsForm, ManageIngredientsFormSet, AddIngredientForms, \
 | 
			
		||||
    BasicFoodUpdateForms, TransformedFoodUpdateForms
 | 
			
		||||
from .tables import FoodTable
 | 
			
		||||
    BasicFoodUpdateForms, TransformedFoodUpdateForms, \
 | 
			
		||||
    DishForm, SupplementFormSet, SupplementFormSetHelper, OrderForm
 | 
			
		||||
from .tables import FoodTable, DishTable, OrderTable
 | 
			
		||||
from .utils import pretty_duration
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -112,6 +117,9 @@ class FoodListView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin, Li
 | 
			
		||||
        context['club_tables'] = tables[3:]
 | 
			
		||||
 | 
			
		||||
        context['can_add_meal'] = PermissionBackend.check_perm(self.request, 'food.transformedfood_add')
 | 
			
		||||
 | 
			
		||||
        context["open_activities"] = Activity.objects.filter(activity_type__name="Perm bouffe", open=True)
 | 
			
		||||
 | 
			
		||||
        return context
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -299,11 +307,19 @@ class ManageIngredientsView(LoginRequiredMixin, UpdateView):
 | 
			
		||||
 | 
			
		||||
            elif form.data[prefix + 'name'] != '':
 | 
			
		||||
                ingredient = Food.objects.get(pk=form.data[prefix + 'name'])
 | 
			
		||||
                self.object.ingredients.add(ingredient)
 | 
			
		||||
                if (prefix + 'fully_used') in form.data and form.data[prefix + 'fully_used'] == 'on':
 | 
			
		||||
                    ingredient.end_of_life = _('Fully used in {meal}'.format(
 | 
			
		||||
                        meal=self.object.name))
 | 
			
		||||
                    ingredient.save()
 | 
			
		||||
                if form.data.get(prefix + 'add_all_same_name') == 'on':
 | 
			
		||||
                    ingredients = Food.objects.filter(name=ingredient.name, owner=ingredient.owner, end_of_life='')
 | 
			
		||||
                    for ingredient in ingredients:
 | 
			
		||||
                        self.object.ingredients.add(ingredient)
 | 
			
		||||
                        if form.data.get(prefix + 'fully_used') == 'on':
 | 
			
		||||
                            ingredient.end_of_life = _('Fully used in {meal}'.format(meal=self.object.name))
 | 
			
		||||
                            ingredient.save()
 | 
			
		||||
                else:
 | 
			
		||||
                    self.object.ingredients.add(ingredient)
 | 
			
		||||
                    if (prefix + 'fully_used') in form.data and form.data[prefix + 'fully_used'] == 'on':
 | 
			
		||||
                        ingredient.end_of_life = _('Fully used in {meal}'.format(
 | 
			
		||||
                            meal=self.object.name))
 | 
			
		||||
                        ingredient.save()
 | 
			
		||||
        # We recalculate new expiry date and allergens
 | 
			
		||||
        self.object.expiry_date = self.object.creation_date + self.object.shelf_life
 | 
			
		||||
        self.object.allergens.clear()
 | 
			
		||||
@@ -526,3 +542,328 @@ class QRCodeRedirectView(RedirectView):
 | 
			
		||||
        if slug:
 | 
			
		||||
            return reverse_lazy('food:qrcode_create', kwargs={'slug': slug})
 | 
			
		||||
        return reverse_lazy('food:list')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DishCreateView(ProtectQuerysetMixin, ProtectedCreateView):
 | 
			
		||||
    """
 | 
			
		||||
    Create a dish
 | 
			
		||||
    """
 | 
			
		||||
    model = Dish
 | 
			
		||||
    form_class = DishForm
 | 
			
		||||
    extra_context = {"title": _('Create dish')}
 | 
			
		||||
 | 
			
		||||
    def get_sample_object(self):
 | 
			
		||||
        activity = Activity.objects.get(pk=self.kwargs["activity_pk"])
 | 
			
		||||
        sample_food = TransformedFood(
 | 
			
		||||
            name="Sample food",
 | 
			
		||||
            owner=activity.organizer,
 | 
			
		||||
            expiry_date=timezone.now() + timedelta(days=7),
 | 
			
		||||
            is_ready=True,
 | 
			
		||||
        )
 | 
			
		||||
        sample_dish = Dish(
 | 
			
		||||
            main=sample_food,
 | 
			
		||||
            price=100,
 | 
			
		||||
            activity=activity,
 | 
			
		||||
        )
 | 
			
		||||
        return sample_dish
 | 
			
		||||
 | 
			
		||||
    def get_context_data(self, **kwargs):
 | 
			
		||||
        context = super().get_context_data(**kwargs)
 | 
			
		||||
 | 
			
		||||
        form = context['form']
 | 
			
		||||
        form.helper = FormHelper()
 | 
			
		||||
        # Remove form tag on the generation of the form in the template (already present on the template)
 | 
			
		||||
        form.helper.form_tag = False
 | 
			
		||||
        # The formset handles the set of the supplements
 | 
			
		||||
        form_set = SupplementFormSet(instance=form.instance)
 | 
			
		||||
        context['formset'] = form_set
 | 
			
		||||
        context['helper'] = SupplementFormSetHelper()
 | 
			
		||||
 | 
			
		||||
        return context
 | 
			
		||||
 | 
			
		||||
    def get_form(self, form_class=None):
 | 
			
		||||
        form = super().get_form(form_class)
 | 
			
		||||
        if "available" in form.fields:
 | 
			
		||||
            del form.fields["available"]
 | 
			
		||||
        return form
 | 
			
		||||
 | 
			
		||||
    @transaction.atomic
 | 
			
		||||
    def form_valid(self, form):
 | 
			
		||||
        activity = Activity.objects.get(pk=self.kwargs["activity_pk"])
 | 
			
		||||
 | 
			
		||||
        form.instance.activity = activity
 | 
			
		||||
 | 
			
		||||
        ret = super().form_valid(form)
 | 
			
		||||
 | 
			
		||||
        # For each supplement, we save it
 | 
			
		||||
        formset = SupplementFormSet(self.request.POST, instance=form.instance)
 | 
			
		||||
        if formset.is_valid():
 | 
			
		||||
            for f in formset:
 | 
			
		||||
                # We don't save the product if the price is not entered, ie. if the line is empty
 | 
			
		||||
                if f.is_valid() and f.instance.price:
 | 
			
		||||
                    f.save()
 | 
			
		||||
                    f.instance.save()
 | 
			
		||||
                else:
 | 
			
		||||
                    f.instance = None
 | 
			
		||||
 | 
			
		||||
        return ret
 | 
			
		||||
 | 
			
		||||
    def get_success_url(self):
 | 
			
		||||
        return reverse_lazy('food:dish_list', kwargs={"activity_pk": self.kwargs["activity_pk"]})
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DishListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
 | 
			
		||||
    """
 | 
			
		||||
    List dishes for this activity
 | 
			
		||||
    """
 | 
			
		||||
    model = Dish
 | 
			
		||||
    table_class = DishTable
 | 
			
		||||
    extra_context = {"title": _('Dishes served during')}
 | 
			
		||||
    template_name = 'food/dish_list.html'
 | 
			
		||||
 | 
			
		||||
    def get_queryset(self):
 | 
			
		||||
        return super().get_queryset().filter(activity__pk=self.kwargs["activity_pk"])
 | 
			
		||||
 | 
			
		||||
    def get_context_data(self, **kwargs):
 | 
			
		||||
        context = super().get_context_data(**kwargs)
 | 
			
		||||
 | 
			
		||||
        activity = Activity.objects.get(pk=self.kwargs["activity_pk"])
 | 
			
		||||
        context["activity"] = activity
 | 
			
		||||
 | 
			
		||||
        context["can_add_dish"] = PermissionBackend.check_perm(self.request, 'food.dish_add')
 | 
			
		||||
 | 
			
		||||
        return context
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DishDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
 | 
			
		||||
    """
 | 
			
		||||
    View a dish for this activity
 | 
			
		||||
    """
 | 
			
		||||
    model = Dish
 | 
			
		||||
    extra_context = {"title": _('Details of:')}
 | 
			
		||||
    context_oject_name = "dish"
 | 
			
		||||
 | 
			
		||||
    def get_context_data(self, **kwargs):
 | 
			
		||||
        context = super().get_context_data(**kwargs)
 | 
			
		||||
 | 
			
		||||
        context["food"] = self.object.main
 | 
			
		||||
 | 
			
		||||
        context["supplements"] = self.object.supplements.all()
 | 
			
		||||
 | 
			
		||||
        context["update"] = PermissionBackend.check_perm(self.request, "food.change_dish")
 | 
			
		||||
 | 
			
		||||
        context["delete"] = not Order.objects.filter(dish=self.get_object()).exists() and PermissionBackend.check_perm(self.request, "food.delete_dish")
 | 
			
		||||
 | 
			
		||||
        return context
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DishUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
 | 
			
		||||
    """
 | 
			
		||||
    A view to update a dish
 | 
			
		||||
    """
 | 
			
		||||
    model = Dish
 | 
			
		||||
    form_class = DishForm
 | 
			
		||||
    extra_context = {"title": _("Update a dish")}
 | 
			
		||||
 | 
			
		||||
    def get_context_data(self, **kwargs):
 | 
			
		||||
        context = super().get_context_data(**kwargs)
 | 
			
		||||
 | 
			
		||||
        form = context['form']
 | 
			
		||||
        form.helper = FormHelper()
 | 
			
		||||
        # Remove form tag on the generation of the form in the template (already present on the template)
 | 
			
		||||
        form.helper.form_tag = False
 | 
			
		||||
        # The formset handles the set of the supplements
 | 
			
		||||
        form_set = SupplementFormSet(instance=form.instance)
 | 
			
		||||
        context['formset'] = form_set
 | 
			
		||||
        context['helper'] = SupplementFormSetHelper()
 | 
			
		||||
 | 
			
		||||
        return context
 | 
			
		||||
 | 
			
		||||
    def get_form(self, form_class=None):
 | 
			
		||||
        form = super().get_form(form_class)
 | 
			
		||||
        if 'main' in form.fields:
 | 
			
		||||
            del form.fields["main"]
 | 
			
		||||
        return form
 | 
			
		||||
 | 
			
		||||
    @transaction.atomic
 | 
			
		||||
    def form_valid(self, form):
 | 
			
		||||
        activity = Activity.objects.get(pk=self.kwargs["activity_pk"])
 | 
			
		||||
 | 
			
		||||
        form.instance.activity = activity
 | 
			
		||||
 | 
			
		||||
        ret = super().form_valid(form)
 | 
			
		||||
 | 
			
		||||
        # For each supplement, we save it
 | 
			
		||||
        formset = SupplementFormSet(self.request.POST, instance=form.instance)
 | 
			
		||||
        saved = []
 | 
			
		||||
        if formset.is_valid():
 | 
			
		||||
            for f in formset:
 | 
			
		||||
                # We don't save the product if the price is not entered, ie. if the line is empty
 | 
			
		||||
                if f.is_valid() and f.instance.price:
 | 
			
		||||
                    f.save()
 | 
			
		||||
                    f.instance.save()
 | 
			
		||||
                    saved.append(f.instance.pk)
 | 
			
		||||
                else:
 | 
			
		||||
                    f.instance = None
 | 
			
		||||
        # Remove old supplements that weren't given in the form
 | 
			
		||||
        Supplement.objects.filter(~Q(pk__in=saved), dish=form.instance).delete()
 | 
			
		||||
 | 
			
		||||
        return ret
 | 
			
		||||
 | 
			
		||||
    def get_success_url(self):
 | 
			
		||||
        return reverse_lazy('food:dish_detail', kwargs={"activity_pk": self.kwargs["activity_pk"], "pk": self.kwargs["pk"]})
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DishDeleteView(ProtectQuerysetMixin, LoginRequiredMixin, DeleteView):
 | 
			
		||||
    """
 | 
			
		||||
    Delete a dish with no order yet
 | 
			
		||||
    """
 | 
			
		||||
    model = Dish
 | 
			
		||||
    extra_context = {"title": _('Delete dish')}
 | 
			
		||||
 | 
			
		||||
    def delete(self, request, *args, **kwargs):
 | 
			
		||||
        if Order.objects.filter(dish=self.get_object()).exists():
 | 
			
		||||
            raise PermissionDenied(_("This dish cannot be deleted because it has already been ordered"))
 | 
			
		||||
        return super().delete(request, *args, **kwargs)
 | 
			
		||||
 | 
			
		||||
    def get_success_url(self):
 | 
			
		||||
        return reverse_lazy('food:dish_list', kwargs={"activity_pk": self.kwargs["activity_pk"]})
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class OrderCreateView(ProtectQuerysetMixin, ProtectedCreateView):
 | 
			
		||||
    """
 | 
			
		||||
    Order a meal
 | 
			
		||||
    """
 | 
			
		||||
    model = Order
 | 
			
		||||
    form_class = OrderForm
 | 
			
		||||
    extra_context = {"title": _('Order food')}
 | 
			
		||||
 | 
			
		||||
    def get_sample_object(self):
 | 
			
		||||
        activity = Activity.objects.get(pk=self.kwargs["activity_pk"])
 | 
			
		||||
        sample_order = Order(
 | 
			
		||||
            user=self.request.user,
 | 
			
		||||
            activity=activity,
 | 
			
		||||
            dish=Dish.objects.filter(activity=activity).last(),
 | 
			
		||||
        )
 | 
			
		||||
        return sample_order
 | 
			
		||||
 | 
			
		||||
    def get_form(self):
 | 
			
		||||
        form = super().get_form()
 | 
			
		||||
 | 
			
		||||
        form.fields["user"].initial = self.request.user
 | 
			
		||||
        form.fields["user"].disabled = True
 | 
			
		||||
 | 
			
		||||
        return form
 | 
			
		||||
 | 
			
		||||
    def form_valid(self, form):
 | 
			
		||||
        activity = Activity.objects.get(pk=self.kwargs["activity_pk"])
 | 
			
		||||
 | 
			
		||||
        form.instance.activity = activity
 | 
			
		||||
 | 
			
		||||
        return super().form_valid(form)
 | 
			
		||||
 | 
			
		||||
    def get_success_url(self):
 | 
			
		||||
        return reverse_lazy('food:food_list')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class OrderListView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin, ListView):
 | 
			
		||||
    """
 | 
			
		||||
    List existing Families
 | 
			
		||||
    """
 | 
			
		||||
    model = Order
 | 
			
		||||
    table_class = OrderTable
 | 
			
		||||
    extra_context = {"title": _('Order list')}
 | 
			
		||||
    paginate_by = 10
 | 
			
		||||
 | 
			
		||||
    def get_queryset(self, **kwargs):
 | 
			
		||||
        activity = Activity.objects.get(pk=self.kwargs["activity_pk"])
 | 
			
		||||
        return Order.objects.filter(activity=activity).order_by('number')
 | 
			
		||||
 | 
			
		||||
    def get_tables(self):
 | 
			
		||||
        activity = Activity.objects.get(pk=self.kwargs["activity_pk"])
 | 
			
		||||
        dishes = Dish.objects.filter(activity=activity)
 | 
			
		||||
 | 
			
		||||
        tables = [OrderTable] * dishes.count()
 | 
			
		||||
        self.tables = tables
 | 
			
		||||
        tables = super().get_tables()
 | 
			
		||||
        for i in range(dishes.count()):
 | 
			
		||||
            tables[i].prefix = dishes[i].main.name
 | 
			
		||||
        return tables
 | 
			
		||||
 | 
			
		||||
    def get_tables_data(self):
 | 
			
		||||
        activity = Activity.objects.get(pk=self.kwargs["activity_pk"])
 | 
			
		||||
        dishes = Dish.objects.filter(activity=activity)
 | 
			
		||||
 | 
			
		||||
        tables = []
 | 
			
		||||
 | 
			
		||||
        for dish in dishes:
 | 
			
		||||
            tables.append(self.get_queryset().order_by('ordered_at').filter(
 | 
			
		||||
                dish=dish, served=False).filter(
 | 
			
		||||
                    PermissionBackend.filter_queryset(self.request, Order, 'view')
 | 
			
		||||
            ))
 | 
			
		||||
 | 
			
		||||
        return tables
 | 
			
		||||
 | 
			
		||||
    def get_context_data(self, **kwargs):
 | 
			
		||||
        context = super().get_context_data(**kwargs)
 | 
			
		||||
 | 
			
		||||
        context["activity"] = Activity.objects.get(pk=self.kwargs["activity_pk"])
 | 
			
		||||
 | 
			
		||||
        return context
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ServedOrderListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
 | 
			
		||||
    """
 | 
			
		||||
    View served orders
 | 
			
		||||
    """
 | 
			
		||||
    model = Order
 | 
			
		||||
    template_name = 'food/served_order_list.html'
 | 
			
		||||
    table_class = OrderTable
 | 
			
		||||
 | 
			
		||||
    def get_queryset(self):
 | 
			
		||||
        return super().get_queryset().filter(activity__pk=self.kwargs["activity_pk"], served=True).order_by('-served_at')
 | 
			
		||||
 | 
			
		||||
    def get_table(self, **kwargs):
 | 
			
		||||
        table = super().get_table(**kwargs)
 | 
			
		||||
 | 
			
		||||
        table.columns.hide("delete")
 | 
			
		||||
 | 
			
		||||
        return table
 | 
			
		||||
 | 
			
		||||
    def get_context_data(self, **kwargs):
 | 
			
		||||
        context = super().get_context_data(**kwargs)
 | 
			
		||||
 | 
			
		||||
        context["activity"] = Activity.objects.get(pk=self.kwargs["activity_pk"])
 | 
			
		||||
 | 
			
		||||
        return context
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class KitchenView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
 | 
			
		||||
    """
 | 
			
		||||
    The view to display useful information for the kitchen
 | 
			
		||||
    """
 | 
			
		||||
    model = Order
 | 
			
		||||
    table_class = OrderTable
 | 
			
		||||
    template_name = 'food/kitchen.html'
 | 
			
		||||
    extra_context = {'title': _('Kitchen')}
 | 
			
		||||
 | 
			
		||||
    def get_queryset(self):
 | 
			
		||||
        return super().get_queryset().filter(~Q(supplements__isnull=True, request=''), activity__pk=self.kwargs["activity_pk"])
 | 
			
		||||
 | 
			
		||||
    def get_context_data(self, **kwargs):
 | 
			
		||||
        context = super().get_context_data(**kwargs)
 | 
			
		||||
 | 
			
		||||
        orders_count = Order.objects.values('dish__main__name').annotate(quantity=Count('id'))
 | 
			
		||||
 | 
			
		||||
        context["orders"] = {o['dish__main__name']: o['quantity'] for o in orders_count}
 | 
			
		||||
 | 
			
		||||
        return context
 | 
			
		||||
 | 
			
		||||
    def get_table(self, **kwargs):
 | 
			
		||||
        table = super().get_table(**kwargs)
 | 
			
		||||
 | 
			
		||||
        hide = ["ordered_at", "serve", "delete"]
 | 
			
		||||
        for field in hide:
 | 
			
		||||
            table.columns.hide(field)
 | 
			
		||||
 | 
			
		||||
        return table
 | 
			
		||||
 
 | 
			
		||||
@@ -13,11 +13,14 @@ $(document).ready(function () {
 | 
			
		||||
    target.addClass('is-invalid')
 | 
			
		||||
    target.removeClass('is-valid')
 | 
			
		||||
 | 
			
		||||
    const isManageIngredients = target.hasClass('manageingredients-autocomplete')
 | 
			
		||||
 | 
			
		||||
    $.getJSON(api_url + (api_url.includes('?') ? '&' : '?') + 'format=json&search=^' + input + api_url_suffix, function (objects) {
 | 
			
		||||
      let html = '<ul class="list-group list-group-flush" id="' + prefix + '_list">'
 | 
			
		||||
 | 
			
		||||
      objects.results.forEach(function (obj) {
 | 
			
		||||
        html += li(prefix + '_' + obj.id, obj[name_field])
 | 
			
		||||
        const extra = isManageIngredients ? ` (${obj.owner_name})` : ''
 | 
			
		||||
        html += li(`${prefix}_${obj.id}`, `${obj[name_field]}${extra}`)
 | 
			
		||||
      })
 | 
			
		||||
      html += '</ul>'
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user