mirror of
https://gitlab.crans.org/bde/nk20
synced 2025-11-15 11:07:46 +01:00
Compare commits
12 Commits
note_sheet
...
04001202f2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
04001202f2 | ||
|
|
af36d1427a | ||
|
|
75a59e0a7a | ||
|
|
af39bf7068 | ||
|
|
4c40566513 | ||
|
|
7c45b59298 | ||
|
|
418268db27 | ||
|
|
73045586a3 | ||
|
|
22d668a75c | ||
|
|
5dfa12fad2 | ||
|
|
5af69f719d | ||
|
|
4f6b1d5b6c |
@@ -48,15 +48,5 @@
|
||||
"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,10 +66,6 @@ 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, Dish, Supplement, Order, FoodTransaction
|
||||
from ..models import Allergen, Food, BasicFood, TransformedFood, QRCode
|
||||
|
||||
|
||||
class AllergenSerializer(serializers.ModelSerializer):
|
||||
@@ -21,13 +21,9 @@ 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 = ['id', 'name', 'owner', 'allergens', 'expiry_date', 'end_of_life', 'is_ready', 'order', 'owner_name']
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class BasicFoodSerializer(serializers.ModelSerializer):
|
||||
@@ -58,43 +54,3 @@ 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,8 +1,7 @@
|
||||
# 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, \
|
||||
DishViewSet, SupplementViewSet, OrderViewSet, FoodTransactionViewSet
|
||||
from .views import AllergenViewSet, FoodViewSet, BasicFoodViewSet, TransformedFoodViewSet, QRCodeViewSet
|
||||
|
||||
|
||||
def register_food_urls(router, path):
|
||||
@@ -14,7 +13,3 @@ 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,9 +5,8 @@ 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, \
|
||||
DishSerializer, SupplementSerializer, OrderSerializer, FoodTransactionSerializer
|
||||
from ..models import Allergen, Food, BasicFood, TransformedFood, QRCode, Dish, Supplement, Order, FoodTransaction
|
||||
from .serializers import AllergenSerializer, FoodSerializer, BasicFoodSerializer, TransformedFoodSerializer, QRCodeSerializer
|
||||
from ..models import Allergen, Food, BasicFood, TransformedFood, QRCode
|
||||
|
||||
|
||||
class AllergenViewSet(ReadProtectedModelViewSet):
|
||||
@@ -73,55 +72,3 @@ 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,17 +4,15 @@
|
||||
from random import shuffle
|
||||
|
||||
from bootstrap_datepicker_plus.widgets import DateTimePickerInput
|
||||
from crispy_forms.helper import FormHelper
|
||||
from django import forms
|
||||
from django.forms import CheckboxSelectMultiple
|
||||
from django.forms.widgets import NumberInput, TextInput
|
||||
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, AmountInput
|
||||
from note_kfet.inputs import Autocomplete
|
||||
from note_kfet.middlewares import get_current_request
|
||||
from permission.backends import PermissionBackend
|
||||
|
||||
from .models import Food, BasicFood, TransformedFood, QRCode, Dish, Supplement, Order, Recipe
|
||||
from .models import Food, BasicFood, TransformedFood, QRCode
|
||||
|
||||
|
||||
class QRCodeForms(forms.ModelForm):
|
||||
@@ -56,7 +54,7 @@ class BasicFoodForms(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = BasicFood
|
||||
fields = ('name', 'owner', 'date_type', 'expiry_date', 'allergens', 'traces', 'order',)
|
||||
fields = ('name', 'owner', 'date_type', 'expiry_date', 'allergens', 'order',)
|
||||
widgets = {
|
||||
"owner": Autocomplete(
|
||||
model=Club,
|
||||
@@ -99,7 +97,7 @@ class BasicFoodUpdateForms(forms.ModelForm):
|
||||
"""
|
||||
class Meta:
|
||||
model = BasicFood
|
||||
fields = ('name', 'owner', 'date_type', 'expiry_date', 'end_of_life', 'is_ready', 'order', 'allergens', 'traces')
|
||||
fields = ('name', 'owner', 'date_type', 'expiry_date', 'end_of_life', 'is_ready', 'order', 'allergens')
|
||||
widgets = {
|
||||
"owner": Autocomplete(
|
||||
model=Club,
|
||||
@@ -135,7 +133,7 @@ class AddIngredientForms(forms.ModelForm):
|
||||
Form for add an ingredient
|
||||
"""
|
||||
fully_used = forms.BooleanField()
|
||||
fully_used.initial = False
|
||||
fully_used.initial = True
|
||||
fully_used.required = False
|
||||
fully_used.label = _("Fully used")
|
||||
|
||||
@@ -143,14 +141,11 @@ class AddIngredientForms(forms.ModelForm):
|
||||
super().__init__(*args, **kwargs)
|
||||
# TODO find a better way to get pk (be not url scheme dependant)
|
||||
pk = get_current_request().path.split('/')[-1]
|
||||
qs = self.fields['ingredients'].queryset.filter(
|
||||
self.fields['ingredients'].queryset = self.fields['ingredients'].queryset.filter(
|
||||
polymorphic_ctype__model="transformedfood",
|
||||
is_ready=False,
|
||||
end_of_life='',
|
||||
).filter(PermissionBackend.filter_queryset(get_current_request(), Food, "change"))
|
||||
if pk:
|
||||
qs = qs.exclude(pk=pk)
|
||||
self.fields['ingredients'].queryset = qs
|
||||
).filter(PermissionBackend.filter_queryset(get_current_request(), Food, "change")).exclude(pk=pk)
|
||||
|
||||
class Meta:
|
||||
model = TransformedFood
|
||||
@@ -162,7 +157,7 @@ class ManageIngredientsForm(forms.Form):
|
||||
Form to manage ingredient
|
||||
"""
|
||||
fully_used = forms.BooleanField()
|
||||
fully_used.initial = False
|
||||
fully_used.initial = True
|
||||
fully_used.required = True
|
||||
fully_used.label = _('Fully used')
|
||||
|
||||
@@ -171,7 +166,7 @@ class ManageIngredientsForm(forms.Form):
|
||||
model=Food,
|
||||
resetable=True,
|
||||
attrs={"api_url": "/api/food/food",
|
||||
"class": "autocomplete manageingredients-autocomplete"},
|
||||
"class": "autocomplete"},
|
||||
)
|
||||
name.label = _('Name')
|
||||
|
||||
@@ -185,110 +180,8 @@ 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")
|
||||
|
||||
|
||||
class RecipeForm(forms.ModelForm):
|
||||
"""
|
||||
Form to create a recipe
|
||||
"""
|
||||
class Meta:
|
||||
model = Recipe
|
||||
fields = ('name',)
|
||||
|
||||
|
||||
class RecipeIngredientsForm(forms.Form):
|
||||
"""
|
||||
Form to add ingredients to a recipe
|
||||
"""
|
||||
name = forms.CharField()
|
||||
name.widget = TextInput()
|
||||
name.label = _("Name")
|
||||
|
||||
|
||||
RecipeIngredientsFormSet = forms.formset_factory(
|
||||
RecipeIngredientsForm,
|
||||
extra=1,
|
||||
)
|
||||
|
||||
|
||||
class UseRecipeForm(forms.Form):
|
||||
"""
|
||||
Form to add ingredients to a TransformedFood using a Recipe
|
||||
"""
|
||||
recipe = forms.ModelChoiceField(
|
||||
queryset=Recipe.objects,
|
||||
label=_('Recipe'),
|
||||
)
|
||||
|
||||
ingredients = forms.ModelMultipleChoiceField(
|
||||
queryset=Food.objects,
|
||||
label=_("Ingredients"),
|
||||
widget=CheckboxSelectMultiple(),
|
||||
)
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
# 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'),
|
||||
),
|
||||
]
|
||||
@@ -1,86 +0,0 @@
|
||||
# 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')},
|
||||
),
|
||||
]
|
||||
@@ -1,19 +0,0 @@
|
||||
# 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'),
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 5.2.6 on 2025-11-02 17:38
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('food', '0004_alter_foodtransaction_order'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='food',
|
||||
name='traces',
|
||||
field=models.ManyToManyField(blank=True, related_name='food_with_traces', to='food.allergen', verbose_name='traces'),
|
||||
),
|
||||
]
|
||||
@@ -1,29 +0,0 @@
|
||||
# Generated by Django 5.2.6 on 2025-11-06 17:02
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('food', '0005_food_traces'),
|
||||
('member', '0015_alter_profile_promotion'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Recipe',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255, verbose_name='name')),
|
||||
('ingredients_json', models.TextField(blank=True, default='[]', help_text='Ingredients of the recipe, encoded in JSON', verbose_name='list of ingredients')),
|
||||
('creater', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='member.club', verbose_name='creater')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Recipe',
|
||||
'verbose_name_plural': 'Recipes',
|
||||
'unique_together': {('name', 'creater')},
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -1,18 +1,13 @@
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import json
|
||||
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):
|
||||
@@ -54,13 +49,6 @@ class Food(PolymorphicModel):
|
||||
verbose_name=_('allergens'),
|
||||
)
|
||||
|
||||
traces = models.ManyToManyField(
|
||||
Allergen,
|
||||
blank=True,
|
||||
verbose_name=_('traces'),
|
||||
related_name='food_with_traces'
|
||||
)
|
||||
|
||||
expiry_date = models.DateTimeField(
|
||||
verbose_name=_('expiry date'),
|
||||
null=False,
|
||||
@@ -99,19 +87,6 @@ class Food(PolymorphicModel):
|
||||
if old_allergens != list(parent.allergens.all()):
|
||||
parent.save(old_allergens=old_allergens)
|
||||
|
||||
@transaction.atomic
|
||||
def update_traces(self):
|
||||
# update parents
|
||||
for parent in self.transformed_ingredient_inv.iterator():
|
||||
old_traces = list(parent.traces.all()).copy()
|
||||
parent.traces.clear()
|
||||
for child in parent.ingredients.iterator():
|
||||
if child.pk != self.pk:
|
||||
parent.traces.set(parent.traces.union(child.traces.all()))
|
||||
parent.traces.set(parent.traces.union(self.traces.all()))
|
||||
if old_traces != list(parent.traces.all()):
|
||||
parent.save(old_traces=old_traces)
|
||||
|
||||
def update_expiry_date(self):
|
||||
# update parents
|
||||
for parent in self.transformed_ingredient_inv.iterator():
|
||||
@@ -163,10 +138,6 @@ class BasicFood(Food):
|
||||
and list(self.allergens.all()) != kwargs['old_allergens']):
|
||||
self.update_allergens()
|
||||
|
||||
if ('old_traces' in kwargs
|
||||
and list(self.traces.all()) != kwargs['old_traces']):
|
||||
self.update_traces()
|
||||
|
||||
# Expiry date
|
||||
if ((self.expiry_date != old_food.expiry_date
|
||||
and self.date_type == 'DLC')
|
||||
@@ -239,7 +210,7 @@ class TransformedFood(Food):
|
||||
created = self.pk is None
|
||||
if not created:
|
||||
# Check if important fields are updated
|
||||
update = {'allergens': False, 'traces': False, 'expiry_date': False}
|
||||
update = {'allergens': False, 'expiry_date': False}
|
||||
old_food = Food.objects.select_for_update().get(pk=self.pk)
|
||||
if not hasattr(self, "_force_save"):
|
||||
# Allergens
|
||||
@@ -249,10 +220,6 @@ class TransformedFood(Food):
|
||||
and list(self.allergens.all()) != kwargs['old_allergens']):
|
||||
update['allergens'] = True
|
||||
|
||||
if ('old_traces' in kwargs
|
||||
and list(self.traces.all()) != kwargs['old_traces']):
|
||||
update['traces'] = True
|
||||
|
||||
# Expiry date
|
||||
update['expiry_date'] = (self.shelf_life != old_food.shelf_life
|
||||
or self.creation_date != old_food.creation_date)
|
||||
@@ -263,7 +230,6 @@ class TransformedFood(Food):
|
||||
if ('old_ingredients' in kwargs
|
||||
and list(self.ingredients.all()) != list(kwargs['old_ingredients'])):
|
||||
update['allergens'] = True
|
||||
update['traces'] = True
|
||||
update['expiry_date'] = True
|
||||
|
||||
# it's preferable to keep a queryset but we allow list too
|
||||
@@ -273,8 +239,6 @@ class TransformedFood(Food):
|
||||
self.check_cycle(self.ingredients.all().difference(kwargs['old_ingredients']), self, [])
|
||||
if update['allergens']:
|
||||
self.update_allergens()
|
||||
if update['traces']:
|
||||
self.update_traces()
|
||||
if update['expiry_date']:
|
||||
self.update_expiry_date()
|
||||
|
||||
@@ -286,10 +250,9 @@ class TransformedFood(Food):
|
||||
|
||||
for child in self.ingredients.iterator():
|
||||
self.allergens.set(self.allergens.union(child.allergens.all()))
|
||||
self.traces.set(self.traces.union(child.traces.all()))
|
||||
if not (child.polymorphic_ctype.model == 'basicfood' and child.date_type == 'DDM'):
|
||||
self.expiry_date = min(self.expiry_date, child.expiry_date)
|
||||
return super().save(force_insert=False, force_update=force_update, using=using, update_fields=update_fields)
|
||||
return super().save(force_insert, force_update, using, update_fields)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Transformed food')
|
||||
@@ -321,242 +284,3 @@ 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,
|
||||
reason=str(self.dish),
|
||||
)
|
||||
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)
|
||||
|
||||
|
||||
class Recipe(models.Model):
|
||||
"""
|
||||
A recipe is a list of ingredients one can use to easily create a recurrent TransformedFood
|
||||
"""
|
||||
name = models.CharField(
|
||||
verbose_name=_("name"),
|
||||
max_length=255,
|
||||
)
|
||||
|
||||
ingredients_json = models.TextField(
|
||||
blank=True,
|
||||
default="[]",
|
||||
verbose_name=_("list of ingredients"),
|
||||
help_text=_("Ingredients of the recipe, encoded in JSON")
|
||||
)
|
||||
|
||||
creater = models.ForeignKey(
|
||||
Club,
|
||||
on_delete=models.CASCADE,
|
||||
verbose_name=_("creater"),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Recipe")
|
||||
verbose_name_plural = _("Recipes")
|
||||
unique_together = ('name', 'creater',)
|
||||
|
||||
def __str__(self):
|
||||
return "{name} ({creater})".format(name=self.name, creater=str(self.creater))
|
||||
|
||||
@property
|
||||
def ingredients(self):
|
||||
"""
|
||||
Ingredients are stored in a JSON string
|
||||
"""
|
||||
return json.loads(self.ingredients_json)
|
||||
|
||||
@ingredients.setter
|
||||
def ingredients(self, ingredients):
|
||||
"""
|
||||
Store ingredients as JSON string
|
||||
"""
|
||||
self.ingredients_json = json.dumps(ingredients, indent=2)
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
/**
|
||||
* 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,11 +3,8 @@
|
||||
|
||||
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, Dish, Order, Recipe
|
||||
from .models import Food
|
||||
|
||||
|
||||
class FoodTable(tables.Table):
|
||||
@@ -32,104 +29,9 @@ class FoodTable(tables.Table):
|
||||
class Meta:
|
||||
model = Food
|
||||
template_name = 'django_tables2/bootstrap4.html'
|
||||
fields = ('name', 'owner', 'qr_code_numbers', 'allergens', 'traces', 'date', 'expiry_date')
|
||||
fields = ('name', 'owner', 'qr_code_numbers', 'allergens', 'date', 'expiry_date')
|
||||
row_attrs = {
|
||||
'class': 'table-row',
|
||||
'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',
|
||||
}
|
||||
|
||||
|
||||
class RecipeTable(tables.Table):
|
||||
"""
|
||||
List all recipes
|
||||
"""
|
||||
def render_ingredients(self, record):
|
||||
return ", ".join(str(q) for q in record.ingredients)
|
||||
|
||||
class Meta:
|
||||
model = Recipe
|
||||
template_name = 'django_tables2/bootstrap4.html'
|
||||
fields = ('name', 'creater', 'ingredients',)
|
||||
row_attrs = {
|
||||
'class': 'table-row',
|
||||
'data-href': lambda record: str(record.pk),
|
||||
'style': 'cursor:pointer',
|
||||
}
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
{% 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 %}
|
||||
@@ -1,44 +0,0 @@
|
||||
{% 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 %}
|
||||
@@ -1,94 +0,0 @@
|
||||
{% 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 %}
|
||||
@@ -1,33 +0,0 @@
|
||||
{% 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 %}
|
||||
@@ -47,9 +47,6 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
<a class="btn btn-sm btn-secondary" href="{% url "food:manage_ingredients" pk=food.pk %}">
|
||||
{% trans "Manage ingredients" %}
|
||||
</a>
|
||||
<a class="btn btn-sm btn-secondary" href="{% url "food:recipe_use" pk=food.pk %}">
|
||||
{% trans "Use a recipe" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
<a class="btn btn-sm btn-primary" href="{% url "food:food_list" %}">
|
||||
{% trans "Return to the food list" %}
|
||||
|
||||
@@ -64,29 +64,13 @@ 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 %}
|
||||
{% if can_view_recipes %}
|
||||
<a class="btn btn-sm btn-secondary" href="{% url 'food:recipe_list' %}">
|
||||
{% trans "View recipes" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if can_add_recipe %}
|
||||
<a class="btn btn-sm btn-primary" href="{% url 'food:recipe_create' %}">
|
||||
{% trans "New recipe" %}
|
||||
</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 %}
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
{% 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,7 +22,6 @@ 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">
|
||||
@@ -35,7 +34,6 @@ 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>
|
||||
@@ -90,7 +88,7 @@ function delete_form_data (form_id) {
|
||||
document.getElementById(prefix + "name").value = "";
|
||||
document.getElementById(prefix + "qrcode_pk").value = "";
|
||||
document.getElementById(prefix + "qrcode").value = "";
|
||||
document.getElementById(prefix + "fully_used").checked = false;
|
||||
document.getElementById(prefix + "fully_used").checked = true;
|
||||
}
|
||||
var form_count = {{ ingredients_count }} + 1;
|
||||
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
{% 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 %}
|
||||
@@ -1,21 +0,0 @@
|
||||
{% 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 %}
|
||||
@@ -1,30 +0,0 @@
|
||||
{% 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%}
|
||||
@@ -1,31 +0,0 @@
|
||||
{% 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 }} {{ recipe.name }}
|
||||
</h3>
|
||||
<div class="card-body">
|
||||
<ul>
|
||||
<li> {% trans "Creater" %} : {{ recipe.creater }}</li>
|
||||
<li> {% trans "Ingredients" %} :
|
||||
{% for ingredient in ingredients %} {{ ingredient }}{% if not forloop.last %},{% endif %}{% endfor %}
|
||||
</li>
|
||||
</ul>
|
||||
{% if update %}
|
||||
<a class="btn btn-sm btn-secondary" href="{% url "food:recipe_update" pk=recipe.pk %}">
|
||||
{% trans "Update" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
<a class="btn btn-sm btn-primary" href="{% url "food:recipe_list" %}">
|
||||
{% trans "Return to recipe list" %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,122 +0,0 @@
|
||||
{% 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="" id="recipe_form">
|
||||
{% csrf_token %}
|
||||
<div class="card-body">
|
||||
{% crispy recipe_form %}
|
||||
{# Keep all form elements in the same card-body for proper structure #}
|
||||
{{ formset.management_form }}
|
||||
<h3 class="text-center mt-4">{% trans "Add ingredients" %}</h3>
|
||||
<table class="table table-condensed table-striped">
|
||||
{% for form in formset %}
|
||||
{% if forloop.first %}
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ form.name.label }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="form_body">
|
||||
{% endif %}
|
||||
<tr class="row-formset ingredients">
|
||||
<td>
|
||||
{# Force prefix on the form fields #}
|
||||
{{ form.name.as_widget }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{# Display buttons to add and remove ingredients #}
|
||||
<div class="card-body">
|
||||
<div class="btn-group btn-block" role="group">
|
||||
<button type="button" id="add_more" class="btn btn-success">{% trans "Add ingredient" %}</button>
|
||||
<button type="button" id="remove_one" class="btn btn-danger">{% trans "Remove ingredient" %}</button>
|
||||
</div>
|
||||
<button class="btn btn-primary" type="submit" form="recipe_form">{% 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.name }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block extrajavascript %}
|
||||
<script>
|
||||
/* script that handles add and remove lines */
|
||||
$(document).ready(function() {
|
||||
const totalFormsInput = $('input[name$="-TOTAL_FORMS"]');
|
||||
const initialFormsInput = $('input[name$="-INITIAL_FORMS"]');
|
||||
|
||||
function updateTotalForms(n) {
|
||||
if (totalFormsInput.length) {
|
||||
totalFormsInput.val(n);
|
||||
}
|
||||
}
|
||||
|
||||
const initialCount = $('#form_body .row-formset').length;
|
||||
updateTotalForms(initialCount);
|
||||
|
||||
const foods = {{ ingredients | safe }};
|
||||
|
||||
function prepopulate () {
|
||||
for (var i = 0; i < {{ ingredients_count }}; i++) {
|
||||
let prefix = 'id_form-' + parseInt(i) + '-';
|
||||
document.getElementById(prefix + 'name').value = foods[i]['name'];
|
||||
};
|
||||
}
|
||||
prepopulate();
|
||||
|
||||
$('#add_more').click(function() {
|
||||
let formIdx = totalFormsInput.length ? parseInt(totalFormsInput.val(), 10) : $('#form_body .row-formset').length;
|
||||
let newForm = $('#for_real').html().replace(/__prefix__/g, formIdx);
|
||||
$('#form_body').append(newForm);
|
||||
updateTotalForms(formIdx + 1);
|
||||
});
|
||||
|
||||
$('#remove_one').click(function() {
|
||||
let formIdx = totalFormsInput.length ? parseInt(totalFormsInput.val(), 10) : $('#form_body .row-formset').length;
|
||||
if (formIdx > 1) {
|
||||
$('#form_body tr.row-formset:last').remove();
|
||||
updateTotalForms(formIdx - 1);
|
||||
}
|
||||
});
|
||||
|
||||
$('#recipe_form').on('submit', function() {
|
||||
const totalInput = $('input[name$="-TOTAL_FORMS"]');
|
||||
const prefix = totalInput.length ? totalInput.attr('name').replace(/-TOTAL_FORMS$/, '') : 'form';
|
||||
|
||||
$('#form_body tr.row-formset').each(function(i) {
|
||||
const input = $(this).find('input,select,textarea').first();
|
||||
if (input.length) {
|
||||
const newName = `${prefix}-${i}-name`;
|
||||
input.attr('name', newName).attr('id', `id_${newName}`).prop('disabled', false);
|
||||
}
|
||||
});
|
||||
|
||||
const visibleCount = $('#form_body tr.row-formset').length;
|
||||
if (totalInput.length) totalInput.val(visibleCount);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -1,32 +0,0 @@
|
||||
{% 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 }}
|
||||
</h3>
|
||||
{% render_table table %}
|
||||
<div class="card-footer">
|
||||
{% if can_add_recipe %}
|
||||
<a class="btn btn-sm btn-success" href="{% url 'food:recipe_create' %}">{% trans "New recipe" %}</a>
|
||||
{% endif %}
|
||||
<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 %}
|
||||
@@ -1,21 +0,0 @@
|
||||
{% 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%}
|
||||
@@ -1,17 +0,0 @@
|
||||
{% 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 %}
|
||||
@@ -1,80 +0,0 @@
|
||||
{% 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 }} {{ object.name }}
|
||||
</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 %}
|
||||
|
||||
{% block extrajavascript %}
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
function refreshIngredients() {
|
||||
// 1️⃣ on récupère l'id de la recette sélectionnée
|
||||
let recipe_id = $("#id_recipe").val() || $("input[name='recipe']:checked").val();
|
||||
|
||||
if (!recipe_id) {
|
||||
// 2️⃣ rien sélectionné → on vide la zone d'ingrédients
|
||||
$("#div_id_ingredients > div").empty().html("<em>Aucune recette sélectionnée</em>");
|
||||
return;
|
||||
}
|
||||
|
||||
// 3️⃣ on interroge le serveur
|
||||
$.getJSON("{% url 'food:get_ingredients' %}", { recipe_id: recipe_id })
|
||||
.done(function (data) {
|
||||
|
||||
// 4️⃣ on cible le bon conteneur
|
||||
const $container = $("#div_id_ingredients > div");
|
||||
$container.empty();
|
||||
|
||||
if (data.ingredients && data.ingredients.length > 0) {
|
||||
// 5️⃣ on crée les cases à cocher
|
||||
data.ingredients.forEach(function (ing, i) {
|
||||
const html = `
|
||||
<div class="form-check">
|
||||
<input type="checkbox"
|
||||
name="ingredients"
|
||||
value="${ing.id}"
|
||||
id="id_ingredients_${i}"
|
||||
class="form-check-input"
|
||||
checked>
|
||||
<label class="form-check-label" for="id_ingredients_${i}">
|
||||
${ing.name} (${ing.qr_code_numbers})
|
||||
</label>
|
||||
</div>
|
||||
`;
|
||||
$container.append(html);
|
||||
});
|
||||
} else {
|
||||
$container.html("<em>Aucun ingrédient trouvé</em>");
|
||||
}
|
||||
})
|
||||
.fail(function (xhr) {
|
||||
console.error("Erreur AJAX:", xhr);
|
||||
$("#div_id_ingredients > div").html("<em>Erreur de chargement des ingrédients</em>");
|
||||
});
|
||||
}
|
||||
|
||||
// 6️⃣ déclenche quand la recette change
|
||||
$("#id_recipe, input[name='recipe']").change(refreshIngredients);
|
||||
|
||||
// 7️⃣ initial
|
||||
refreshIngredients();
|
||||
});
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
@@ -6,12 +6,9 @@ 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, \
|
||||
DishViewSet, SupplementViewSet, OrderViewSet, FoodTransactionViewSet
|
||||
from ..models import Allergen, BasicFood, TransformedFood, QRCode, Dish, Supplement, Order, FoodTransaction
|
||||
from ..api.views import AllergenViewSet, BasicFoodViewSet, TransformedFoodViewSet, QRCodeViewSet
|
||||
from ..models import Allergen, BasicFood, TransformedFood, QRCode
|
||||
|
||||
|
||||
class TestFood(TestCase):
|
||||
@@ -56,293 +53,73 @@ 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', kwargs={"slug": 2}))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
def test_qrcode_create(self):
|
||||
"""
|
||||
Display QRCode creation
|
||||
"""
|
||||
response = self.client.get(reverse('food:qrcode_create'))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_basicfood_create(self):
|
||||
"""
|
||||
Display BasicFood creation
|
||||
"""
|
||||
response = self.client.get(reverse('food:basicfood_create', 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_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_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_create(self):
|
||||
"""
|
||||
Display Food update
|
||||
"""
|
||||
response = self.client.get(reverse('food:food_update'))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_food_view(self):
|
||||
"""
|
||||
Display Food detail
|
||||
"""
|
||||
response = self.client.get(reverse('food:food_view', args=(self.basicfood.pk,)))
|
||||
self.assertEqual(response.status_code, 302)
|
||||
def test_food_view(self):
|
||||
"""
|
||||
Display Food detail
|
||||
"""
|
||||
response = self.client.get(reverse('food:food_view'))
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
def test_basicfood_view(self):
|
||||
"""
|
||||
Display BasicFood detail
|
||||
"""
|
||||
response = self.client.get(reverse('food:basicfood_view', args=(self.basicfood.pk,)))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
def test_basicfood_view(self):
|
||||
"""
|
||||
Display BasicFood detail
|
||||
"""
|
||||
response = self.client.get(reverse('food:basicfood_view'))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_transformedfood_view(self):
|
||||
"""
|
||||
Display TransformedFood detail
|
||||
"""
|
||||
response = self.client.get(reverse('food:transformedfood_view', args=(self.transformedfood.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_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())
|
||||
def test_add_ingredient(self):
|
||||
"""
|
||||
Display add ingredient view
|
||||
"""
|
||||
response = self.client.get(reverse('food:add_ingredient'))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
class TestFoodAPI(TestAPI):
|
||||
def setUp(self) -> None:
|
||||
super().setUp()
|
||||
super().setUP()
|
||||
|
||||
self.allergen = Allergen.objects.create(
|
||||
name='name',
|
||||
@@ -368,84 +145,26 @@ class TestFoodAPI(TestAPI):
|
||||
food_container=self.basicfood,
|
||||
)
|
||||
|
||||
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_allergen_api(self):
|
||||
"""
|
||||
Load Allergen API page and test all filters and permissions
|
||||
"""
|
||||
self.check_viewset(AllergenViewSet, '/api/food/allergen/')
|
||||
|
||||
self.dish = Dish.objects.create(
|
||||
main=self.transformedfood,
|
||||
price=500,
|
||||
activity=self.activity,
|
||||
available=True,
|
||||
)
|
||||
def test_basicfood_api(self):
|
||||
"""
|
||||
Load BasicFood API page and test all filters and permissions
|
||||
"""
|
||||
self.check_viewset(BasicFoodViewSet, '/api/food/basicfood/')
|
||||
|
||||
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_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/')
|
||||
def test_qrcode_api(self):
|
||||
"""
|
||||
Load QRCode API page and test all filters and permissions
|
||||
"""
|
||||
self.check_viewset(QRCodeViewSet, '/api/food/qrcode/')
|
||||
|
||||
@@ -9,30 +9,14 @@ app_name = 'food'
|
||||
|
||||
urlpatterns = [
|
||||
path('', views.FoodListView.as_view(), name='food_list'),
|
||||
path('<int:slug>/', views.QRCodeCreateView.as_view(), name='qrcode_create'),
|
||||
path('<int:slug>/add/basic/', views.BasicFoodCreateView.as_view(), name='basicfood_create'),
|
||||
path('add/transformed/', views.TransformedFoodCreateView.as_view(), name='transformedfood_create'),
|
||||
path('update/<int:pk>/', views.FoodUpdateView.as_view(), name='food_update'),
|
||||
path('update/ingredients/<int:pk>/', views.ManageIngredientsView.as_view(), name='manage_ingredients'),
|
||||
path('detail/<int:pk>/', views.FoodDetailView.as_view(), name='food_view'),
|
||||
path('detail/basic/<int:pk>/', views.BasicFoodDetailView.as_view(), name='basicfood_view'),
|
||||
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('<int:slug>', views.QRCodeCreateView.as_view(), name='qrcode_create'),
|
||||
path('<int:slug>/add/basic', views.BasicFoodCreateView.as_view(), name='basicfood_create'),
|
||||
path('add/transformed', views.TransformedFoodCreateView.as_view(), name='transformedfood_create'),
|
||||
path('update/<int:pk>', views.FoodUpdateView.as_view(), name='food_update'),
|
||||
path('update/ingredients/<int:pk>', views.ManageIngredientsView.as_view(), name='manage_ingredients'),
|
||||
path('detail/<int:pk>', views.FoodDetailView.as_view(), name='food_view'),
|
||||
path('detail/basic/<int:pk>', views.BasicFoodDetailView.as_view(), name='basicfood_view'),
|
||||
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'),
|
||||
path('recipe/add/', views.RecipeCreateView.as_view(), name='recipe_create'),
|
||||
path('recipe/', views.RecipeListView.as_view(), name='recipe_list'),
|
||||
path('recipe/<int:pk>/', views.RecipeDetailView.as_view(), name='recipe_detail'),
|
||||
path('recipe/<int:pk>/update/', views.RecipeUpdateView.as_view(), name='recipe_update'),
|
||||
path('update/ingredients/<int:pk>/recipe/', views.UseRecipeView.as_view(), name='recipe_use'),
|
||||
path('ajax/get_ingredients/', views.get_ingredients_for_recipe, name='get_ingredients'),
|
||||
]
|
||||
|
||||
@@ -4,32 +4,25 @@
|
||||
from datetime import timedelta
|
||||
|
||||
from api.viewsets import is_regex
|
||||
from crispy_forms.helper import FormHelper
|
||||
from django_tables2.views import SingleTableView, MultiTableMixin
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django_tables2.views import MultiTableMixin
|
||||
from django.db import transaction
|
||||
from django.db.models import Q, Count
|
||||
from django.http import HttpResponseRedirect, Http404, JsonResponse
|
||||
from django.views.decorators.http import require_GET
|
||||
from django.db.models import Q
|
||||
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, Order, Dish, Supplement, Recipe
|
||||
from .models import Food, BasicFood, TransformedFood, QRCode
|
||||
from .forms import QRCodeForms, BasicFoodForms, TransformedFoodForms, \
|
||||
ManageIngredientsForm, ManageIngredientsFormSet, AddIngredientForms, \
|
||||
BasicFoodUpdateForms, TransformedFoodUpdateForms, \
|
||||
DishForm, SupplementFormSet, SupplementFormSetHelper, OrderForm, RecipeForm, \
|
||||
RecipeIngredientsForm, RecipeIngredientsFormSet, UseRecipeForm
|
||||
from .tables import FoodTable, DishTable, OrderTable, RecipeTable
|
||||
BasicFoodUpdateForms, TransformedFoodUpdateForms
|
||||
from .tables import FoodTable
|
||||
from .utils import pretty_duration
|
||||
|
||||
|
||||
@@ -81,11 +74,15 @@ class FoodListView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin, Li
|
||||
|
||||
search_table = qs.filter(PermissionBackend.filter_queryset(self.request, Food, 'view'))
|
||||
# table open
|
||||
open_table = self.get_queryset().order_by('expiry_date').filter(
|
||||
open_table = self.get_queryset().filter(
|
||||
Q(polymorphic_ctype__model='transformedfood')
|
||||
| Q(polymorphic_ctype__model='basicfood', basicfood__date_type='DLC')).filter(
|
||||
expiry_date__lt=timezone.now(), end_of_life='').filter(
|
||||
PermissionBackend.filter_queryset(self.request, Food, 'view'))
|
||||
open_table = open_table.union(self.get_queryset().filter(
|
||||
Q(end_of_life='', order__iexact='open')
|
||||
).filter(
|
||||
PermissionBackend.filter_queryset(self.request, Food, 'view'))).order_by('expiry_date')
|
||||
# table served
|
||||
served_table = self.get_queryset().order_by('-pk').filter(
|
||||
end_of_life='', is_ready=True).exclude(
|
||||
@@ -119,13 +116,6 @@ class FoodListView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin, Li
|
||||
context['club_tables'] = tables[3:]
|
||||
|
||||
context['can_add_meal'] = PermissionBackend.check_perm(self.request, 'food.transformedfood_add')
|
||||
|
||||
context['can_add_recipe'] = PermissionBackend.check_perm(self.request, 'food.recipe_add')
|
||||
|
||||
context['can_view_recipes'] = PermissionBackend.check_perm(self.request, 'food.recipe_view')
|
||||
|
||||
context["open_activities"] = Activity.objects.filter(activity_type__name="Perm bouffe", open=True)
|
||||
|
||||
return context
|
||||
|
||||
|
||||
@@ -241,8 +231,6 @@ class BasicFoodCreateView(ProtectQuerysetMixin, ProtectedCreateView):
|
||||
for field in context['form'].fields:
|
||||
if field == 'allergens':
|
||||
context['form'].fields[field].initial = getattr(food, field).all()
|
||||
elif field == 'traces':
|
||||
context['form'].fields[field].initial = getattr(food, field).all()
|
||||
else:
|
||||
context['form'].fields[field].initial = getattr(food, field)
|
||||
|
||||
@@ -302,42 +290,34 @@ class ManageIngredientsView(LoginRequiredMixin, UpdateView):
|
||||
def form_valid(self, form):
|
||||
old_ingredients = list(self.object.ingredients.all()).copy()
|
||||
old_allergens = list(self.object.allergens.all()).copy()
|
||||
old_traces = list(self.object.traces.all()).copy()
|
||||
self.object.ingredients.clear()
|
||||
for i in range(self.object.ingredients.all().count() + 1 + MAX_FORMS):
|
||||
prefix = 'form-' + str(i) + '-'
|
||||
|
||||
ingredient = None
|
||||
if form.data[prefix + 'qrcode'] not in ['0', '', 'NaN']:
|
||||
if form.data[prefix + 'qrcode'] not in ['0', '']:
|
||||
ingredient = QRCode.objects.get(pk=form.data[prefix + 'qrcode']).food_container
|
||||
|
||||
elif form.data[prefix + 'name'] != '':
|
||||
ingredient = Food.objects.get(pk=form.data[prefix + 'name'])
|
||||
|
||||
if form.data.get(prefix + 'add_all_same_name') == 'on':
|
||||
ingredients = Food.objects.filter(name=ingredient.name, owner=ingredient.owner, end_of_life='')
|
||||
else:
|
||||
ingredients = [ingredient]
|
||||
|
||||
for ingredient in ingredients:
|
||||
self.object.ingredients.add(ingredient)
|
||||
if (prefix + 'fully_used') in form.data and form.data[prefix + 'fully_used'] == 'on':
|
||||
ingredient.end_of_life = _('Fully used in {meal}'.format(
|
||||
meal=self.object.name))
|
||||
ingredient.save()
|
||||
|
||||
elif form.data[prefix + 'name'] != '':
|
||||
ingredient = Food.objects.get(pk=form.data[prefix + 'name'])
|
||||
self.object.ingredients.add(ingredient)
|
||||
if (prefix + 'fully_used') in form.data and form.data[prefix + 'fully_used'] == 'on':
|
||||
ingredient.end_of_life = _('Fully used in {meal}'.format(
|
||||
meal=self.object.name))
|
||||
ingredient.save()
|
||||
# We recalculate new expiry date and allergens
|
||||
self.object.expiry_date = self.object.creation_date + self.object.shelf_life
|
||||
self.object.allergens.clear()
|
||||
self.object.traces.clear()
|
||||
|
||||
for ingredient in self.object.ingredients.iterator():
|
||||
if not (ingredient.polymorphic_ctype.model == 'basicfood' and ingredient.date_type == 'DDM'):
|
||||
self.object.expiry_date = min(self.object.expiry_date, ingredient.expiry_date)
|
||||
self.object.allergens.set(self.object.allergens.union(ingredient.allergens.all()))
|
||||
self.object.traces.set(self.object.traces.union(ingredient.traces.all()))
|
||||
|
||||
self.object.save(old_ingredients=old_ingredients, old_allergens=old_allergens, old_traces=old_traces)
|
||||
self.object.save(old_ingredients=old_ingredients, old_allergens=old_allergens)
|
||||
return HttpResponseRedirect(self.get_success_url())
|
||||
|
||||
def get_context_data(self, *args, **kwargs):
|
||||
@@ -361,7 +341,6 @@ class ManageIngredientsView(LoginRequiredMixin, UpdateView):
|
||||
'qr_number': '' if qr.count() == 0 else qr[0].qr_code_number,
|
||||
'fully_used': 'true' if ingredient.end_of_life else '',
|
||||
})
|
||||
|
||||
return context
|
||||
|
||||
def get_success_url(self, **kwargs):
|
||||
@@ -390,15 +369,13 @@ class AddIngredientView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
|
||||
for meal in meals:
|
||||
old_ingredients = list(meal.ingredients.all()).copy()
|
||||
old_allergens = list(meal.allergens.all()).copy()
|
||||
old_traces = list(meal.traces.all()).copy()
|
||||
meal.ingredients.add(self.object.pk)
|
||||
# update allergen and expiry date if necessary
|
||||
if not (self.object.polymorphic_ctype.model == 'basicfood'
|
||||
and self.object.date_type == 'DDM'):
|
||||
meal.expiry_date = min(meal.expiry_date, self.object.expiry_date)
|
||||
meal.allergens.set(meal.allergens.union(self.object.allergens.all()))
|
||||
meal.traces.set(meal.traces.union(self.object.traces.all()))
|
||||
meal.save(old_ingredients=old_ingredients, old_allergens=old_allergens, old_traces=old_traces)
|
||||
meal.save(old_ingredients=old_ingredients, old_allergens=old_allergens)
|
||||
if 'fully_used' in form.data:
|
||||
if not self.object.end_of_life:
|
||||
self.object.end_of_life = _(f'Food fully used in : {meal.name}')
|
||||
@@ -428,7 +405,6 @@ class FoodUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
|
||||
form.instance.creater = self.request.user
|
||||
food = Food.objects.get(pk=self.kwargs['pk'])
|
||||
old_allergens = list(food.allergens.all()).copy()
|
||||
old_traces = list(food.traces.all()).copy()
|
||||
|
||||
if food.polymorphic_ctype.model == 'transformedfood':
|
||||
old_ingredients = food.ingredients.all()
|
||||
@@ -442,7 +418,7 @@ class FoodUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
|
||||
if food.polymorphic_ctype.model == 'transformedfood':
|
||||
form.instance.save(old_ingredients=old_ingredients)
|
||||
else:
|
||||
form.instance.save(old_allergens=old_allergens, old_traces=old_traces)
|
||||
form.instance.save(old_allergens=old_allergens)
|
||||
return ans
|
||||
|
||||
def get_form_class(self, **kwargs):
|
||||
@@ -475,7 +451,7 @@ class FoodDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
fields = ["name", "owner", "expiry_date", "allergens", "traces", "is_ready", "end_of_life", "order"]
|
||||
fields = ["name", "owner", "expiry_date", "allergens", "is_ready", "end_of_life", "order"]
|
||||
|
||||
fields = dict([(field, getattr(self.object, field)) for field in fields])
|
||||
if fields["is_ready"]:
|
||||
@@ -484,8 +460,6 @@ class FoodDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||
fields["is_ready"] = _("No")
|
||||
fields["allergens"] = ", ".join(
|
||||
allergen.name for allergen in fields["allergens"].all())
|
||||
fields["traces"] = ", ".join(
|
||||
trace.name for trace in fields["traces"].all())
|
||||
|
||||
context["fields"] = [(
|
||||
Food._meta.get_field(field).verbose_name.capitalize(),
|
||||
@@ -556,518 +530,3 @@ 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"], served=False)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
orders_count = Order.objects.filter(activity__pk=self.kwargs["activity_pk"], served=False).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
|
||||
|
||||
|
||||
class RecipeCreateView(ProtectQuerysetMixin, ProtectedCreateView):
|
||||
"""
|
||||
Create a recipe
|
||||
"""
|
||||
model = Recipe
|
||||
form_class = RecipeForm
|
||||
extra_context = {"title": _("Create a recipe")}
|
||||
|
||||
def get_sample_object(self):
|
||||
return Recipe(name='Sample recipe')
|
||||
|
||||
@transaction.atomic
|
||||
def form_valid(self, form):
|
||||
formset = RecipeIngredientsFormSet(self.request.POST)
|
||||
if formset.is_valid():
|
||||
ingredients = [f.cleaned_data['name'] for f in formset if f.cleaned_data.get('name')]
|
||||
self.object = form.save(commit=False)
|
||||
self.object.ingredients = ingredients
|
||||
self.object.save()
|
||||
return super().form_valid(form)
|
||||
else:
|
||||
return self.form_invalid(form)
|
||||
|
||||
def get_context_data(self, *args, **kwargs):
|
||||
context = super().get_context_data(*args, **kwargs)
|
||||
context['form'] = RecipeIngredientsForm()
|
||||
context['recipe_form'] = self.get_form()
|
||||
if self.request.POST:
|
||||
context['formset'] = RecipeIngredientsFormSet(self.request.POST,)
|
||||
else:
|
||||
context['formset'] = RecipeIngredientsFormSet()
|
||||
return context
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy('food:recipe_list')
|
||||
|
||||
|
||||
class RecipeListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
|
||||
"""
|
||||
List all recipes
|
||||
"""
|
||||
model = Recipe
|
||||
table_class = RecipeTable
|
||||
extra_context = {"title": _('All recipes')}
|
||||
|
||||
def get_context_data(self, *args, **kwargs):
|
||||
context = super().get_context_data(*args, **kwargs)
|
||||
|
||||
context['can_add_recipe'] = PermissionBackend.check_perm(self.request, 'food.recipe_add')
|
||||
|
||||
return context
|
||||
|
||||
|
||||
class RecipeDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||
"""
|
||||
List all recipes
|
||||
"""
|
||||
model = Recipe
|
||||
extra_context = {"title": _('Details of:')}
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["ingredients"] = self.object.ingredients
|
||||
context["update"] = PermissionBackend.check_perm(self.request, "food.change_recipe")
|
||||
|
||||
return context
|
||||
|
||||
|
||||
class RecipeUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
|
||||
"""
|
||||
Create a recipe
|
||||
"""
|
||||
model = Recipe
|
||||
form_class = RecipeForm
|
||||
extra_context = {"title": _("Create a recipe")}
|
||||
|
||||
def get_sample_object(self):
|
||||
return Recipe(name='Sample recipe')
|
||||
|
||||
@transaction.atomic
|
||||
def form_valid(self, form):
|
||||
formset = RecipeIngredientsFormSet(self.request.POST)
|
||||
if formset.is_valid():
|
||||
ingredients = [f.cleaned_data['name'] for f in formset if f.cleaned_data.get('name')]
|
||||
self.object = form.save(commit=False)
|
||||
self.object.ingredients = ingredients
|
||||
self.object.save()
|
||||
return super().form_valid(form)
|
||||
else:
|
||||
return self.form_invalid(form)
|
||||
|
||||
def get_context_data(self, *args, **kwargs):
|
||||
context = super().get_context_data(*args, **kwargs)
|
||||
context['form'] = RecipeIngredientsForm()
|
||||
context['recipe_form'] = self.get_form()
|
||||
if self.request.POST:
|
||||
formset = RecipeIngredientsFormSet(self.request.POST,)
|
||||
else:
|
||||
formset = RecipeIngredientsFormSet()
|
||||
ingredients = self.object.ingredients
|
||||
context["ingredients_count"] = len(ingredients)
|
||||
formset.extra += len(ingredients)
|
||||
context["formset"] = formset
|
||||
context["ingredients"] = []
|
||||
for ingredient in ingredients:
|
||||
context["ingredients"].append({"name": ingredient})
|
||||
return context
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy('food:recipe_detail', kwargs={"pk": self.object.pk})
|
||||
|
||||
|
||||
class UseRecipeView(LoginRequiredMixin, UpdateView):
|
||||
"""
|
||||
Add ingredients to a TransformedFood using a Recipe
|
||||
"""
|
||||
model = TransformedFood
|
||||
fields = ('ingredients',)
|
||||
template_name = 'food/use_recipe_form.html'
|
||||
extra_context = {"title": _("Use a recipe for:")}
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
context["form"] = UseRecipeForm()
|
||||
return context
|
||||
|
||||
def form_valid(self, form):
|
||||
old_ingredients = list(self.object.ingredients.all()).copy()
|
||||
old_allergens = list(self.object.allergens.all()).copy()
|
||||
old_traces = list(self.object.traces.all()).copy()
|
||||
if "ingredients" in form.data:
|
||||
ingredients_pk = form.data.getlist("ingredients")
|
||||
ingredients = Food.objects.all().filter(pk__in=ingredients_pk)
|
||||
for ingredient in ingredients:
|
||||
self.object.ingredients.add(ingredient)
|
||||
|
||||
# We recalculate new expiry date and allergens
|
||||
self.object.expiry_date = self.object.creation_date + self.object.shelf_life
|
||||
self.object.allergens.clear()
|
||||
self.object.traces.clear()
|
||||
|
||||
for ingredient in self.object.ingredients.iterator():
|
||||
if not (ingredient.polymorphic_ctype.model == 'basicfood' and ingredient.date_type == 'DDM'):
|
||||
self.object.expiry_date = min(self.object.expiry_date, ingredient.expiry_date)
|
||||
self.object.allergens.set(self.object.allergens.union(ingredient.allergens.all()))
|
||||
self.object.traces.set(self.object.traces.union(ingredient.traces.all()))
|
||||
|
||||
self.object.save(old_ingredients=old_ingredients, old_allergens=old_allergens, old_traces=old_traces)
|
||||
return HttpResponseRedirect(self.get_success_url())
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy('food:transformedfood_view', kwargs={"pk": self.object.pk})
|
||||
|
||||
|
||||
@require_GET
|
||||
def get_ingredients_for_recipe(request):
|
||||
recipe_id = request.GET.get('recipe_id')
|
||||
if not recipe_id:
|
||||
return JsonResponse({'error': 'Missing recipe_id'}, status=400)
|
||||
|
||||
try:
|
||||
recipe = Recipe.objects.get(pk=recipe_id)
|
||||
except Recipe.DoesNotExist:
|
||||
return JsonResponse({'error': 'Recipe not found'}, status=404)
|
||||
|
||||
# 🔧 Supporte les deux cas : ManyToMany ou simple liste
|
||||
ingredients_field = recipe.ingredients
|
||||
|
||||
if hasattr(ingredients_field, "values_list"):
|
||||
# Cas ManyToManyField
|
||||
ingredient_names = list(ingredients_field.values_list('name', flat=True))
|
||||
elif isinstance(ingredients_field, (list, tuple)):
|
||||
# Cas liste directe
|
||||
ingredient_names = ingredients_field
|
||||
else:
|
||||
return JsonResponse({'error': 'Unsupported ingredients type'}, status=500)
|
||||
|
||||
# Union des Foods dont le nom commence par un nom d’ingrédient
|
||||
query = Q()
|
||||
for name in ingredient_names:
|
||||
query |= Q(name__istartswith=name)
|
||||
|
||||
qs = Food.objects.filter(query).distinct()
|
||||
qs = qs.filter(PermissionBackend.filter_queryset(request, Food, 'view'))
|
||||
|
||||
data = [{'id': f.id, 'name': f.name, 'qr_code_numbers': ", ".join(str(q.qr_code_number) for q in f.QR_code.all())} for f in qs]
|
||||
return JsonResponse({'ingredients': data})
|
||||
|
||||
@@ -417,7 +417,7 @@ class Membership(models.Model):
|
||||
A membership is valid if today is between the start and the end date.
|
||||
"""
|
||||
if self.date_end is not None:
|
||||
return self.date_start.toordinal() <= datetime.datetime.now().toordinal() < self.date_end.toordinal()
|
||||
return self.date_start.toordinal() <= datetime.datetime.now().toordinal() <= self.date_end.toordinal()
|
||||
else:
|
||||
return self.date_start.toordinal() <= datetime.datetime.now().toordinal()
|
||||
|
||||
|
||||
@@ -228,7 +228,7 @@ function consume (source, source_alias, dest, quantity, amount, reason, type, ca
|
||||
addMsg(interpolate(gettext('Warning, the transaction from the note %s succeed, ' +
|
||||
'but the emitter note %s is negative.'), [source_alias, source_alias]), 'warning', 30000)
|
||||
}
|
||||
if (source.membership && source.membership.date_end < new Date().toISOString()) {
|
||||
if (source.membership && source.membership.date_end <= new Date().toISOString()) {
|
||||
addMsg(interpolate(gettext('Warning, the emitter note %s is no more a BDE member.'), [source_alias]),
|
||||
'danger', 30000)
|
||||
}
|
||||
|
||||
@@ -310,10 +310,10 @@ $('#btn_transfer').click(function () {
|
||||
destination: dest.note.id,
|
||||
destination_alias: dest.name
|
||||
}).done(function () {
|
||||
if (source.note.membership && source.note.membership.date_end < new Date().toISOString()) {
|
||||
if (source.note.membership && source.note.membership.date_end <= new Date().toISOString()) {
|
||||
addMsg(interpolate(gettext('Warning, the emitter note %s is no more a BDE member.'), [source.name]), 'danger', 30000)
|
||||
}
|
||||
if (dest.note.membership && dest.note.membership.date_end < new Date().toISOString()) {
|
||||
if (dest.note.membership && dest.note.membership.date_end <= new Date().toISOString()) {
|
||||
addMsg(interpolate(gettext('Warning, the destination note %s is no more a BDE member.'), [dest.name]), 'danger', 30000)
|
||||
}
|
||||
|
||||
@@ -414,7 +414,7 @@ $('#btn_transfer').click(function () {
|
||||
bank: $('#bank').val()
|
||||
}).done(function () {
|
||||
addMsg(gettext('Credit/debit succeed!'), 'success', 10000)
|
||||
if (user_note.membership && user_note.membership.date_end < new Date().toISOString()) { addMsg(gettext('Warning, the emitter note %s is no more a BDE member.'), 'danger', 10000) }
|
||||
if (user_note.membership && user_note.membership.date_end <= new Date().toISOString()) { addMsg(gettext('Warning, the emitter note %s is no more a BDE member.'), 'danger', 10000) }
|
||||
reset()
|
||||
}).fail(function (err) {
|
||||
const errObj = JSON.parse(err.responseText)
|
||||
|
||||
18
apps/treasury/migrations/0011_sogecredit_valid.py
Normal file
18
apps/treasury/migrations/0011_sogecredit_valid.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.2.6 on 2025-09-28 20:12
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('treasury', '0010_alter_invoice_bde'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='sogecredit',
|
||||
name='valid',
|
||||
field=models.BooleanField(blank=True, default=False, verbose_name='Valid'),
|
||||
),
|
||||
]
|
||||
@@ -308,6 +308,12 @@ class SogeCredit(models.Model):
|
||||
null=True,
|
||||
)
|
||||
|
||||
valid = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("Valid"),
|
||||
blank=True,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Credit from the Société générale")
|
||||
verbose_name_plural = _("Credits from the Société générale")
|
||||
@@ -332,7 +338,7 @@ class SogeCredit(models.Model):
|
||||
last_name=self.user.last_name,
|
||||
first_name=self.user.first_name,
|
||||
bank="Société générale",
|
||||
valid=False,
|
||||
valid=True,
|
||||
)
|
||||
credit_transaction._force_save = True
|
||||
credit_transaction.save()
|
||||
@@ -346,12 +352,12 @@ class SogeCredit(models.Model):
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def valid(self):
|
||||
def valid_legacy(self):
|
||||
return self.credit_transaction and self.credit_transaction.valid
|
||||
|
||||
@property
|
||||
def amount(self):
|
||||
if self.valid:
|
||||
if self.valid_legacy:
|
||||
return self.credit_transaction.total
|
||||
amount = 0
|
||||
transactions_wei = self.transactions.filter(membership__club__weiclub__isnull=False)
|
||||
@@ -397,7 +403,7 @@ class SogeCredit(models.Model):
|
||||
self.transactions.add(m.transaction)
|
||||
|
||||
for tr in self.transactions.all():
|
||||
tr.valid = False
|
||||
tr.valid = True
|
||||
tr.save()
|
||||
|
||||
def invalidate(self):
|
||||
@@ -422,6 +428,7 @@ class SogeCredit(models.Model):
|
||||
self.invalidate()
|
||||
# Refresh credit amount
|
||||
self.save()
|
||||
self.valid = True
|
||||
self.credit_transaction.valid = True
|
||||
self.credit_transaction._force_save = True
|
||||
self.credit_transaction.save()
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 284 KiB After Width: | Height: | Size: 104 KiB |
@@ -56,6 +56,7 @@ class InvoiceTable(tables.Table):
|
||||
model = Invoice
|
||||
template_name = 'django_tables2/bootstrap4.html'
|
||||
fields = ('id', 'name', 'object', 'acquitted', 'invoice',)
|
||||
order_by = ('-id',)
|
||||
|
||||
|
||||
class RemittanceTable(tables.Table):
|
||||
|
||||
@@ -417,7 +417,7 @@ class SogeCreditListView(LoginRequiredMixin, ProtectQuerysetMixin, SingleTableVi
|
||||
)
|
||||
|
||||
if "valid" not in self.request.GET or not self.request.GET["valid"]:
|
||||
qs = qs.filter(credit_transaction__valid=False)
|
||||
qs = qs.filter(valid=False)
|
||||
|
||||
return qs
|
||||
|
||||
|
||||
@@ -680,7 +680,7 @@ class TestWEIRegistration(TestCase):
|
||||
self.assertTrue(soge_credit.exists())
|
||||
soge_credit = soge_credit.get()
|
||||
self.assertTrue(membership.transaction in soge_credit.transactions.all())
|
||||
self.assertFalse(membership.transaction.valid)
|
||||
self.assertTrue(membership.transaction.valid)
|
||||
|
||||
# Check that if the WEI is started, we can't update a wei
|
||||
self.wei.date_start = date(2000, 1, 1)
|
||||
|
||||
@@ -4366,6 +4366,14 @@ msgstr ""
|
||||
msgid "Forgotten your password or username?"
|
||||
msgstr "Passwort oder Username vergessen?"
|
||||
|
||||
#: note_kfet/templates/registration/login.html:44
|
||||
msgid "Download on the AppStore"
|
||||
msgstr "Im AppStore herunterladen"
|
||||
|
||||
#: note_kfet/templates/registration/login.html:48
|
||||
msgid "Get it on Google Play"
|
||||
msgstr "Bei Google Play herunterladen"
|
||||
|
||||
#: note_kfet/templates/registration/password_change_done.html:13
|
||||
msgid "Your password was changed."
|
||||
msgstr "Ihr Passwort wurde geändert."
|
||||
|
||||
@@ -4281,6 +4281,14 @@ msgstr ""
|
||||
msgid "Forgotten your password or username?"
|
||||
msgstr "¿ Contraseña o nombre de usuario olvidado ?"
|
||||
|
||||
#: note_kfet/templates/registration/login.html:44
|
||||
msgid "Download on the AppStore"
|
||||
msgstr "Descargar en la AppStore"
|
||||
|
||||
#: note_kfet/templates/registration/login.html:48
|
||||
msgid "Get it on Google Play"
|
||||
msgstr "Descargar en Google Play"
|
||||
|
||||
#: note_kfet/templates/registration/password_change_done.html:13
|
||||
msgid "Your password was changed."
|
||||
msgstr "Su contraseña fue cambiada con éxito."
|
||||
|
||||
@@ -4584,6 +4584,14 @@ msgstr ""
|
||||
msgid "Forgotten your password or username?"
|
||||
msgstr "Mot de passe ou pseudo oublié ?"
|
||||
|
||||
#: note_kfet/templates/registration/login.html:44
|
||||
msgid "Download on the AppStore"
|
||||
msgstr "Télécharger sur l'AppStore"
|
||||
|
||||
#: note_kfet/templates/registration/login.html:48
|
||||
msgid "Get it on Google Play"
|
||||
msgstr "Télécharger sur Google Play"
|
||||
|
||||
#: note_kfet/templates/registration/password_change_done.html:13
|
||||
msgid "Your password was changed."
|
||||
msgstr "Votre mot de passe a bien été changé."
|
||||
|
||||
50
note_kfet/static/img/appstore_badge_fr.svg
Normal file
50
note_kfet/static/img/appstore_badge_fr.svg
Normal file
@@ -0,0 +1,50 @@
|
||||
<svg id="livetype" xmlns="http://www.w3.org/2000/svg" width="126.50751" height="40" viewBox="0 0 126.50751 40">
|
||||
<title>Download_on_the_App_Store_Badge_FR_RGB_blk_100517</title>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M116.97821,0H9.53468c-.3667,0-.729,0-1.09473.002-.30615.002-.60986.00781-.91895.0127A13.21476,13.21476,0,0,0,5.5171.19141a6.66509,6.66509,0,0,0-1.90088.627A6.43779,6.43779,0,0,0,1.99757,1.99707,6.25844,6.25844,0,0,0,.81935,3.61816a6.60119,6.60119,0,0,0-.625,1.90332,12.993,12.993,0,0,0-.1792,2.002C.00587,7.83008.00489,8.1377,0,8.44434V31.5586c.00489.3105.00587.6113.01515.9219a12.99232,12.99232,0,0,0,.1792,2.0019,6.58756,6.58756,0,0,0,.625,1.9043A6.20778,6.20778,0,0,0,1.99757,38.001a6.27445,6.27445,0,0,0,1.61865,1.1787,6.70082,6.70082,0,0,0,1.90088.6308,13.45514,13.45514,0,0,0,2.0039.1768c.30909.0068.6128.0107.91895.0107C8.80567,40,9.168,40,9.53468,40H116.97821c.3594,0,.7246,0,1.084-.002.3047,0,.6172-.0039.9219-.0107a13.279,13.279,0,0,0,2-.1768,6.80432,6.80432,0,0,0,1.9082-.6308,6.27742,6.27742,0,0,0,1.6172-1.1787,6.39482,6.39482,0,0,0,1.1816-1.6143,6.60413,6.60413,0,0,0,.6191-1.9043,13.50641,13.50641,0,0,0,.1856-2.0019c.0039-.3106.0039-.6114.0039-.9219.0078-.3633.0078-.7246.0078-1.0938V9.53613c0-.36621,0-.72949-.0078-1.09179,0-.30664,0-.61426-.0039-.9209a13.50709,13.50709,0,0,0-.1856-2.002,6.6177,6.6177,0,0,0-.6191-1.90332,6.46619,6.46619,0,0,0-2.7988-2.7998,6.76753,6.76753,0,0,0-1.9082-.627,13.04394,13.04394,0,0,0-2-.17676c-.3047-.00488-.6172-.01074-.9219-.01269-.3594-.002-.7246-.002-1.084-.002Z" style="fill: #a6a6a6"/>
|
||||
<path d="M8.44482,39.125c-.30467,0-.602-.0039-.90428-.0107a12.68714,12.68714,0,0,1-1.86914-.1631,5.88381,5.88381,0,0,1-1.65674-.5479,5.40573,5.40573,0,0,1-1.397-1.0166,5.32082,5.32082,0,0,1-1.02051-1.3965,5.72186,5.72186,0,0,1-.543-1.6572,12.41351,12.41351,0,0,1-.1665-1.875c-.00634-.2109-.01464-.9131-.01464-.9131V8.44434S.88185,7.75293.8877,7.5498a12.37138,12.37138,0,0,1,.16552-1.87207,5.75577,5.75577,0,0,1,.54347-1.6621A5.37365,5.37365,0,0,1,2.61182,2.61768,5.56562,5.56562,0,0,1,4.01417,1.59521a5.82309,5.82309,0,0,1,1.65332-.54394A12.58579,12.58579,0,0,1,7.543.88721L8.44532.875h109.612l.9131.0127a12.38493,12.38493,0,0,1,1.8584.16259,5.93833,5.93833,0,0,1,1.6709.54785,5.59375,5.59375,0,0,1,2.415,2.41993,5.76267,5.76267,0,0,1,.5352,1.64892,12.995,12.995,0,0,1,.1738,1.88721c.0029.2832.0029.5874.0029.89014.0079.375.0079.73193.0079,1.09179V30.4648c0,.3633,0,.7178-.0079,1.0752,0,.3252,0,.6231-.0039.9297a12.73126,12.73126,0,0,1-.1709,1.8535,5.739,5.739,0,0,1-.54,1.67,5.48029,5.48029,0,0,1-1.0156,1.3857,5.4129,5.4129,0,0,1-1.3994,1.0225,5.86168,5.86168,0,0,1-1.668.5498,12.54218,12.54218,0,0,1-1.8692.1631c-.2929.0068-.5996.0107-.8974.0107l-1.084.002Z"/>
|
||||
</g>
|
||||
<g>
|
||||
<g id="_Group_" data-name="<Group>">
|
||||
<g id="_Group_2" data-name="<Group>">
|
||||
<g id="_Group_3" data-name="<Group>">
|
||||
<path id="_Path_" data-name="<Path>" d="M24.7718,20.30068a4.94881,4.94881,0,0,1,2.35656-4.15206,5.06566,5.06566,0,0,0-3.99116-2.15768c-1.67924-.17626-3.30719,1.00483-4.1629,1.00483-.87227,0-2.18977-.98733-3.6085-.95814a5.31529,5.31529,0,0,0-4.47292,2.72787c-1.934,3.34842-.49141,8.26947,1.3612,10.97608.9269,1.32535,2.01018,2.8058,3.42763,2.7533,1.38706-.05753,1.9051-.88448,3.5794-.88448,1.65876,0,2.14479.88448,3.591.8511,1.48838-.02416,2.42613-1.33124,3.32051-2.66914A10.962,10.962,0,0,0,27.691,24.69985,4.78205,4.78205,0,0,1,24.7718,20.30068Z" style="fill: #fff"/>
|
||||
<path id="_Path_2" data-name="<Path>" d="M22.04017,12.21089a4.87248,4.87248,0,0,0,1.11452-3.49062,4.95746,4.95746,0,0,0-3.20758,1.65961,4.63634,4.63634,0,0,0-1.14371,3.36139A4.09905,4.09905,0,0,0,22.04017,12.21089Z" style="fill: #fff"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g id="_Group_4" data-name="<Group>">
|
||||
<g>
|
||||
<path d="M35.65528,14.70166V9.57813h-1.877V8.73486h4.67676v.84326H36.582v5.12354Z" style="fill: #fff"/>
|
||||
<path d="M42.76466,13.48584a1.828,1.828,0,0,1-1.95117,1.30273,2.04531,2.04531,0,0,1-2.08008-2.32422,2.07685,2.07685,0,0,1,2.07617-2.35254c1.25293,0,2.00879.856,2.00879,2.27v.31006H39.63868v.0498a1.1902,1.1902,0,0,0,1.19922,1.29,1.07934,1.07934,0,0,0,1.07129-.5459Zm-3.126-1.45117H41.9131a1.08647,1.08647,0,0,0-1.1084-1.1665A1.15162,1.15162,0,0,0,39.63868,12.03467ZM40.2754,9.4458l1.03809-1.42236h1.042L41.19337,9.4458Z" style="fill: #fff"/>
|
||||
<path d="M44.05274,8.44092h.88867v6.26074h-.88867Z" style="fill: #fff"/>
|
||||
<path d="M50.208,13.48584a1.828,1.828,0,0,1-1.95117,1.30273,2.04531,2.04531,0,0,1-2.08008-2.32422,2.07685,2.07685,0,0,1,2.07617-2.35254c1.25293,0,2.00879.856,2.00879,2.27v.31006H47.082v.0498a1.1902,1.1902,0,0,0,1.19922,1.29,1.07934,1.07934,0,0,0,1.07129-.5459Zm-3.126-1.45117h2.27441a1.08647,1.08647,0,0,0-1.1084-1.1665A1.15162,1.15162,0,0,0,47.082,12.03467Zm.63672-2.58887,1.03809-1.42236h1.042L48.63673,9.4458Z" style="fill: #fff"/>
|
||||
<path d="M54.40333,11.67041a1.00546,1.00546,0,0,0-1.06348-.76465c-.74414,0-1.19922.57031-1.19922,1.52979,0,.97607.459,1.55908,1.19922,1.55908a.97873.97873,0,0,0,1.06348-.74023h.86426a1.762,1.762,0,0,1-1.92285,1.53418,2.06791,2.06791,0,0,1-2.11328-2.353,2.05305,2.05305,0,0,1,2.1084-2.32373,1.77731,1.77731,0,0,1,1.92773,1.55859Z" style="fill: #fff"/>
|
||||
<path d="M56.44728,8.44092h.88086v2.48145h.07031a1.3856,1.3856,0,0,1,1.373-.80664,1.48339,1.48339,0,0,1,1.55078,1.67871v2.90723h-.88965v-2.688c0-.71924-.335-1.0835-.96289-1.0835a1.05194,1.05194,0,0,0-1.13379,1.1416v2.62988h-.88867Z" style="fill: #fff"/>
|
||||
<path d="M61.43946,13.42822c0-.81055.60352-1.27783,1.6748-1.34424l1.21973-.07031V11.625c0-.47559-.31445-.74414-.92188-.74414-.49609,0-.83984.18213-.93848.50049h-.86035c.09082-.77344.81836-1.26953,1.83984-1.26953,1.12891,0,1.76514.562,1.76514,1.51318v3.07666h-.855v-.63281H64.293a1.515,1.515,0,0,1-1.35254.707A1.36026,1.36026,0,0,1,61.43946,13.42822Zm2.89453-.38477V12.667l-1.09961.07031c-.62012.0415-.90137.25244-.90137.64941,0,.40527.35156.64111.835.64111A1.0615,1.0615,0,0,0,64.334,13.04346Z" style="fill: #fff"/>
|
||||
<path d="M66.60987,10.19873h.85547v.69043h.06641a1.22092,1.22092,0,0,1,1.21582-.76514,1.86836,1.86836,0,0,1,.39648.03711v.877a2.43442,2.43442,0,0,0-.49609-.05371A1.05507,1.05507,0,0,0,67.49855,12.043v2.65869h-.88867Z" style="fill: #fff"/>
|
||||
<path d="M69.96144,15.15234h.90918c.0752.32666.45117.5376,1.05078.5376.74023,0,1.17871-.35156,1.17871-.94678v-.86426H73.0337a1.51433,1.51433,0,0,1-1.38965.75635c-1.14941,0-1.86035-.88867-1.86035-2.23682,0-1.373.71875-2.27441,1.86914-2.27441a1.56045,1.56045,0,0,1,1.41406.79395h.07031v-.71924h.85156v4.54c0,1.02979-.80664,1.68311-2.08008,1.68311C70.7837,16.42188,70.05616,15.91748,69.96144,15.15234Zm3.15527-2.7583c0-.897-.46387-1.47168-1.2207-1.47168-.76465,0-1.19434.57471-1.19434,1.47168,0,.89746.42969,1.47217,1.19434,1.47217C72.65773,13.86621,73.11671,13.2959,73.11671,12.394Z" style="fill: #fff"/>
|
||||
<path d="M79.21241,13.48584a1.828,1.828,0,0,1-1.95117,1.30273,2.04531,2.04531,0,0,1-2.08008-2.32422,2.07685,2.07685,0,0,1,2.07617-2.35254c1.25293,0,2.00879.856,2.00879,2.27v.31006H76.08644v.0498a1.1902,1.1902,0,0,0,1.19922,1.29,1.07934,1.07934,0,0,0,1.07129-.5459Zm-3.126-1.45117h2.27441a1.08647,1.08647,0,0,0-1.1084-1.1665A1.15162,1.15162,0,0,0,76.08644,12.03467Z" style="fill: #fff"/>
|
||||
<path d="M80.45948,10.19873H81.315v.69043h.06641a1.22092,1.22092,0,0,1,1.21582-.76514,1.86836,1.86836,0,0,1,.39648.03711v.877a2.43442,2.43442,0,0,0-.49609-.05371A1.05507,1.05507,0,0,0,81.34815,12.043v2.65869h-.88867Z" style="fill: #fff"/>
|
||||
<path d="M86.19581,12.44824c0-1.42285.73145-2.32422,1.86914-2.32422a1.484,1.484,0,0,1,1.38086.79h.06641V8.44092h.88867v6.26074h-.85156v-.71143H89.479a1.56284,1.56284,0,0,1-1.41406.78564C86.91944,14.77588,86.19581,13.87451,86.19581,12.44824Zm.918,0c0,.95508.4502,1.52979,1.20313,1.52979.749,0,1.21191-.583,1.21191-1.52588,0-.93848-.46777-1.52979-1.21191-1.52979C87.56886,10.92236,87.11378,11.501,87.11378,12.44824Z" style="fill: #fff"/>
|
||||
<path d="M91.60206,13.42822c0-.81055.60352-1.27783,1.6748-1.34424l1.21973-.07031V11.625c0-.47559-.31445-.74414-.92187-.74414-.49609,0-.83984.18213-.93848.50049h-.86035c.09082-.77344.81836-1.26953,1.83984-1.26953,1.12891,0,1.76563.562,1.76563,1.51318v3.07666h-.85547v-.63281h-.07031a1.515,1.515,0,0,1-1.35254.707A1.36026,1.36026,0,0,1,91.60206,13.42822Zm2.89453-.38477V12.667L93.397,12.7373c-.62012.0415-.90137.25244-.90137.64941,0,.40527.35156.64111.835.64111A1.0615,1.0615,0,0,0,94.49659,13.04346Z" style="fill: #fff"/>
|
||||
<path d="M96.773,10.19873h.85547v.71533h.06641a1.348,1.348,0,0,1,1.34375-.80225,1.46456,1.46456,0,0,1,1.55859,1.6748v2.915h-.88867V12.00977c0-.72363-.31445-1.0835-.97168-1.0835a1.03294,1.03294,0,0,0-1.0752,1.14111v2.63428H96.773Z" style="fill: #fff"/>
|
||||
<path d="M103.61769,10.11182c1.0127,0,1.6748.47119,1.76172,1.26514h-.85254c-.082-.33057-.40527-.5415-.90918-.5415-.49609,0-.873.23535-.873.58691,0,.269.22754.43848.71582.55029l.748.17334c.85645.19873,1.25781.56689,1.25781,1.22852,0,.84766-.79,1.41406-1.86523,1.41406-1.07129,0-1.76953-.48389-1.84863-1.28174h.88965a.91365.91365,0,0,0,.97949.562c.55371,0,.94727-.248.94727-.60791,0-.26855-.21094-.44238-.66211-.5498l-.78516-.18213c-.85645-.20264-1.25293-.58691-1.25293-1.25684C101.86866,10.67383,102.60011,10.11182,103.61769,10.11182Z" style="fill: #fff"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<path d="M35.19825,18.06689h1.85938V30.48535H35.19825Z" style="fill: #fff"/>
|
||||
<path d="M39.29786,22.61084l1.01563-4.54395h1.80664l-1.23047,4.54395Z" style="fill: #fff"/>
|
||||
<path d="M49.14649,27.12891H44.4131l-1.13672,3.35645H41.27149l4.4834-12.41846h2.083l4.4834,12.41846H50.28224Zm-4.24316-1.54883h3.752l-1.84961-5.44775h-.05176Z" style="fill: #fff"/>
|
||||
<path d="M62.00294,25.959c0,2.81348-1.50586,4.62109-3.77832,4.62109a3.0693,3.0693,0,0,1-2.84863-1.584h-.043v4.48438h-1.8584V21.43115h1.79883V22.937h.03418a3.21162,3.21162,0,0,1,2.88281-1.60059C60.48829,21.33643,62.00294,23.15283,62.00294,25.959Zm-1.91016,0c0-1.8335-.94727-3.03857-2.39258-3.03857-1.41992,0-2.375,1.23047-2.375,3.03857,0,1.82422.95508,3.0459,2.375,3.0459C59.14552,29.00488,60.09278,27.80859,60.09278,25.959Z" style="fill: #fff"/>
|
||||
<path d="M71.9673,25.959c0,2.81348-1.50586,4.62109-3.77832,4.62109a3.0693,3.0693,0,0,1-2.84863-1.584h-.043v4.48438H63.43946V21.43115H65.2378V22.937H65.272a3.21162,3.21162,0,0,1,2.88281-1.60059C70.45265,21.33643,71.9673,23.15283,71.9673,25.959Zm-1.91016,0c0-1.8335-.94727-3.03857-2.39258-3.03857-1.41992,0-2.375,1.23047-2.375,3.03857,0,1.82422.95508,3.0459,2.375,3.0459C69.10987,29.00488,70.05714,27.80859,70.05714,25.959Z" style="fill: #fff"/>
|
||||
<path d="M78.55323,27.02539c.1377,1.23145,1.334,2.04,2.96875,2.04,1.56641,0,2.69336-.80859,2.69336-1.91895,0-.96387-.67969-1.541-2.28906-1.93652l-1.60937-.38818C78.03663,24.271,76.978,23.20459,76.978,21.47412c0-2.14258,1.86719-3.61426,4.51855-3.61426,2.624,0,4.42285,1.47168,4.4834,3.61426H84.104c-.1123-1.23926-1.13672-1.9873-2.63379-1.9873s-2.52148.75684-2.52148,1.8584c0,.87793.6543,1.39453,2.25488,1.79l1.36816.33594c2.54785.60254,3.60645,1.62646,3.60645,3.44287,0,2.32324-1.85059,3.77832-4.79395,3.77832-2.75391,0-4.61328-1.4209-4.7334-3.667Z" style="fill: #fff"/>
|
||||
<path d="M90.19,19.28857v2.14258h1.72168v1.47168H90.19v4.9917c0,.77539.34473,1.13672,1.10156,1.13672a5.80752,5.80752,0,0,0,.61133-.043v1.46289a5.10351,5.10351,0,0,1-1.03223.08594c-1.833,0-2.54785-.68848-2.54785-2.44434V22.90283H87.00636V21.43115h1.31641V19.28857Z" style="fill: #fff"/>
|
||||
<path d="M92.90773,25.959c0-2.84912,1.67773-4.63916,4.29395-4.63916,2.625,0,4.29492,1.79,4.29492,4.63916,0,2.85645-1.66113,4.63867-4.29492,4.63867C94.56886,30.59766,92.90773,28.81543,92.90773,25.959Zm6.69531,0c0-1.95459-.89551-3.10791-2.40137-3.10791s-2.40039,1.16211-2.40039,3.10791c0,1.96191.89453,3.10645,2.40039,3.10645S99.603,27.9209,99.603,25.959Z" style="fill: #fff"/>
|
||||
<path d="M103.02882,21.43115h1.77246v1.541h.043a2.1594,2.1594,0,0,1,2.17773-1.63574,2.86616,2.86616,0,0,1,.63672.06934V23.144a2.59794,2.59794,0,0,0-.835-.1123,1.8728,1.8728,0,0,0-1.93652,2.0835v5.37012h-1.8584Z" style="fill: #fff"/>
|
||||
<path d="M116.22608,27.82617c-.25,1.64355-1.85059,2.77148-3.89844,2.77148-2.63379,0-4.26855-1.76465-4.26855-4.5957,0-2.84033,1.64355-4.68213,4.19043-4.68213,2.50488,0,4.08008,1.7207,4.08008,4.46631v.63672h-6.39453v.1123a2.358,2.358,0,0,0,2.43555,2.56445,2.04834,2.04834,0,0,0,2.09082-1.27344ZM109.94386,25.124h4.52637a2.17744,2.17744,0,0,0-2.2207-2.29834A2.29214,2.29214,0,0,0,109.94386,25.124Z" style="fill: #fff"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 12 KiB |
49
note_kfet/static/img/playstore_badge_fr.svg
Normal file
49
note_kfet/static/img/playstore_badge_fr.svg
Normal file
@@ -0,0 +1,49 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="artwork" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 135 40">
|
||||
<!-- Generator: Adobe Illustrator 29.5.1, SVG Export Plug-In . SVG Version: 2.1.0 Build 141) -->
|
||||
<defs>
|
||||
<style>
|
||||
.st0 {
|
||||
fill: #4285f4;
|
||||
}
|
||||
|
||||
.st1 {
|
||||
fill: #a6a6a6;
|
||||
}
|
||||
|
||||
.st2 {
|
||||
fill: #34a853;
|
||||
}
|
||||
|
||||
.st3 {
|
||||
fill: #fbbc04;
|
||||
}
|
||||
|
||||
.st4, .st5 {
|
||||
fill: #fff;
|
||||
}
|
||||
|
||||
.st6 {
|
||||
fill: #ea4335;
|
||||
}
|
||||
|
||||
.st5 {
|
||||
font-family: GoogleSans-Medium, 'Google Sans';
|
||||
font-size: 8.7px;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g>
|
||||
<rect width="135" height="40" rx="5" ry="5"/>
|
||||
<path class="st1" d="M130,.8c2.316,0,4.2,1.884,4.2,4.2v30c0,2.316-1.884,4.2-4.2,4.2H5c-2.316,0-4.2-1.884-4.2-4.2V5C.8,2.684,2.684.8,5,.8h125M130,0H5C2.25,0,0,2.25,0,5v30c0,2.75,2.25,5,5,5h125c2.75,0,5-2.25,5-5V5C135,2.25,132.75,0,130,0h0Z"/>
|
||||
<path class="st4" d="M68.136,21.752c-2.352,0-4.269,1.788-4.269,4.253,0,2.449,1.917,4.253,4.269,4.253s4.269-1.804,4.269-4.253c0-2.465-1.917-4.253-4.269-4.253ZM68.136,28.583c-1.289,0-2.4-1.063-2.4-2.578,0-1.531,1.112-2.578,2.4-2.578s2.4,1.047,2.4,2.578c0,1.514-1.112,2.578-2.4,2.578ZM58.822,21.752c-2.352,0-4.269,1.788-4.269,4.253,0,2.449,1.917,4.253,4.269,4.253s4.269-1.804,4.269-4.253c0-2.465-1.917-4.253-4.269-4.253ZM58.822,28.583c-1.289,0-2.4-1.063-2.4-2.578,0-1.531,1.112-2.578,2.4-2.578s2.4,1.047,2.4,2.578c0,1.514-1.112,2.578-2.4,2.578ZM47.744,23.057v1.804h4.318c-.129,1.015-.467,1.756-.983,2.272-.628.628-1.611,1.321-3.335,1.321-2.658,0-4.736-2.143-4.736-4.801s2.078-4.801,4.736-4.801c1.434,0,2.481.564,3.254,1.289l1.273-1.273c-1.079-1.031-2.513-1.82-4.527-1.82-3.641,0-6.702,2.964-6.702,6.605s3.061,6.605,6.702,6.605c1.965,0,3.448-.644,4.608-1.853,1.192-1.192,1.563-2.868,1.563-4.221,0-.419-.032-.805-.097-1.128h-6.074ZM93.052,24.458c-.354-.95-1.434-2.707-3.641-2.707-2.191,0-4.011,1.724-4.011,4.253,0,2.384,1.804,4.253,4.221,4.253,1.949,0,3.077-1.192,3.544-1.885l-1.45-.967c-.483.709-1.144,1.176-2.094,1.176s-1.627-.435-2.062-1.289l5.687-2.352-.193-.483ZM87.252,25.876c-.048-1.643,1.273-2.481,2.223-2.481.741,0,1.369.37,1.579.902l-3.802,1.579ZM82.628,30h1.869v-12.502h-1.869v12.502ZM79.567,22.702h-.064c-.419-.499-1.224-.951-2.239-.951-2.127,0-4.076,1.869-4.076,4.269,0,2.384,1.949,4.237,4.076,4.237,1.015,0,1.82-.451,2.239-.967h.064v.612c0,1.627-.87,2.497-2.272,2.497-1.144,0-1.853-.822-2.143-1.514l-1.627.677c.467,1.128,1.708,2.513,3.77,2.513,2.191,0,4.044-1.289,4.044-4.43v-7.636h-1.772v.693ZM77.425,28.583c-1.289,0-2.368-1.079-2.368-2.562,0-1.498,1.079-2.594,2.368-2.594,1.273,0,2.272,1.096,2.272,2.594,0,1.482-.999,2.562-2.272,2.562ZM101.806,17.499h-4.471v12.501h1.866v-4.736h2.605c2.068,0,4.101-1.497,4.101-3.883s-2.033-3.882-4.101-3.882ZM101.854,23.524h-2.654v-4.285h2.654c1.395,0,2.187,1.155,2.187,2.143,0,.969-.792,2.143-2.187,2.143ZM113.386,21.729c-1.351,0-2.75.595-3.329,1.914l1.657.692c.354-.692,1.013-.917,1.705-.917.965,0,1.946.579,1.962,1.608v.129c-.338-.193-1.061-.483-1.946-.483-1.785,0-3.603.981-3.603,2.815,0,1.673,1.463,2.75,3.104,2.75,1.254,0,1.946-.563,2.38-1.222h.064v.965h1.801v-4.793c0-2.22-1.657-3.458-3.796-3.458ZM113.16,28.58c-.611,0-1.464-.305-1.464-1.061,0-.965,1.061-1.335,1.978-1.335.82,0,1.206.177,1.705.418-.145,1.158-1.142,1.978-2.219,1.978ZM123.743,22.002l-2.139,5.42h-.064l-2.219-5.42h-2.01l3.329,7.575-1.898,4.214h1.946l5.131-11.789h-2.075ZM106.936,30h1.866v-12.501h-1.866v12.501Z"/>
|
||||
<g>
|
||||
<path class="st6" d="M20.717,19.424l-10.647,11.3s.001.005.002.007c.327,1.227,1.447,2.13,2.777,2.13.531,0,1.031-.144,1.459-.396l.034-.02,11.984-6.915-5.609-6.106Z"/>
|
||||
<path class="st3" d="M31.488,17.5l-.01-.007-5.174-3-5.829,5.187,5.849,5.848,5.146-2.969c.902-.487,1.515-1.438,1.515-2.535,0-1.09-.604-2.036-1.498-2.525Z"/>
|
||||
<path class="st0" d="M10.07,9.277c-.064.236-.098.484-.098.74v19.968c0,.256.033.504.098.739l11.013-11.011-11.013-10.436Z"/>
|
||||
<path class="st2" d="M20.796,20.001l5.51-5.509-11.97-6.94c-.435-.261-.943-.411-1.486-.411-1.33,0-2.452.905-2.779,2.134,0,0,0,.002,0,.003l10.726,10.724Z"/>
|
||||
</g>
|
||||
</g>
|
||||
<text class="st5" transform="translate(41.08 13.134)"><tspan x="0" y="0">DISPONIBLE SUR</tspan></text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.2 KiB |
@@ -13,14 +13,11 @@ $(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) {
|
||||
const extra = isManageIngredients ? ` (${obj.owner_name})` : ''
|
||||
html += li(`${prefix}_${obj.id}`, `${obj[name_field]}${extra}`)
|
||||
html += li(prefix + '_' + obj.id, obj[name_field])
|
||||
})
|
||||
html += '</ul>'
|
||||
|
||||
|
||||
@@ -39,6 +39,17 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<a href="{% url 'password_reset' %}"
|
||||
class="badge badge-light">{% trans 'Forgotten your password or username?' %}</a>
|
||||
</form>
|
||||
|
||||
<div class="text-center mt-4">
|
||||
<a href="https://apps.apple.com/fr/app/la-note-kfet/id6754661723" class="d-inline-block mx-1" aria-label="{% trans 'Download on the AppStore' %}" style="cursor: pointer;">
|
||||
<img src="/static/img/appstore_badge_fr.svg"
|
||||
alt="{% trans 'Download on the AppStore' %}" style="height: 50px;">
|
||||
</a>
|
||||
<a href="https://play.google.com/store/apps/details?id=org.crans.bde.note&hl=fr" class="d-inline-block mx-1" aria-label="{% trans 'Get it on Google Play' %}" style="cursor: pointer;">
|
||||
<img src="/static/img/playstore_badge_fr.svg"
|
||||
alt="{% trans 'Get it on Google Play' %}" style="height: 50px;">
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -12,7 +12,7 @@ django-filter~=25.1
|
||||
django-mailer~=2.3.2
|
||||
django-oauth-toolkit~=3.0.1
|
||||
django-phonenumber-field~=8.1.0
|
||||
django-polymorphic~=3.1.0
|
||||
django-polymorphic~=4.1.0
|
||||
djangorestframework~=3.16.0
|
||||
django-rest-polymorphic~=0.1.10
|
||||
django-tables2~=2.7.5
|
||||
|
||||
Reference in New Issue
Block a user