mirror of
https://gitlab.crans.org/bde/nk20
synced 2025-11-15 11:07:46 +01:00
Compare commits
20 Commits
033c466cf7
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7af3c42a02 | ||
|
|
73b63186fd | ||
|
|
e119e2295c | ||
|
|
37beb8f421 | ||
|
|
cae86bcd46 | ||
|
|
74aee64161 | ||
|
|
206a967827 | ||
|
|
04001202f2 | ||
|
|
69aedccbae | ||
|
|
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>
|
||||
|
||||
@@ -152,9 +152,11 @@ class ActivityDetailView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMix
|
||||
def get_tables_data(self):
|
||||
return [
|
||||
Guest.objects.filter(activity=self.object)
|
||||
.filter(PermissionBackend.filter_queryset(self.request, Guest, "view")),
|
||||
.filter(PermissionBackend.filter_queryset(self.request, Guest, "view"))
|
||||
.distinct(),
|
||||
self.object.opener.filter(activity=self.object)
|
||||
.filter(PermissionBackend.filter_queryset(self.request, Opener, "view")),
|
||||
.filter(PermissionBackend.filter_queryset(self.request, Opener, "view"))
|
||||
.distinct(),
|
||||
]
|
||||
|
||||
def render_to_response(self, context, **response_kwargs):
|
||||
@@ -309,7 +311,7 @@ class ActivityInviteView(ProtectQuerysetMixin, ProtectedCreateView):
|
||||
@transaction.atomic
|
||||
def form_valid(self, form):
|
||||
form.instance.activity = Activity.objects\
|
||||
.filter(PermissionBackend.filter_queryset(self.request, Activity, "view")).get(pk=self.kwargs["pk"])
|
||||
.filter(PermissionBackend.filter_queryset(self.request, Activity, "view")).distinct().get(pk=self.kwargs["pk"])
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_success_url(self, **kwargs):
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -50,6 +50,15 @@ class CustomLoginView(LoginView):
|
||||
self.request.session['permission_mask'] = form.cleaned_data['permission_mask'].rank
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
user_agent = self.request.META.get('HTTP_USER_AGENT', '').lower()
|
||||
|
||||
context['display_appstore_badge'] = 'iphone' in user_agent or 'android' not in user_agent
|
||||
context['display_playstore_badge'] = 'android' in user_agent or 'iphone' not in user_agent
|
||||
|
||||
return context
|
||||
|
||||
|
||||
class UserUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
|
||||
"""
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -927,7 +927,7 @@
|
||||
"note",
|
||||
"transactiontemplate"
|
||||
],
|
||||
"query": "{\"destination\": [\"club\", \"note\"]}",
|
||||
"query": "[\"AND\", {\"destination\": [\"club\", \"note\"]}, {\"category__name\": \"Clubs\"}]",
|
||||
"type": "view",
|
||||
"mask": 2,
|
||||
"field": "",
|
||||
@@ -943,7 +943,7 @@
|
||||
"note",
|
||||
"transactiontemplate"
|
||||
],
|
||||
"query": "{\"destination\": [\"club\", \"note\"]}",
|
||||
"query": "[\"AND\", {\"destination\": [\"club\", \"note\"]}, {\"category__name\": \"Clubs\"}]",
|
||||
"type": "add",
|
||||
"mask": 3,
|
||||
"field": "",
|
||||
@@ -959,7 +959,7 @@
|
||||
"note",
|
||||
"transactiontemplate"
|
||||
],
|
||||
"query": "{\"destination\": [\"club\", \"note\"]}",
|
||||
"query": "[\"AND\", {\"destination\": [\"club\", \"note\"]}, {\"category__name\": \"Clubs\"}]",
|
||||
"type": "change",
|
||||
"mask": 3,
|
||||
"field": "",
|
||||
@@ -3484,7 +3484,23 @@
|
||||
"mask": 1,
|
||||
"permanent": false,
|
||||
"description": "Voir la bouffe servie"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "permission.permission",
|
||||
"pk": 223,
|
||||
"fields": {
|
||||
"model": [
|
||||
"note",
|
||||
"templatecategory"
|
||||
],
|
||||
"query": "{\"name\": \"Clubs\"}",
|
||||
"type": "view",
|
||||
"mask": 2,
|
||||
"field": "",
|
||||
"permanent": false,
|
||||
"description": "Voir la catégorie de bouton Clubs"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "permission.permission",
|
||||
@@ -4896,7 +4912,6 @@
|
||||
19,
|
||||
20,
|
||||
21,
|
||||
27,
|
||||
59,
|
||||
60,
|
||||
61,
|
||||
@@ -4907,6 +4922,7 @@
|
||||
182,
|
||||
184,
|
||||
185,
|
||||
223,
|
||||
239,
|
||||
240,
|
||||
241
|
||||
@@ -5271,6 +5287,12 @@
|
||||
176,
|
||||
177,
|
||||
197,
|
||||
211,
|
||||
212,
|
||||
213,
|
||||
214,
|
||||
215,
|
||||
216,
|
||||
311,
|
||||
319
|
||||
]
|
||||
|
||||
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 |
57
note_kfet/static/img/appstore_badge_fr_preorder.svg
Normal file
57
note_kfet/static/img/appstore_badge_fr_preorder.svg
Normal file
@@ -0,0 +1,57 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Calque_2" data-name="Calque 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 126.5 40">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: #a6a6a6;
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
fill: #fff;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g id="livetype">
|
||||
<g>
|
||||
<g>
|
||||
<path class="cls-1" d="M116.97,0H9.53c-.37,0-.73,0-1.09,0-.31,0-.61,0-.92.01-.67.02-1.34.06-2,.18-.67.12-1.29.32-1.9.63-.6.31-1.15.71-1.62,1.18-.48.47-.88,1.02-1.18,1.62-.31.61-.51,1.23-.63,1.9-.12.66-.16,1.33-.18,2,0,.31-.01.61-.02.92v23.11c0,.31,0,.61.02.92.02.67.06,1.34.18,2,.12.67.31,1.3.63,1.9.3.6.7,1.14,1.18,1.61.47.48,1.02.88,1.62,1.18.61.31,1.23.51,1.9.63.66.12,1.34.16,2,.18.31,0,.61.01.92.01.37,0,.73,0,1.09,0h107.44c.36,0,.72,0,1.08,0,.3,0,.62,0,.92-.01.67-.02,1.34-.06,2-.18.67-.12,1.29-.32,1.91-.63.6-.3,1.14-.7,1.62-1.18.48-.47.87-1.02,1.18-1.61.31-.61.51-1.23.62-1.9.12-.66.16-1.33.19-2,0-.31,0-.61,0-.92,0-.36,0-.72,0-1.09V9.54c0-.37,0-.73,0-1.09,0-.31,0-.61,0-.92-.02-.67-.06-1.34-.19-2-.11-.67-.31-1.29-.62-1.9-.31-.6-.71-1.15-1.18-1.62-.47-.47-1.02-.87-1.62-1.18-.62-.31-1.24-.51-1.91-.63-.66-.12-1.33-.16-2-.18-.3,0-.62-.01-.92-.01-.36,0-.72,0-1.08,0h0Z"/>
|
||||
<path d="M8.44,39.12c-.3,0-.6,0-.9-.01-.56-.02-1.22-.05-1.87-.16-.61-.11-1.15-.29-1.66-.55-.52-.26-.99-.61-1.4-1.02-.41-.41-.75-.87-1.02-1.4-.26-.5-.44-1.05-.54-1.66-.12-.67-.15-1.36-.17-1.88,0-.21-.01-.91-.01-.91V8.44s0-.69.01-.89c.01-.52.04-1.21.17-1.87.11-.61.28-1.16.54-1.66.27-.52.61-.99,1.02-1.4.41-.41.88-.76,1.4-1.02.51-.26,1.06-.44,1.65-.54.67-.12,1.36-.15,1.88-.16h.9s109.6-.01,109.6-.01h.91c.51.03,1.2.06,1.86.18.6.11,1.15.28,1.67.55.51.26.98.61,1.39,1.02.41.41.75.88,1.02,1.4.26.51.43,1.05.54,1.65.12.63.15,1.28.17,1.89,0,.28,0,.59,0,.89,0,.38,0,.73,0,1.09v20.93c0,.36,0,.72,0,1.08,0,.33,0,.62,0,.93-.02.59-.06,1.24-.17,1.85-.1.61-.28,1.16-.54,1.67-.27.52-.61.99-1.02,1.39-.41.42-.88.76-1.4,1.02-.52.26-1.05.44-1.67.55-.64.12-1.3.15-1.87.16-.29,0-.6.01-.9.01h-1.08s-108.53,0-108.53,0Z"/>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path class="cls-2" d="M24.77,20.3c-.03-2.75,2.25-4.09,2.36-4.15-1.29-1.88-3.29-2.14-3.99-2.16-1.68-.18-3.31,1-4.16,1s-2.19-.99-3.61-.96c-1.83.03-3.54,1.09-4.47,2.73-1.93,3.35-.49,8.27,1.36,10.98.93,1.33,2.01,2.81,3.43,2.75,1.39-.06,1.91-.88,3.58-.88s2.14.88,3.59.85c1.49-.02,2.43-1.33,3.32-2.67,1.07-1.52,1.5-3.02,1.52-3.09-.03-.01-2.89-1.1-2.92-4.4Z"/>
|
||||
<path class="cls-2" d="M22.04,12.21c.75-.93,1.26-2.2,1.11-3.49-1.08.05-2.43.75-3.21,1.66-.69.8-1.3,2.12-1.14,3.36,1.21.09,2.46-.61,3.24-1.53Z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="cls-2" d="M37.55,8.73c1.17,0,1.97.81,1.97,1.99s-.83,1.97-2,1.97h-1.38v2.01h-.93v-5.97h2.34ZM36.14,11.88h1.17c.8,0,1.27-.41,1.27-1.15s-.45-1.17-1.27-1.17h-1.17v2.32Z"/>
|
||||
<path class="cls-2" d="M40.73,10.2h.86v.69h.07c.13-.44.63-.77,1.22-.77.13,0,.3.01.4.04v.88c-.07-.02-.34-.05-.5-.05-.67,0-1.15.43-1.15,1.06v2.66h-.89v-4.5Z"/>
|
||||
<path class="cls-2" d="M47.94,13.49c-.2.81-.92,1.3-1.95,1.3-1.29,0-2.08-.88-2.08-2.32s.81-2.35,2.08-2.35,2.01.86,2.01,2.27v.31h-3.18v.05c.03.79.49,1.29,1.2,1.29.54,0,.91-.19,1.07-.55h.86ZM44.81,12.03h2.27c-.02-.71-.45-1.17-1.11-1.17s-1.12.46-1.17,1.17ZM45.45,9.45l1.04-1.42h1.04l-1.16,1.42h-.92Z"/>
|
||||
<path class="cls-2" d="M52.14,11.67c-.1-.44-.47-.76-1.06-.76-.74,0-1.2.57-1.2,1.53s.46,1.56,1.2,1.56c.56,0,.95-.26,1.06-.74h.86c-.12.91-.81,1.53-1.92,1.53-1.31,0-2.11-.88-2.11-2.35s.8-2.32,2.11-2.32c1.13,0,1.81.66,1.93,1.56h-.86Z"/>
|
||||
<path class="cls-2" d="M53.92,12.45c0-1.45.81-2.34,2.12-2.34s2.12.88,2.12,2.34-.81,2.34-2.12,2.34-2.12-.88-2.12-2.34ZM57.25,12.45c0-.98-.44-1.55-1.21-1.55s-1.21.57-1.21,1.55.43,1.55,1.21,1.55,1.21-.57,1.21-1.55Z"/>
|
||||
<path class="cls-2" d="M59.35,10.2h.86v.72h.07c.2-.51.65-.81,1.25-.81s1.04.32,1.24.81h.07c.23-.49.74-.81,1.37-.81.91,0,1.44.55,1.44,1.49v3.1h-.89v-2.87c0-.61-.29-.91-.87-.91s-.95.41-.95.94v2.83h-.87v-2.96c0-.51-.34-.82-.87-.82s-.95.44-.95,1.02v2.75h-.89v-4.5Z"/>
|
||||
<path class="cls-2" d="M67.02,10.2h.86v.72h.07c.2-.51.65-.81,1.25-.81s1.04.32,1.24.81h.07c.23-.49.74-.81,1.37-.81.91,0,1.44.55,1.44,1.49v3.1h-.89v-2.87c0-.61-.29-.91-.87-.91s-.95.41-.95.94v2.83h-.87v-2.96c0-.51-.34-.82-.87-.82s-.95.44-.95,1.02v2.75h-.89v-4.5Z"/>
|
||||
<path class="cls-2" d="M74.43,13.43c0-.81.6-1.28,1.67-1.34l1.22-.07v-.39c0-.48-.31-.74-.92-.74-.5,0-.84.18-.94.5h-.86c.09-.77.82-1.27,1.84-1.27,1.13,0,1.77.56,1.77,1.51v3.08h-.86v-.63h-.07c-.27.45-.76.71-1.35.71-.87,0-1.5-.52-1.5-1.35ZM77.33,13.04v-.38l-1.1.07c-.62.04-.9.25-.9.65s.35.64.83.64c.67,0,1.17-.43,1.17-.98Z"/>
|
||||
<path class="cls-2" d="M79.6,10.2h.86v.72h.07c.22-.5.67-.8,1.34-.8,1,0,1.56.6,1.56,1.67v2.92h-.89v-2.69c0-.72-.31-1.08-.97-1.08s-1.08.44-1.08,1.14v2.63h-.89v-4.5Z"/>
|
||||
<path class="cls-2" d="M84.58,12.45c0-1.42.73-2.32,1.87-2.32.62,0,1.14.29,1.38.79h.07v-2.47h.89v6.26h-.85v-.71h-.07c-.27.49-.79.79-1.41.79-1.15,0-1.87-.9-1.87-2.33ZM85.5,12.45c0,.96.45,1.53,1.2,1.53s1.21-.58,1.21-1.53-.47-1.53-1.21-1.53-1.2.58-1.2,1.53Z"/>
|
||||
<path class="cls-2" d="M94.05,13.49c-.2.81-.92,1.3-1.95,1.3-1.29,0-2.08-.88-2.08-2.32s.81-2.35,2.08-2.35,2.01.86,2.01,2.27v.31h-3.18v.05c.03.79.49,1.29,1.2,1.29.54,0,.91-.19,1.07-.55h.86ZM90.92,12.03h2.27c-.02-.71-.45-1.17-1.11-1.17s-1.12.46-1.17,1.17Z"/>
|
||||
<path class="cls-2" d="M95.3,10.2h.86v.69h.07c.13-.44.63-.77,1.22-.77.13,0,.3.01.4.04v.88c-.07-.02-.34-.05-.5-.05-.67,0-1.15.43-1.15,1.06v2.66h-.89v-4.5Z"/>
|
||||
<path class="cls-2" d="M102.9,10.11c1.01,0,1.67.47,1.76,1.27h-.85c-.08-.33-.41-.54-.91-.54s-.87.24-.87.59c0,.27.23.44.72.55l.75.17c.86.2,1.26.57,1.26,1.23,0,.85-.79,1.41-1.87,1.41s-1.77-.48-1.85-1.28h.89c.11.35.44.56.98.56s.95-.25.95-.61c0-.27-.21-.44-.66-.55l-.79-.18c-.86-.2-1.25-.59-1.25-1.26,0-.8.73-1.36,1.75-1.36Z"/>
|
||||
<path class="cls-2" d="M109.74,14.7h-.86v-.72h-.07c-.22.51-.68.8-1.36.8-1,0-1.55-.61-1.55-1.67v-2.92h.89v2.69c0,.73.29,1.08.95,1.08.72,0,1.11-.43,1.11-1.13v-2.63h.89v4.5Z"/>
|
||||
<path class="cls-2" d="M111.16,10.2h.86v.69h.07c.13-.44.63-.77,1.22-.77.13,0,.3.01.4.04v.88c-.07-.02-.34-.05-.5-.05-.67,0-1.15.43-1.15,1.06v2.66h-.89v-4.5Z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<path class="cls-2" d="M35.2,18.07h1.86v12.42h-1.86v-12.42Z"/>
|
||||
<path class="cls-2" d="M39.3,22.61l1.02-4.54h1.81l-1.23,4.54h-1.59Z"/>
|
||||
<path class="cls-2" d="M49.15,27.13h-4.73l-1.14,3.36h-2l4.48-12.42h2.08l4.48,12.42h-2.04l-1.14-3.36ZM44.9,25.58h3.75l-1.85-5.45h-.05l-1.85,5.45Z"/>
|
||||
<path class="cls-2" d="M62,25.96c0,2.81-1.51,4.62-3.78,4.62-1.29,0-2.31-.58-2.85-1.58h-.04v4.48h-1.86v-12.05h1.8v1.51h.03c.52-.97,1.62-1.6,2.88-1.6,2.3,0,3.81,1.82,3.81,4.62ZM60.09,25.96c0-1.83-.95-3.04-2.39-3.04s-2.38,1.23-2.38,3.04.96,3.05,2.38,3.05,2.39-1.2,2.39-3.05Z"/>
|
||||
<path class="cls-2" d="M71.97,25.96c0,2.81-1.51,4.62-3.78,4.62-1.29,0-2.31-.58-2.85-1.58h-.04v4.48h-1.86v-12.05h1.8v1.51h.03c.52-.97,1.62-1.6,2.88-1.6,2.3,0,3.81,1.82,3.81,4.62ZM70.06,25.96c0-1.83-.95-3.04-2.39-3.04s-2.38,1.23-2.38,3.04.96,3.05,2.38,3.05,2.39-1.2,2.39-3.05Z"/>
|
||||
<path class="cls-2" d="M78.55,27.03c.14,1.23,1.33,2.04,2.97,2.04s2.69-.81,2.69-1.92c0-.96-.68-1.54-2.29-1.94l-1.61-.39c-2.28-.55-3.34-1.62-3.34-3.35,0-2.14,1.87-3.61,4.52-3.61s4.42,1.47,4.48,3.61h-1.88c-.11-1.24-1.14-1.99-2.63-1.99s-2.52.76-2.52,1.86c0,.88.65,1.39,2.25,1.79l1.37.34c2.55.6,3.61,1.63,3.61,3.44,0,2.32-1.85,3.78-4.79,3.78-2.75,0-4.61-1.42-4.73-3.67h1.9Z"/>
|
||||
<path class="cls-2" d="M90.19,19.29v2.14h1.72v1.47h-1.72v4.99c0,.78.34,1.14,1.1,1.14.19,0,.49-.03.61-.04v1.46c-.21.05-.62.09-1.03.09-1.83,0-2.55-.69-2.55-2.44v-5.19h-1.32v-1.47h1.32v-2.14h1.87Z"/>
|
||||
<path class="cls-2" d="M92.91,25.96c0-2.85,1.68-4.64,4.29-4.64s4.29,1.79,4.29,4.64-1.66,4.64-4.29,4.64-4.29-1.78-4.29-4.64ZM99.6,25.96c0-1.95-.9-3.11-2.4-3.11s-2.4,1.16-2.4,3.11.9,3.11,2.4,3.11,2.4-1.14,2.4-3.11Z"/>
|
||||
<path class="cls-2" d="M103.03,21.43h1.77v1.54h.04c.28-1.02,1.11-1.64,2.18-1.64.27,0,.49.04.64.07v1.74c-.15-.06-.47-.11-.83-.11-1.2,0-1.94.81-1.94,2.08v5.37h-1.86v-9.05Z"/>
|
||||
<path class="cls-2" d="M116.23,27.83c-.25,1.64-1.85,2.77-3.9,2.77-2.63,0-4.27-1.76-4.27-4.6s1.64-4.68,4.19-4.68,4.08,1.72,4.08,4.47v.64h-6.39v.11c0,1.55.97,2.56,2.44,2.56,1.03,0,1.84-.49,2.09-1.27h1.76ZM109.94,25.12h4.53c-.04-1.39-.93-2.3-2.22-2.3s-2.21.93-2.31,2.3Z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 8.2 KiB |
63
note_kfet/static/img/playstore_badge_fr.svg
Normal file
63
note_kfet/static/img/playstore_badge_fr.svg
Normal file
@@ -0,0 +1,63 @@
|
||||
<?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.2.1, SVG Export Plug-In . SVG Version: 2.1.0 Build 116) -->
|
||||
<defs>
|
||||
<style>
|
||||
.st0 {
|
||||
fill: #4285f4;
|
||||
}
|
||||
|
||||
.st1 {
|
||||
isolation: isolate;
|
||||
}
|
||||
|
||||
.st2 {
|
||||
fill: #a6a6a6;
|
||||
}
|
||||
|
||||
.st3 {
|
||||
fill: #34a853;
|
||||
}
|
||||
|
||||
.st4 {
|
||||
fill: #fbbc04;
|
||||
}
|
||||
|
||||
.st5 {
|
||||
fill: #fff;
|
||||
}
|
||||
|
||||
.st6 {
|
||||
fill: #ea4335;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g>
|
||||
<rect width="135" height="40" rx="5" ry="5"/>
|
||||
<path class="st2" d="M130,.8c2.3,0,4.2,1.9,4.2,4.2v30c0,2.3-1.9,4.2-4.2,4.2H5c-2.3,0-4.2-1.9-4.2-4.2V5c0-2.3,1.9-4.2,4.2-4.2h125M130,0H5C2.2,0,0,2.2,0,5v30c0,2.8,2.2,5,5,5h125c2.8,0,5-2.2,5-5V5c0-2.8-2.2-5-5-5h0Z"/>
|
||||
<path class="st5" d="M68.1,21.8c-2.4,0-4.3,1.8-4.3,4.3s1.9,4.3,4.3,4.3,4.3-1.8,4.3-4.3-1.9-4.3-4.3-4.3ZM68.1,28.6c-1.3,0-2.4-1.1-2.4-2.6s1.1-2.6,2.4-2.6,2.4,1,2.4,2.6-1.1,2.6-2.4,2.6ZM58.8,21.8c-2.4,0-4.3,1.8-4.3,4.3s1.9,4.3,4.3,4.3,4.3-1.8,4.3-4.3-1.9-4.3-4.3-4.3ZM58.8,28.6c-1.3,0-2.4-1.1-2.4-2.6s1.1-2.6,2.4-2.6,2.4,1,2.4,2.6-1.1,2.6-2.4,2.6ZM47.7,23.1v1.8h4.3c-.1,1-.5,1.8-1,2.3-.6.6-1.6,1.3-3.3,1.3-2.7,0-4.7-2.1-4.7-4.8s2.1-4.8,4.7-4.8,2.5.6,3.3,1.3l1.3-1.3c-1.1-1-2.5-1.8-4.5-1.8-3.6,0-6.7,3-6.7,6.6s3.1,6.6,6.7,6.6,3.4-.6,4.6-1.9c1.2-1.2,1.6-2.9,1.6-4.2s0-.8,0-1.1h-6.1,0ZM93.1,24.5c-.4-1-1.4-2.7-3.6-2.7s-4,1.7-4,4.3,1.8,4.3,4.2,4.3,3.1-1.2,3.5-1.9l-1.4-1c-.5.7-1.1,1.2-2.1,1.2s-1.6-.4-2.1-1.3l5.7-2.4-.2-.5h0ZM87.3,25.9c0-1.6,1.3-2.5,2.2-2.5s1.4.4,1.6.9c0,0-3.8,1.6-3.8,1.6ZM82.6,30h1.9v-12.5h-1.9v12.5ZM79.6,22.7h0c-.4-.5-1.2-1-2.2-1-2.1,0-4.1,1.9-4.1,4.3s1.9,4.2,4.1,4.2,1.8-.5,2.2-1h0v.6c0,1.6-.9,2.5-2.3,2.5s-1.9-.8-2.1-1.5l-1.6.7c.5,1.1,1.7,2.5,3.8,2.5s4-1.3,4-4.4v-7.6h-1.8s0,.7,0,.7ZM77.4,28.6c-1.3,0-2.4-1.1-2.4-2.6s1.1-2.6,2.4-2.6,2.3,1.1,2.3,2.6-1,2.6-2.3,2.6ZM101.8,17.5h-4.5v12.5h1.9v-4.7h2.6c2.1,0,4.1-1.5,4.1-3.9s-2-3.9-4.1-3.9ZM101.9,23.5h-2.7v-4.3h2.7c1.4,0,2.2,1.2,2.2,2.1s-.8,2.1-2.2,2.1h0ZM113.4,21.7c-1.4,0-2.8.6-3.3,1.9l1.7.7c.4-.7,1-.9,1.7-.9s1.9.6,2,1.6h0c-.3,0-1.1-.4-1.9-.4-1.8,0-3.6,1-3.6,2.8s1.5,2.8,3.1,2.8,1.9-.6,2.4-1.2h0v1h1.8v-4.8c0-2.2-1.7-3.5-3.8-3.5h0ZM113.2,28.6c-.6,0-1.5-.3-1.5-1.1s1.1-1.3,2-1.3,1.2.2,1.7.4c-.1,1.2-1.1,2-2.2,2ZM123.7,22l-2.1,5.4h0l-2.2-5.4h-2l3.3,7.6-1.9,4.2h1.9l5.1-11.8h-2.1ZM106.9,30h1.9v-12.5h-1.9v12.5Z"/>
|
||||
<g>
|
||||
<path class="st6" d="M20.7,19.4l-10.6,11.3s0,0,0,0c.3,1.2,1.4,2.1,2.8,2.1s1-.1,1.5-.4h0s12-6.9,12-6.9l-5.6-6.1Z"/>
|
||||
<path class="st4" d="M31.5,17.5h0s-5.2-3-5.2-3l-5.8,5.2,5.8,5.8,5.1-3c.9-.5,1.5-1.4,1.5-2.5s-.6-2-1.5-2.5h0Z"/>
|
||||
<path class="st0" d="M10.1,9.3c0,.2,0,.5,0,.7v20c0,.3,0,.5,0,.7l11-11s-11-10.4-11-10.4Z"/>
|
||||
<path class="st3" d="M20.8,20l5.5-5.5-12-6.9c-.4-.3-.9-.4-1.5-.4-1.3,0-2.5.9-2.8,2.1h0s10.7,10.7,10.7,10.7h0Z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g class="st1">
|
||||
<g class="st1">
|
||||
<path class="st5" d="M41.8,6.9h2c.6,0,1.2.1,1.7.4s.9.6,1.1,1.1c.3.5.4,1,.4,1.6s-.1,1.1-.4,1.6c-.3.5-.6.8-1.1,1.1s-1,.4-1.7.4h-2v-6.2ZM43.8,12.2c.7,0,1.2-.2,1.6-.6.4-.4.6-.9.6-1.6s-.2-1.2-.6-1.6c-.4-.4-.9-.6-1.6-.6h-1v4.4h1Z"/>
|
||||
<path class="st5" d="M48.1,6.9h1v6.2h-1v-6.2Z"/>
|
||||
<path class="st5" d="M50.9,12.8c-.4-.3-.7-.7-.9-1.3l.9-.4c0,.3.3.6.5.8s.5.3.8.3.6,0,.8-.2c.2-.2.3-.4.3-.7s0-.5-.3-.6-.5-.3-1-.5h-.4c-.4-.3-.8-.5-1.1-.8-.3-.3-.4-.6-.4-1.1s0-.6.3-.9c.2-.3.4-.5.7-.6.3-.2.6-.2,1-.2.5,0,1,.1,1.3.4s.5.6.7.9l-.9.4c0-.2-.2-.4-.4-.5-.2-.2-.4-.2-.7-.2s-.5,0-.7.2-.3.3-.3.6,0,.4.3.5c.2.1.4.3.8.4h.4c.5.3.9.6,1.2.9s.4.7.4,1.2-.1.7-.3,1c-.2.3-.5.5-.8.6-.3.1-.7.2-1,.2-.5,0-1-.2-1.4-.5Z"/>
|
||||
<path class="st5" d="M55.5,6.9h2.2c.4,0,.7,0,1,.2.3.2.6.4.7.7s.3.6.3,1,0,.7-.3,1-.4.5-.7.7c-.3.2-.6.2-1,.2h-1.2v2.4h-1v-6.2ZM57.7,9.8c.2,0,.4,0,.6-.1.2,0,.3-.2.4-.4,0-.2.1-.3.1-.5s0-.3-.1-.5c0-.2-.2-.3-.4-.4-.2,0-.3-.1-.6-.1h-1.2v2h1.2Z"/>
|
||||
<path class="st5" d="M61.9,12.8c-.5-.3-.9-.7-1.2-1.2-.3-.5-.4-1-.4-1.6s.1-1.1.4-1.6c.3-.5.7-.9,1.2-1.2.5-.3,1-.4,1.6-.4s1.1.1,1.6.4c.5.3.9.7,1.2,1.2.3.5.4,1,.4,1.6s-.1,1.1-.4,1.6c-.3.5-.7.9-1.2,1.2-.5.3-1,.4-1.6.4s-1.2-.1-1.6-.4ZM64.6,12c.3-.2.6-.5.8-.8.2-.4.3-.8.3-1.2s0-.9-.3-1.2-.5-.6-.8-.8-.7-.3-1.1-.3-.8,0-1.1.3c-.3.2-.6.5-.8.8s-.3.8-.3,1.2.1.9.3,1.2c.2.4.5.6.8.8.3.2.7.3,1.1.3s.8,0,1.1-.3Z"/>
|
||||
<path class="st5" d="M67.9,6.9h1.2l2.8,4.5h0v-1.2c0,0,0-3.3,0-3.3h1v6.2h-1l-2.9-4.8h0v1.2c0,0,0,3.6,0,3.6h-1v-6.2Z"/>
|
||||
<path class="st5" d="M74.2,6.9h1v6.2h-1v-6.2Z"/>
|
||||
<path class="st5" d="M76.6,6.9h2.3c.3,0,.6,0,.9.2.3.1.5.3.7.6.2.3.3.5.3.8s0,.6-.2.8c-.2.2-.4.4-.6.5h0c.3.2.6.3.8.6s.3.6.3.9,0,.6-.3.9c-.2.3-.4.5-.7.6-.3.1-.6.2-1,.2h-2.4v-6.2ZM78.9,9.5c.3,0,.5,0,.7-.3.2-.2.3-.4.3-.6s0-.4-.2-.6c-.2-.2-.4-.3-.6-.3h-1.4v1.7h1.3ZM79,12.2c.3,0,.5,0,.7-.3.2-.2.3-.4.3-.6s0-.5-.3-.6c-.2-.2-.4-.3-.7-.3h-1.4v1.8h1.5Z"/>
|
||||
<path class="st5" d="M82,6.9h1v5.3h2.7v.9h-3.6v-6.2Z"/>
|
||||
<path class="st5" d="M86.7,6.9h3.8v.9h-2.8v1.7h2.5v.9h-2.5v1.7h2.8v.9h-3.8v-6.2Z"/>
|
||||
<path class="st5" d="M93.9,12.8c-.4-.3-.7-.7-.9-1.3l.9-.4c0,.3.3.6.5.8.2.2.5.3.8.3s.6,0,.8-.2c.2-.2.3-.4.3-.7s0-.5-.3-.6-.5-.3-1-.5h-.4c-.4-.3-.8-.5-1.1-.8-.3-.3-.4-.6-.4-1.1s0-.6.3-.9.4-.5.7-.6.6-.2,1-.2c.5,0,1,.1,1.3.4.3.3.5.6.7.9l-.9.4c0-.2-.2-.4-.4-.5-.2-.2-.4-.2-.7-.2s-.5,0-.7.2-.3.3-.3.6,0,.4.3.5c.2.1.4.3.8.4h.4c.5.3.9.6,1.2.9s.4.7.4,1.2-.1.7-.3,1c-.2.3-.5.5-.8.6-.3.1-.7.2-1,.2-.5,0-1-.2-1.4-.5Z"/>
|
||||
<path class="st5" d="M99.5,13c-.4-.2-.6-.5-.8-.9s-.3-.8-.3-1.3v-3.8h1v3.9c0,.5.1.8.4,1.1s.6.4,1,.4.8-.1,1-.4c.2-.3.4-.7.4-1.1v-3.9h1v3.8c0,.5,0,.9-.3,1.3s-.5.7-.8.9c-.4.2-.8.3-1.3.3s-.9-.1-1.2-.3Z"/>
|
||||
<path class="st5" d="M104.4,6.9h2.2c.4,0,.7,0,1,.2.3.2.5.4.7.7.2.3.3.6.3,1s-.1.8-.4,1.1c-.3.3-.6.5-1,.7h0s1.7,2.5,1.7,2.5h0c0,0-1.1,0-1.1,0l-1.6-2.4h-.7v2.4h-1v-6.2ZM106.5,9.8c.3,0,.5,0,.7-.3s.3-.4.3-.7,0-.3-.1-.5c0-.2-.2-.3-.3-.4-.2,0-.3-.1-.5-.1h-1.2v2h1.2Z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.8 KiB |
66
note_kfet/static/img/playstore_badge_fr_preorder.svg
Normal file
66
note_kfet/static/img/playstore_badge_fr_preorder.svg
Normal file
@@ -0,0 +1,66 @@
|
||||
<?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.2.1, SVG Export Plug-In . SVG Version: 2.1.0 Build 116) -->
|
||||
<defs>
|
||||
<style>
|
||||
.st0 {
|
||||
fill: #4285f4;
|
||||
}
|
||||
|
||||
.st1 {
|
||||
isolation: isolate;
|
||||
}
|
||||
|
||||
.st2 {
|
||||
fill: #a6a6a6;
|
||||
}
|
||||
|
||||
.st3 {
|
||||
fill: #34a853;
|
||||
}
|
||||
|
||||
.st4 {
|
||||
fill: #fbbc04;
|
||||
}
|
||||
|
||||
.st5 {
|
||||
fill: #fff;
|
||||
}
|
||||
|
||||
.st6 {
|
||||
fill: #ea4335;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g>
|
||||
<rect width="135" height="40" rx="5" ry="5"/>
|
||||
<path class="st2" d="M130,.8c2.3,0,4.2,1.9,4.2,4.2v30c0,2.3-1.9,4.2-4.2,4.2H5c-2.3,0-4.2-1.9-4.2-4.2V5c0-2.3,1.9-4.2,4.2-4.2h125M130,0H5C2.2,0,0,2.2,0,5v30c0,2.8,2.2,5,5,5h125c2.8,0,5-2.2,5-5V5c0-2.8-2.2-5-5-5h0Z"/>
|
||||
<path class="st5" d="M68.1,21.8c-2.4,0-4.3,1.8-4.3,4.3s1.9,4.3,4.3,4.3,4.3-1.8,4.3-4.3-1.9-4.3-4.3-4.3ZM68.1,28.6c-1.3,0-2.4-1.1-2.4-2.6s1.1-2.6,2.4-2.6,2.4,1,2.4,2.6-1.1,2.6-2.4,2.6ZM58.8,21.8c-2.4,0-4.3,1.8-4.3,4.3s1.9,4.3,4.3,4.3,4.3-1.8,4.3-4.3-1.9-4.3-4.3-4.3ZM58.8,28.6c-1.3,0-2.4-1.1-2.4-2.6s1.1-2.6,2.4-2.6,2.4,1,2.4,2.6-1.1,2.6-2.4,2.6ZM47.7,23.1v1.8h4.3c-.1,1-.5,1.8-1,2.3-.6.6-1.6,1.3-3.3,1.3-2.7,0-4.7-2.1-4.7-4.8s2.1-4.8,4.7-4.8,2.5.6,3.3,1.3l1.3-1.3c-1.1-1-2.5-1.8-4.5-1.8-3.6,0-6.7,3-6.7,6.6s3.1,6.6,6.7,6.6,3.4-.6,4.6-1.9c1.2-1.2,1.6-2.9,1.6-4.2s0-.8,0-1.1h-6.1,0ZM93.1,24.5c-.4-1-1.4-2.7-3.6-2.7s-4,1.7-4,4.3,1.8,4.3,4.2,4.3,3.1-1.2,3.5-1.9l-1.4-1c-.5.7-1.1,1.2-2.1,1.2s-1.6-.4-2.1-1.3l5.7-2.4-.2-.5h0ZM87.3,25.9c0-1.6,1.3-2.5,2.2-2.5s1.4.4,1.6.9c0,0-3.8,1.6-3.8,1.6ZM82.6,30h1.9v-12.5h-1.9v12.5ZM79.6,22.7h0c-.4-.5-1.2-1-2.2-1-2.1,0-4.1,1.9-4.1,4.3s1.9,4.2,4.1,4.2,1.8-.5,2.2-1h0v.6c0,1.6-.9,2.5-2.3,2.5s-1.9-.8-2.1-1.5l-1.6.7c.5,1.1,1.7,2.5,3.8,2.5s4-1.3,4-4.4v-7.6h-1.8s0,.7,0,.7ZM77.4,28.6c-1.3,0-2.4-1.1-2.4-2.6s1.1-2.6,2.4-2.6,2.3,1.1,2.3,2.6-1,2.6-2.3,2.6ZM101.8,17.5h-4.5v12.5h1.9v-4.7h2.6c2.1,0,4.1-1.5,4.1-3.9s-2-3.9-4.1-3.9ZM101.9,23.5h-2.7v-4.3h2.7c1.4,0,2.2,1.2,2.2,2.1s-.8,2.1-2.2,2.1h0ZM113.4,21.7c-1.4,0-2.8.6-3.3,1.9l1.7.7c.4-.7,1-.9,1.7-.9s1.9.6,2,1.6h0c-.3,0-1.1-.4-1.9-.4-1.8,0-3.6,1-3.6,2.8s1.5,2.8,3.1,2.8,1.9-.6,2.4-1.2h0v1h1.8v-4.8c0-2.2-1.7-3.5-3.8-3.5h0ZM113.2,28.6c-.6,0-1.5-.3-1.5-1.1s1.1-1.3,2-1.3,1.2.2,1.7.4c-.1,1.2-1.1,2-2.2,2ZM123.7,22l-2.1,5.4h0l-2.2-5.4h-2l3.3,7.6-1.9,4.2h1.9l5.1-11.8h-2.1ZM106.9,30h1.9v-12.5h-1.9v12.5Z"/>
|
||||
<g>
|
||||
<path class="st6" d="M20.7,19.4l-10.6,11.3s0,0,0,0c.3,1.2,1.4,2.1,2.8,2.1s1-.1,1.5-.4h0s12-6.9,12-6.9l-5.6-6.1Z"/>
|
||||
<path class="st4" d="M31.5,17.5h0s-5.2-3-5.2-3l-5.8,5.2,5.8,5.8,5.1-3c.9-.5,1.5-1.4,1.5-2.5s-.6-2-1.5-2.5h0Z"/>
|
||||
<path class="st0" d="M10.1,9.3c0,.2,0,.5,0,.7v20c0,.3,0,.5,0,.7l11-11s-11-10.4-11-10.4Z"/>
|
||||
<path class="st3" d="M20.8,20l5.5-5.5-12-6.9c-.4-.3-.9-.4-1.5-.4-1.3,0-2.5.9-2.8,2.1h0s10.7,10.7,10.7,10.7h0Z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g class="st1">
|
||||
<g class="st1">
|
||||
<path class="st5" d="M42.1,12.8c-.4-.3-.6-.7-.8-1.2l.8-.3c0,.3.2.6.5.8.2.2.5.3.8.3s.5,0,.7-.2c.2-.1.3-.3.3-.6s0-.4-.3-.6c-.2-.2-.5-.3-.9-.5h-.4c-.4-.3-.7-.5-1-.7-.3-.3-.4-.6-.4-1s0-.5.2-.8c.2-.2.4-.4.6-.6.3-.1.6-.2.9-.2.5,0,.9.1,1.2.4.3.2.5.5.6.8l-.8.3c0-.2-.2-.3-.3-.5s-.4-.2-.6-.2-.5,0-.7.2c-.2.1-.3.3-.3.5s0,.4.2.5c.2.1.4.3.8.4h.4c.5.3.9.5,1.1.8.3.3.4.6.4,1.1s0,.7-.3.9c-.2.3-.4.4-.7.6-.3.1-.6.2-.9.2-.5,0-.9-.1-1.3-.4Z"/>
|
||||
<path class="st5" d="M46.4,7.4h3.5v.9h-2.6v1.6h2.3v.8h-2.3v1.6h2.6v.9h-3.5v-5.7Z"/>
|
||||
<path class="st5" d="M52.8,7.4h2c.3,0,.6,0,.9.2.3.1.5.4.7.6s.3.6.3.9,0,.6-.3.9-.4.5-.7.6c-.3.1-.6.2-.9.2h-1.1v2.2h-.9v-5.7ZM54.8,10.1c.2,0,.4,0,.5-.1s.3-.2.3-.3.1-.3.1-.4,0-.3-.1-.4-.2-.2-.3-.3c-.1,0-.3-.1-.5-.1h-1.1v1.8h1.1Z"/>
|
||||
<path class="st5" d="M57.6,7.4h2c.3,0,.7,0,.9.2s.5.4.7.6.2.6.2.9-.1.7-.4,1-.6.5-.9.6h0s1.6,2.3,1.6,2.3h0s-1,0-1,0l-1.5-2.2h-.7v2.2h-.9v-5.7ZM59.6,10.1c.3,0,.5,0,.7-.3.2-.2.3-.4.3-.7s0-.3-.1-.4-.2-.3-.3-.3-.3-.1-.5-.1h-1.1v1.8h1.1Z"/>
|
||||
<path class="st5" d="M62.5,7.4h3.5v.9h-2.6v1.6h2.3v.8h-2.3v1.6h2.6v.9h-3.5v-5.7ZM64.2,5.9h1l-.6,1.1h-.7l.4-1.1Z"/>
|
||||
<path class="st5" d="M67.1,7.4h.9v5.7h-.9v-5.7Z"/>
|
||||
<path class="st5" d="M69.3,7.4h1.1l2.6,4.2h0v-1.1s0-3.1,0-3.1h.9v5.7h-.9l-2.7-4.4h0v1.1s0,3.3,0,3.3h-.9v-5.7Z"/>
|
||||
<path class="st5" d="M75.5,12.8c-.4-.3-.6-.7-.8-1.2l.8-.3c0,.3.2.6.5.8.2.2.5.3.8.3s.5,0,.7-.2c.2-.1.3-.3.3-.6s0-.4-.3-.6c-.2-.2-.5-.3-.9-.5h-.4c-.4-.3-.7-.5-1-.7s-.4-.6-.4-1,0-.5.2-.8c.2-.2.4-.4.6-.6.3-.1.6-.2.9-.2.5,0,.9.1,1.2.4.3.2.5.5.6.8l-.8.3c0-.2-.2-.3-.3-.5-.2-.1-.4-.2-.6-.2s-.5,0-.7.2c-.2.1-.3.3-.3.5s0,.4.2.5.4.3.8.4h.4c.5.3.9.5,1.1.8.3.3.4.6.4,1.1s0,.7-.3.9c-.2.3-.4.4-.7.6-.3.1-.6.2-.9.2-.5,0-.9-.1-1.3-.4Z"/>
|
||||
<path class="st5" d="M80.9,12.9c-.5-.3-.8-.6-1.1-1.1s-.4-1-.4-1.5.1-1.1.4-1.5.6-.8,1.1-1.1,1-.4,1.5-.4.8,0,1.2.2c.4.2.7.4.9.7l-.6.6c-.2-.2-.4-.4-.7-.5-.2-.1-.5-.2-.8-.2s-.7,0-1.1.3c-.3.2-.6.4-.8.7-.2.3-.3.7-.3,1.1s0,.8.3,1.1c.2.3.4.6.8.7s.7.3,1.1.3c.6,0,1.2-.3,1.6-.8l.6.6c-.3.3-.6.6-1,.8-.4.2-.8.3-1.3.3s-1.1-.1-1.5-.4Z"/>
|
||||
<path class="st5" d="M85.7,7.4h2c.3,0,.7,0,.9.2.3.1.5.4.7.6.2.3.2.6.2.9s-.1.7-.4,1-.6.5-.9.6h0s1.6,2.3,1.6,2.3h0s-1,0-1,0l-1.5-2.2h-.7v2.2h-.9v-5.7ZM87.7,10.1c.3,0,.5,0,.7-.3.2-.2.3-.4.3-.7s0-.3-.1-.4c0-.1-.2-.3-.3-.3s-.3-.1-.5-.1h-1.1v1.8h1.1Z"/>
|
||||
<path class="st5" d="M90.6,7.4h.9v5.7h-.9v-5.7Z"/>
|
||||
<path class="st5" d="M92.8,7.4h2c.3,0,.7,0,.9.2.3.1.5.4.7.6.2.3.2.6.2.9s-.1.7-.4,1-.6.5-.9.6h0s1.6,2.3,1.6,2.3h0s-1,0-1,0l-1.5-2.2h-.7v2.2h-.9v-5.7ZM94.8,10.1c.3,0,.5,0,.7-.3s.3-.4.3-.7,0-.3-.1-.4c0-.1-.2-.3-.3-.3s-.3-.1-.5-.1h-1.1v1.8h1.1Z"/>
|
||||
<path class="st5" d="M97.7,7.4h3.5v.9h-2.6v1.6h2.3v.8h-2.3v1.6h2.6v.9h-3.5v-5.7Z"/>
|
||||
<path class="st5" d="M104.3,12.8c-.4-.3-.6-.7-.8-1.2l.8-.3c0,.3.2.6.5.8.2.2.5.3.8.3s.5,0,.7-.2c.2-.1.3-.3.3-.6s0-.4-.3-.6c-.2-.2-.5-.3-.9-.5h-.4c-.4-.3-.7-.5-1-.7s-.4-.6-.4-1,0-.5.2-.8c.2-.2.4-.4.6-.6.3-.1.6-.2.9-.2.5,0,.9.1,1.2.4.3.2.5.5.6.8l-.8.3c0-.2-.2-.3-.3-.5-.2-.1-.4-.2-.6-.2s-.5,0-.7.2c-.2.1-.3.3-.3.5s0,.4.2.5.4.3.8.4h.4c.5.3.9.5,1.1.8.3.3.4.6.4,1.1s0,.7-.3.9c-.2.3-.4.4-.7.6-.3.1-.6.2-.9.2-.5,0-.9-.1-1.3-.4Z"/>
|
||||
<path class="st5" d="M109.5,13c-.3-.2-.6-.5-.8-.8-.2-.4-.3-.8-.3-1.2v-3.5h.9v3.6c0,.4.1.8.3,1,.2.3.5.4.9.4s.7-.1.9-.4c.2-.3.3-.6.3-1v-3.6h.9v3.5c0,.5,0,.9-.3,1.2-.2.4-.4.6-.8.8-.3.2-.7.3-1.2.3s-.8,0-1.1-.3Z"/>
|
||||
<path class="st5" d="M114,7.4h2c.3,0,.7,0,.9.2.3.1.5.4.7.6.2.3.2.6.2.9s-.1.7-.4,1-.6.5-.9.6h0s1.6,2.3,1.6,2.3h0s-1,0-1,0l-1.5-2.2h-.7v2.2h-.9v-5.7ZM116,10.1c.3,0,.5,0,.7-.3s.3-.4.3-.7,0-.3-.1-.4c0-.1-.2-.3-.3-.3s-.3-.1-.5-.1h-1.1v1.8h1.1Z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.4 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,23 @@ 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">
|
||||
{% now "Ymd" as current_date_str %}
|
||||
|
||||
{% if display_appstore_badge %}
|
||||
<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/' %}{% if current_date_str < '20260201' %}appstore_badge_fr_preorder.svg{% else %}appstore_badge_fr.svg{% endif %}"
|
||||
alt="{% trans 'Download on the AppStore' %}" style="height: 50px;">
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if display_playstore_badge %}
|
||||
<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/' %}{% if current_date_str < '20260201' %}playstore_badge_fr_preorder.svg{% else %}playstore_badge_fr.svg{% endif %}"
|
||||
alt="{% trans 'Get it on Google Play' %}" style="height: 50px;">
|
||||
</a>
|
||||
{% endif %}
|
||||
</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