mirror of
				https://gitlab.crans.org/bde/nk20
				synced 2025-10-31 07:49:57 +01:00 
			
		
		
		
	Compare commits
	
		
			12 Commits
		
	
	
		
			note_sheet
			...
			app_downlo
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 04001202f2 | ||
|  | af36d1427a | ||
|  | 75a59e0a7a | ||
|  | af39bf7068 | ||
|  | 4c40566513 | ||
|  | 7c45b59298 | ||
|  | 418268db27 | ||
|  | 73045586a3 | ||
|  | 22d668a75c | ||
|  | 5dfa12fad2 | ||
|  | 5af69f719d | ||
|  | 4f6b1d5b6c | 
| @@ -48,15 +48,5 @@ | ||||
|             "can_invite": true, | ||||
|             "guest_entry_fee": 0 | ||||
|         } | ||||
|     }, | ||||
|     { | ||||
|         "model": "activity.activitytype", | ||||
|         "pk": 8, | ||||
|         "fields": { | ||||
|             "name": "Perm bouffe", | ||||
|             "manage_entries": false, | ||||
|             "can_invite": false, | ||||
|             "guest_entry_fee": 0 | ||||
|         } | ||||
|     } | ||||
| ] | ||||
| ] | ||||
|   | ||||
| @@ -66,10 +66,6 @@ SPDX-License-Identifier: GPL-3.0-or-later | ||||
|             <a class="btn btn-warning btn-sm my-1" href="{% url 'activity:activity_entry' pk=activity.pk %}"> {% trans "Entry page" %}</a> | ||||
|         {% endif %} | ||||
|  | ||||
|         {% if activity.activity_type.name == "Perm bouffe" %} | ||||
|             <a class="btn btn-warning btn-sm my-1" href="{% url 'food:dish_list' activity_pk=activity.pk %}"> {% trans "Dish page" %}</a> | ||||
|         {% endif %} | ||||
|  | ||||
|         {% if request.path_info == activity_detail_url %} | ||||
|             {% if activity.valid and ".change__open"|has_perm:activity %} | ||||
|                 <a class="btn btn-warning btn-sm my-1" id="open_activity"> {% if activity.open %}{% trans "close"|capfirst %}{% else %}{% trans "open"|capfirst %}{% endif %}</a> | ||||
|   | ||||
| @@ -3,7 +3,7 @@ | ||||
|  | ||||
| from rest_framework import serializers | ||||
|  | ||||
| from ..models import Allergen, Food, BasicFood, TransformedFood, QRCode, Dish, Supplement, Order, FoodTransaction | ||||
| from ..models import Allergen, Food, BasicFood, TransformedFood, QRCode | ||||
|  | ||||
|  | ||||
| class AllergenSerializer(serializers.ModelSerializer): | ||||
| @@ -54,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) | ||||
|   | ||||
| @@ -3,12 +3,10 @@ | ||||
|  | ||||
| from api.viewsets import ReadProtectedModelViewSet | ||||
| from django_filters.rest_framework import DjangoFilterBackend | ||||
| from django.utils import timezone | ||||
| 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): | ||||
| @@ -74,61 +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', ] | ||||
|  | ||||
|     def perform_update(self, serializer): | ||||
|         instance = serializer.save() | ||||
|         if instance.served and not instance.served_at: | ||||
|             instance.served_at = timezone.now() | ||||
|             instance.save() | ||||
|  | ||||
|  | ||||
| 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,16 +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.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 | ||||
| from .models import Food, BasicFood, TransformedFood, QRCode | ||||
|  | ||||
|  | ||||
| class QRCodeForms(forms.ModelForm): | ||||
| @@ -186,60 +185,3 @@ 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=0, | ||||
| ) | ||||
|  | ||||
|  | ||||
| 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") | ||||
|   | ||||
| @@ -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')}, | ||||
|         ), | ||||
|     ] | ||||
| @@ -4,14 +4,10 @@ | ||||
| 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): | ||||
| @@ -288,199 +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 | ||||
|  | ||||
|         elif self.served: | ||||
|             if FoodTransaction.objects.filter(order=self).exists(): | ||||
|                 transaction = FoodTransaction.objects.get(order=self) | ||||
|                 transaction.valid = True | ||||
|                 transaction.save() | ||||
|             else: | ||||
|                 transaction = FoodTransaction( | ||||
|                     source=self.user.note, | ||||
|                     destination=self.activity.organizer.note, | ||||
|                     amount=self.amount, | ||||
|                     quantity=1, | ||||
|                     valid=True, | ||||
|                     order=self, | ||||
|                 ) | ||||
|                 transaction.save() | ||||
|         else: | ||||
|             if FoodTransaction.objects.filter(order=self).exists(): | ||||
|                 transaction = FoodTransaction.objects.get(order=self) | ||||
|                 transaction.valid = False | ||||
|                 transaction.save() | ||||
|  | ||||
|         return super().save(*args, **kwargs) | ||||
|  | ||||
|  | ||||
| class FoodTransaction(Transaction): | ||||
|     """ | ||||
|     Special type of :model:`note.Transaction` associated to a :model:`food.Order`. | ||||
|     """ | ||||
|     order = models.ForeignKey( | ||||
|         Order, | ||||
|         on_delete=models.PROTECT, | ||||
|         related_name='transaction', | ||||
|         verbose_name=_('order') | ||||
|     ) | ||||
|  | ||||
|     class Meta: | ||||
|         verbose_name = _("food transaction") | ||||
|         verbose_name_plural = _("food transactions") | ||||
|   | ||||
| @@ -1,46 +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) { | ||||
|     console.log("update") | ||||
|   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 | ||||
| from .models import Food | ||||
|  | ||||
|  | ||||
| class FoodTable(tables.Table): | ||||
| @@ -38,84 +35,3 @@ class FoodTable(tables.Table): | ||||
|             'data-href': lambda record: 'detail/' + str(record.pk), | ||||
|             'style': 'cursor:pointer', | ||||
|         } | ||||
|  | ||||
|  | ||||
| class DishTable(tables.Table): | ||||
|     """ | ||||
|     List dishes | ||||
|     """ | ||||
|     supplements = tables.Column(empty_values=(), verbose_name=_('Available supplements'), orderable=False) | ||||
|  | ||||
|     def render_supplements(self, record): | ||||
|         return ", ".join(str(q.food) for q in record.supplements.all()) | ||||
|  | ||||
|     def render_price(self, value): | ||||
|         return pretty_money(value) | ||||
|  | ||||
|     class Meta: | ||||
|         model = Dish | ||||
|         template_name = 'django_tables2/bootstrap4.html' | ||||
|         fields = ('main', 'supplements', 'price', 'available') | ||||
|         row_attrs = { | ||||
|             'class': 'table-row', | ||||
|             'data-href': lambda record: str(record.pk), | ||||
|             'style': 'cursor:pointer', | ||||
|         } | ||||
|  | ||||
|  | ||||
| DELETE_TEMPLATE = """ | ||||
| <button id="{{ record.pk }}" | ||||
|         class="btn btn-danger btn-sm" | ||||
|         onclick="delete_button(this.id, 'orders_table_{{ table.prefix }}')"> | ||||
|     {{ delete_trans }} | ||||
| </button> | ||||
| """ | ||||
|  | ||||
|  | ||||
| SERVE_TEMPLATE = """ | ||||
| <button id="{{ record.pk }}" | ||||
|         class="btn btn-sm {% if record.served %}btn-secondary{% else %}btn-success{% endif %}" | ||||
|         onclick="serve_button(this.id, 'orders_table_{{ table.prefix }}', {{ record.served|yesno:'true,false' }})"> | ||||
|     {% if record.served %} | ||||
|         {{ record.served_at|date:"d/m/Y H:i" }} | ||||
|     {% else %}""" + _('Serve') + """ | ||||
|     {% endif %} | ||||
| </button> | ||||
| """ | ||||
|  | ||||
|  | ||||
| class OrderTable(tables.Table): | ||||
|     """ | ||||
|     Lis all orders. | ||||
|     """ | ||||
|     delete = tables.TemplateColumn( | ||||
|         template_code=DELETE_TEMPLATE, | ||||
|         extra_context={"delete_trans": _('Delete')}, | ||||
|         orderable=False, | ||||
|         attrs={'td': {'class': lambda record: 'col-sm-1' + ( | ||||
|             ' d-none' if not PermissionBackend.check_perm( | ||||
|                 get_current_request(), "food.delete_order", | ||||
|                 record) else '')}}, verbose_name=_("Delete"), ) | ||||
|  | ||||
|     serve = tables.TemplateColumn( | ||||
|         template_code=SERVE_TEMPLATE, | ||||
|         extra_context={"serve_trans": _('Serve')}, | ||||
|         orderable=False, | ||||
|         attrs={'td': {'class': lambda record: 'col-sm-1' + ( | ||||
|             ' d-none' if not PermissionBackend.check_perm( | ||||
|                 get_current_request(), "food.change_order_saved", | ||||
|                 record) else '')}}, verbose_name=_("Serve"), ) | ||||
|  | ||||
|     request = tables.Column( | ||||
|         orderable=False | ||||
|     ) | ||||
|  | ||||
|     class Meta: | ||||
|         model = Order | ||||
|         template_name = 'django_tables2/bootstrap4.html' | ||||
|         fields = ('ordered_at', 'user', 'dish', 'supplements', 'request', 'serve', 'delete') | ||||
|         order_by = ('ordered_at', ) | ||||
|         row_attrs = { | ||||
|             'class': 'table-row', | ||||
|             '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,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 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 %} | ||||
|     {% 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 "Ajouter des suppléments (optionnel)" %} | ||||
|   </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 %} | ||||
| @@ -64,19 +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 %} | ||||
|     {% 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,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,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 %} | ||||
| @@ -19,14 +19,4 @@ urlpatterns = [ | ||||
|     path('detail/transformed/<int:pk>', views.TransformedFoodDetailView.as_view(), name='transformedfood_view'), | ||||
|     path('add/ingredient/<int:pk>', views.AddIngredientView.as_view(), name='add_ingredient'), | ||||
|     path('redirect/', views.QRCodeRedirectView.as_view(), name='redirect_view'), | ||||
|     # TODO not always store activity_pk in url | ||||
|     path('activity/<int:activity_pk>/dishes/add/', views.DishCreateView.as_view(), name='dish_create'), | ||||
|     path('activity/<int:activity_pk>/dishes/', views.DishListView.as_view(), name='dish_list'), | ||||
|     path('activity/<int:activity_pk>/dishes/<int:pk>/', views.DishDetailView.as_view(), name='dish_detail'), | ||||
|     path('activity/<int:activity_pk>/dishes/<int:pk>/update/', views.DishUpdateView.as_view(), name='dish_update'), | ||||
|     path('activity/<int:activity_pk>/dishes/<int:pk>/delete/', views.DishDeleteView.as_view(), name='dish_delete'), | ||||
|     path('activity/<int:activity_pk>/order/', views.OrderCreateView.as_view(), name='order_create'), | ||||
|     path('activity/<int:activity_pk>/orders/', views.OrderListView.as_view(), name='order_list'), | ||||
|     path('activity/<int:activity_pk>/orders/served', views.ServedOrderListView.as_view(), name='served_order_list'), | ||||
|     path('activity/orders/<int:pk>/delete/', views.OrderDeleteView.as_view(), name='order_delete'), | ||||
| ] | ||||
|   | ||||
| @@ -4,30 +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 | ||||
| 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 | ||||
| from .models import Food, BasicFood, TransformedFood, QRCode | ||||
| from .forms import QRCodeForms, BasicFoodForms, TransformedFoodForms, \ | ||||
|     ManageIngredientsForm, ManageIngredientsFormSet, AddIngredientForms, \ | ||||
|     BasicFoodUpdateForms, TransformedFoodUpdateForms, \ | ||||
|     DishForm, SupplementFormSet, SupplementFormSetHelper, OrderForm | ||||
| from .tables import FoodTable, DishTable, OrderTable | ||||
|     BasicFoodUpdateForms, TransformedFoodUpdateForms | ||||
| from .tables import FoodTable | ||||
| from .utils import pretty_duration | ||||
|  | ||||
|  | ||||
| @@ -79,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( | ||||
| @@ -117,9 +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["open_activities"] = Activity.objects.filter(activity_type__name="Perm bouffe", open=True) | ||||
|  | ||||
|         return context | ||||
|  | ||||
|  | ||||
| @@ -534,270 +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: | ||||
|                 if f.is_valid(): | ||||
|                     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_form(self, **kwargs): | ||||
|         form = super().get_form(**kwargs) | ||||
|         if 'main' in form.fields: | ||||
|             del form.fields["main"] | ||||
|         return form | ||||
|  | ||||
|  | ||||
| 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) | ||||
|  | ||||
|     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 OrderDeleteView(ProtectQuerysetMixin, LoginRequiredMixin, DeleteView): | ||||
|     """ | ||||
|     Delete an order | ||||
|     """ | ||||
|     model = Order | ||||
|     extra_context = {"title": _('Delete dish')} | ||||
|  | ||||
|     def delete(self, request, *args, **kwargs): | ||||
|         if self.get_object().served: | ||||
|             raise PermissionDenied(_("This order cannot be deleted because it has already been served")) | ||||
|         return super().delete(request, *args, **kwargs) | ||||
|  | ||||
|     def get_success_url(self): | ||||
|         return reverse_lazy('food:order_list', kwargs={"activity_pk": self.kwargs["activity_pk"]}) | ||||
|   | ||||
| @@ -417,7 +417,7 @@ class Membership(models.Model): | ||||
|         A membership is valid if today is between the start and the end date. | ||||
|         """ | ||||
|         if self.date_end is not None: | ||||
|             return self.date_start.toordinal() <= datetime.datetime.now().toordinal() < self.date_end.toordinal() | ||||
|             return self.date_start.toordinal() <= datetime.datetime.now().toordinal() <= self.date_end.toordinal() | ||||
|         else: | ||||
|             return self.date_start.toordinal() <= datetime.datetime.now().toordinal() | ||||
|  | ||||
|   | ||||
| @@ -228,7 +228,7 @@ function consume (source, source_alias, dest, quantity, amount, reason, type, ca | ||||
|           addMsg(interpolate(gettext('Warning, the transaction from the note %s succeed, ' + | ||||
|               'but the emitter note %s is negative.'), [source_alias, source_alias]), 'warning', 30000) | ||||
|         } | ||||
|         if (source.membership && source.membership.date_end < new Date().toISOString()) { | ||||
|         if (source.membership && source.membership.date_end <= new Date().toISOString()) { | ||||
|           addMsg(interpolate(gettext('Warning, the emitter note %s is no more a BDE member.'), [source_alias]), | ||||
|               'danger', 30000) | ||||
|         } | ||||
|   | ||||
| @@ -310,10 +310,10 @@ $('#btn_transfer').click(function () { | ||||
|             destination: dest.note.id, | ||||
|             destination_alias: dest.name | ||||
|           }).done(function () { | ||||
|           if (source.note.membership && source.note.membership.date_end < new Date().toISOString()) { | ||||
|           if (source.note.membership && source.note.membership.date_end <= new Date().toISOString()) { | ||||
|             addMsg(interpolate(gettext('Warning, the emitter note %s is no more a BDE member.'), [source.name]), 'danger', 30000) | ||||
|           } | ||||
|           if (dest.note.membership && dest.note.membership.date_end < new Date().toISOString()) { | ||||
|           if (dest.note.membership && dest.note.membership.date_end <= new Date().toISOString()) { | ||||
|             addMsg(interpolate(gettext('Warning, the destination note %s is no more a BDE member.'), [dest.name]), 'danger', 30000) | ||||
|           } | ||||
|  | ||||
| @@ -414,7 +414,7 @@ $('#btn_transfer').click(function () { | ||||
|         bank: $('#bank').val() | ||||
|       }).done(function () { | ||||
|       addMsg(gettext('Credit/debit succeed!'), 'success', 10000) | ||||
|       if (user_note.membership && user_note.membership.date_end < new Date().toISOString()) { addMsg(gettext('Warning, the emitter note %s is no more a BDE member.'), 'danger', 10000) } | ||||
|       if (user_note.membership && user_note.membership.date_end <= new Date().toISOString()) { addMsg(gettext('Warning, the emitter note %s is no more a BDE member.'), 'danger', 10000) } | ||||
|       reset() | ||||
|     }).fail(function (err) { | ||||
|       const errObj = JSON.parse(err.responseText) | ||||
|   | ||||
							
								
								
									
										18
									
								
								apps/treasury/migrations/0011_sogecredit_valid.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								apps/treasury/migrations/0011_sogecredit_valid.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| # Generated by Django 5.2.6 on 2025-09-28 20:12 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('treasury', '0010_alter_invoice_bde'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name='sogecredit', | ||||
|             name='valid', | ||||
|             field=models.BooleanField(blank=True, default=False, verbose_name='Valid'), | ||||
|         ), | ||||
|     ] | ||||
| @@ -308,6 +308,12 @@ class SogeCredit(models.Model): | ||||
|         null=True, | ||||
|     ) | ||||
|  | ||||
|     valid = models.BooleanField( | ||||
|         default=False, | ||||
|         verbose_name=_("Valid"), | ||||
|         blank=True, | ||||
|     ) | ||||
|  | ||||
|     class Meta: | ||||
|         verbose_name = _("Credit from the Société générale") | ||||
|         verbose_name_plural = _("Credits from the Société générale") | ||||
| @@ -332,7 +338,7 @@ class SogeCredit(models.Model): | ||||
|                 last_name=self.user.last_name, | ||||
|                 first_name=self.user.first_name, | ||||
|                 bank="Société générale", | ||||
|                 valid=False, | ||||
|                 valid=True, | ||||
|             ) | ||||
|             credit_transaction._force_save = True | ||||
|             credit_transaction.save() | ||||
| @@ -346,12 +352,12 @@ class SogeCredit(models.Model): | ||||
|         return super().save(*args, **kwargs) | ||||
|  | ||||
|     @property | ||||
|     def valid(self): | ||||
|     def valid_legacy(self): | ||||
|         return self.credit_transaction and self.credit_transaction.valid | ||||
|  | ||||
|     @property | ||||
|     def amount(self): | ||||
|         if self.valid: | ||||
|         if self.valid_legacy: | ||||
|             return self.credit_transaction.total | ||||
|         amount = 0 | ||||
|         transactions_wei = self.transactions.filter(membership__club__weiclub__isnull=False) | ||||
| @@ -397,7 +403,7 @@ class SogeCredit(models.Model): | ||||
|                         self.transactions.add(m.transaction) | ||||
|  | ||||
|         for tr in self.transactions.all(): | ||||
|             tr.valid = False | ||||
|             tr.valid = True | ||||
|             tr.save() | ||||
|  | ||||
|     def invalidate(self): | ||||
| @@ -422,6 +428,7 @@ class SogeCredit(models.Model): | ||||
|         self.invalidate() | ||||
|         # Refresh credit amount | ||||
|         self.save() | ||||
|         self.valid = True | ||||
|         self.credit_transaction.valid = True | ||||
|         self.credit_transaction._force_save = True | ||||
|         self.credit_transaction.save() | ||||
|   | ||||
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 284 KiB After Width: | Height: | Size: 104 KiB | 
| @@ -56,6 +56,7 @@ class InvoiceTable(tables.Table): | ||||
|         model = Invoice | ||||
|         template_name = 'django_tables2/bootstrap4.html' | ||||
|         fields = ('id', 'name', 'object', 'acquitted', 'invoice',) | ||||
|         order_by = ('-id',) | ||||
|  | ||||
|  | ||||
| class RemittanceTable(tables.Table): | ||||
|   | ||||
| @@ -74,7 +74,6 @@ class InvoiceCreateView(ProtectQuerysetMixin, ProtectedCreateView): | ||||
|  | ||||
|         # For each product, we save it | ||||
|         formset = ProductFormSet(self.request.POST, instance=form.instance) | ||||
|         print(formset) | ||||
|         if formset.is_valid(): | ||||
|             for f in formset: | ||||
|                 # We don't save the product if the designation is not entered, ie. if the line is empty | ||||
| @@ -418,7 +417,7 @@ class SogeCreditListView(LoginRequiredMixin, ProtectQuerysetMixin, SingleTableVi | ||||
|                 ) | ||||
|  | ||||
|         if "valid" not in self.request.GET or not self.request.GET["valid"]: | ||||
|             qs = qs.filter(credit_transaction__valid=False) | ||||
|             qs = qs.filter(valid=False) | ||||
|  | ||||
|         return qs | ||||
|  | ||||
|   | ||||
| @@ -680,7 +680,7 @@ class TestWEIRegistration(TestCase): | ||||
|             self.assertTrue(soge_credit.exists()) | ||||
|             soge_credit = soge_credit.get() | ||||
|             self.assertTrue(membership.transaction in soge_credit.transactions.all()) | ||||
|             self.assertFalse(membership.transaction.valid) | ||||
|             self.assertTrue(membership.transaction.valid) | ||||
|  | ||||
|         # Check that if the WEI is started, we can't update a wei | ||||
|         self.wei.date_start = date(2000, 1, 1) | ||||
|   | ||||
| @@ -4366,6 +4366,14 @@ msgstr "" | ||||
| msgid "Forgotten your password or username?" | ||||
| msgstr "Passwort oder Username vergessen?" | ||||
|  | ||||
| #: note_kfet/templates/registration/login.html:44 | ||||
| msgid "Download on the AppStore" | ||||
| msgstr "Im AppStore herunterladen" | ||||
|  | ||||
| #: note_kfet/templates/registration/login.html:48 | ||||
| msgid "Get it on Google Play" | ||||
| msgstr "Bei Google Play herunterladen" | ||||
|  | ||||
| #: note_kfet/templates/registration/password_change_done.html:13 | ||||
| msgid "Your password was changed." | ||||
| msgstr "Ihr Passwort wurde geändert." | ||||
|   | ||||
| @@ -4281,6 +4281,14 @@ msgstr "" | ||||
| msgid "Forgotten your password or username?" | ||||
| msgstr "¿ Contraseña o nombre de usuario olvidado ?" | ||||
|  | ||||
| #: note_kfet/templates/registration/login.html:44 | ||||
| msgid "Download on the AppStore" | ||||
| msgstr "Descargar en la AppStore" | ||||
|  | ||||
| #: note_kfet/templates/registration/login.html:48 | ||||
| msgid "Get it on Google Play" | ||||
| msgstr "Descargar en Google Play" | ||||
|  | ||||
| #: note_kfet/templates/registration/password_change_done.html:13 | ||||
| msgid "Your password was changed." | ||||
| msgstr "Su contraseña fue cambiada con éxito." | ||||
|   | ||||
| @@ -4584,6 +4584,14 @@ msgstr "" | ||||
| msgid "Forgotten your password or username?" | ||||
| msgstr "Mot de passe ou pseudo oublié ?" | ||||
|  | ||||
| #: note_kfet/templates/registration/login.html:44 | ||||
| msgid "Download on the AppStore" | ||||
| msgstr "Télécharger sur l'AppStore" | ||||
|  | ||||
| #: note_kfet/templates/registration/login.html:48 | ||||
| msgid "Get it on Google Play" | ||||
| msgstr "Télécharger sur Google Play" | ||||
|  | ||||
| #: note_kfet/templates/registration/password_change_done.html:13 | ||||
| msgid "Your password was changed." | ||||
| msgstr "Votre mot de passe a bien été changé." | ||||
|   | ||||
							
								
								
									
										50
									
								
								note_kfet/static/img/appstore_badge_fr.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								note_kfet/static/img/appstore_badge_fr.svg
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,50 @@ | ||||
| <svg id="livetype" xmlns="http://www.w3.org/2000/svg" width="126.50751" height="40" viewBox="0 0 126.50751 40"> | ||||
|   <title>Download_on_the_App_Store_Badge_FR_RGB_blk_100517</title> | ||||
|   <g> | ||||
|     <g> | ||||
|       <path d="M116.97821,0H9.53468c-.3667,0-.729,0-1.09473.002-.30615.002-.60986.00781-.91895.0127A13.21476,13.21476,0,0,0,5.5171.19141a6.66509,6.66509,0,0,0-1.90088.627A6.43779,6.43779,0,0,0,1.99757,1.99707,6.25844,6.25844,0,0,0,.81935,3.61816a6.60119,6.60119,0,0,0-.625,1.90332,12.993,12.993,0,0,0-.1792,2.002C.00587,7.83008.00489,8.1377,0,8.44434V31.5586c.00489.3105.00587.6113.01515.9219a12.99232,12.99232,0,0,0,.1792,2.0019,6.58756,6.58756,0,0,0,.625,1.9043A6.20778,6.20778,0,0,0,1.99757,38.001a6.27445,6.27445,0,0,0,1.61865,1.1787,6.70082,6.70082,0,0,0,1.90088.6308,13.45514,13.45514,0,0,0,2.0039.1768c.30909.0068.6128.0107.91895.0107C8.80567,40,9.168,40,9.53468,40H116.97821c.3594,0,.7246,0,1.084-.002.3047,0,.6172-.0039.9219-.0107a13.279,13.279,0,0,0,2-.1768,6.80432,6.80432,0,0,0,1.9082-.6308,6.27742,6.27742,0,0,0,1.6172-1.1787,6.39482,6.39482,0,0,0,1.1816-1.6143,6.60413,6.60413,0,0,0,.6191-1.9043,13.50641,13.50641,0,0,0,.1856-2.0019c.0039-.3106.0039-.6114.0039-.9219.0078-.3633.0078-.7246.0078-1.0938V9.53613c0-.36621,0-.72949-.0078-1.09179,0-.30664,0-.61426-.0039-.9209a13.50709,13.50709,0,0,0-.1856-2.002,6.6177,6.6177,0,0,0-.6191-1.90332,6.46619,6.46619,0,0,0-2.7988-2.7998,6.76753,6.76753,0,0,0-1.9082-.627,13.04394,13.04394,0,0,0-2-.17676c-.3047-.00488-.6172-.01074-.9219-.01269-.3594-.002-.7246-.002-1.084-.002Z" style="fill: #a6a6a6"/> | ||||
|       <path d="M8.44482,39.125c-.30467,0-.602-.0039-.90428-.0107a12.68714,12.68714,0,0,1-1.86914-.1631,5.88381,5.88381,0,0,1-1.65674-.5479,5.40573,5.40573,0,0,1-1.397-1.0166,5.32082,5.32082,0,0,1-1.02051-1.3965,5.72186,5.72186,0,0,1-.543-1.6572,12.41351,12.41351,0,0,1-.1665-1.875c-.00634-.2109-.01464-.9131-.01464-.9131V8.44434S.88185,7.75293.8877,7.5498a12.37138,12.37138,0,0,1,.16552-1.87207,5.75577,5.75577,0,0,1,.54347-1.6621A5.37365,5.37365,0,0,1,2.61182,2.61768,5.56562,5.56562,0,0,1,4.01417,1.59521a5.82309,5.82309,0,0,1,1.65332-.54394A12.58579,12.58579,0,0,1,7.543.88721L8.44532.875h109.612l.9131.0127a12.38493,12.38493,0,0,1,1.8584.16259,5.93833,5.93833,0,0,1,1.6709.54785,5.59375,5.59375,0,0,1,2.415,2.41993,5.76267,5.76267,0,0,1,.5352,1.64892,12.995,12.995,0,0,1,.1738,1.88721c.0029.2832.0029.5874.0029.89014.0079.375.0079.73193.0079,1.09179V30.4648c0,.3633,0,.7178-.0079,1.0752,0,.3252,0,.6231-.0039.9297a12.73126,12.73126,0,0,1-.1709,1.8535,5.739,5.739,0,0,1-.54,1.67,5.48029,5.48029,0,0,1-1.0156,1.3857,5.4129,5.4129,0,0,1-1.3994,1.0225,5.86168,5.86168,0,0,1-1.668.5498,12.54218,12.54218,0,0,1-1.8692.1631c-.2929.0068-.5996.0107-.8974.0107l-1.084.002Z"/> | ||||
|     </g> | ||||
|     <g> | ||||
|       <g id="_Group_" data-name="<Group>"> | ||||
|         <g id="_Group_2" data-name="<Group>"> | ||||
|           <g id="_Group_3" data-name="<Group>"> | ||||
|             <path id="_Path_" data-name="<Path>" d="M24.7718,20.30068a4.94881,4.94881,0,0,1,2.35656-4.15206,5.06566,5.06566,0,0,0-3.99116-2.15768c-1.67924-.17626-3.30719,1.00483-4.1629,1.00483-.87227,0-2.18977-.98733-3.6085-.95814a5.31529,5.31529,0,0,0-4.47292,2.72787c-1.934,3.34842-.49141,8.26947,1.3612,10.97608.9269,1.32535,2.01018,2.8058,3.42763,2.7533,1.38706-.05753,1.9051-.88448,3.5794-.88448,1.65876,0,2.14479.88448,3.591.8511,1.48838-.02416,2.42613-1.33124,3.32051-2.66914A10.962,10.962,0,0,0,27.691,24.69985,4.78205,4.78205,0,0,1,24.7718,20.30068Z" style="fill: #fff"/> | ||||
|             <path id="_Path_2" data-name="<Path>" d="M22.04017,12.21089a4.87248,4.87248,0,0,0,1.11452-3.49062,4.95746,4.95746,0,0,0-3.20758,1.65961,4.63634,4.63634,0,0,0-1.14371,3.36139A4.09905,4.09905,0,0,0,22.04017,12.21089Z" style="fill: #fff"/> | ||||
|           </g> | ||||
|         </g> | ||||
|       </g> | ||||
|       <g id="_Group_4" data-name="<Group>"> | ||||
|         <g> | ||||
|           <path d="M35.65528,14.70166V9.57813h-1.877V8.73486h4.67676v.84326H36.582v5.12354Z" style="fill: #fff"/> | ||||
|           <path d="M42.76466,13.48584a1.828,1.828,0,0,1-1.95117,1.30273,2.04531,2.04531,0,0,1-2.08008-2.32422,2.07685,2.07685,0,0,1,2.07617-2.35254c1.25293,0,2.00879.856,2.00879,2.27v.31006H39.63868v.0498a1.1902,1.1902,0,0,0,1.19922,1.29,1.07934,1.07934,0,0,0,1.07129-.5459Zm-3.126-1.45117H41.9131a1.08647,1.08647,0,0,0-1.1084-1.1665A1.15162,1.15162,0,0,0,39.63868,12.03467ZM40.2754,9.4458l1.03809-1.42236h1.042L41.19337,9.4458Z" style="fill: #fff"/> | ||||
|           <path d="M44.05274,8.44092h.88867v6.26074h-.88867Z" style="fill: #fff"/> | ||||
|           <path d="M50.208,13.48584a1.828,1.828,0,0,1-1.95117,1.30273,2.04531,2.04531,0,0,1-2.08008-2.32422,2.07685,2.07685,0,0,1,2.07617-2.35254c1.25293,0,2.00879.856,2.00879,2.27v.31006H47.082v.0498a1.1902,1.1902,0,0,0,1.19922,1.29,1.07934,1.07934,0,0,0,1.07129-.5459Zm-3.126-1.45117h2.27441a1.08647,1.08647,0,0,0-1.1084-1.1665A1.15162,1.15162,0,0,0,47.082,12.03467Zm.63672-2.58887,1.03809-1.42236h1.042L48.63673,9.4458Z" style="fill: #fff"/> | ||||
|           <path d="M54.40333,11.67041a1.00546,1.00546,0,0,0-1.06348-.76465c-.74414,0-1.19922.57031-1.19922,1.52979,0,.97607.459,1.55908,1.19922,1.55908a.97873.97873,0,0,0,1.06348-.74023h.86426a1.762,1.762,0,0,1-1.92285,1.53418,2.06791,2.06791,0,0,1-2.11328-2.353,2.05305,2.05305,0,0,1,2.1084-2.32373,1.77731,1.77731,0,0,1,1.92773,1.55859Z" style="fill: #fff"/> | ||||
|           <path d="M56.44728,8.44092h.88086v2.48145h.07031a1.3856,1.3856,0,0,1,1.373-.80664,1.48339,1.48339,0,0,1,1.55078,1.67871v2.90723h-.88965v-2.688c0-.71924-.335-1.0835-.96289-1.0835a1.05194,1.05194,0,0,0-1.13379,1.1416v2.62988h-.88867Z" style="fill: #fff"/> | ||||
|           <path d="M61.43946,13.42822c0-.81055.60352-1.27783,1.6748-1.34424l1.21973-.07031V11.625c0-.47559-.31445-.74414-.92188-.74414-.49609,0-.83984.18213-.93848.50049h-.86035c.09082-.77344.81836-1.26953,1.83984-1.26953,1.12891,0,1.76514.562,1.76514,1.51318v3.07666h-.855v-.63281H64.293a1.515,1.515,0,0,1-1.35254.707A1.36026,1.36026,0,0,1,61.43946,13.42822Zm2.89453-.38477V12.667l-1.09961.07031c-.62012.0415-.90137.25244-.90137.64941,0,.40527.35156.64111.835.64111A1.0615,1.0615,0,0,0,64.334,13.04346Z" style="fill: #fff"/> | ||||
|           <path d="M66.60987,10.19873h.85547v.69043h.06641a1.22092,1.22092,0,0,1,1.21582-.76514,1.86836,1.86836,0,0,1,.39648.03711v.877a2.43442,2.43442,0,0,0-.49609-.05371A1.05507,1.05507,0,0,0,67.49855,12.043v2.65869h-.88867Z" style="fill: #fff"/> | ||||
|           <path d="M69.96144,15.15234h.90918c.0752.32666.45117.5376,1.05078.5376.74023,0,1.17871-.35156,1.17871-.94678v-.86426H73.0337a1.51433,1.51433,0,0,1-1.38965.75635c-1.14941,0-1.86035-.88867-1.86035-2.23682,0-1.373.71875-2.27441,1.86914-2.27441a1.56045,1.56045,0,0,1,1.41406.79395h.07031v-.71924h.85156v4.54c0,1.02979-.80664,1.68311-2.08008,1.68311C70.7837,16.42188,70.05616,15.91748,69.96144,15.15234Zm3.15527-2.7583c0-.897-.46387-1.47168-1.2207-1.47168-.76465,0-1.19434.57471-1.19434,1.47168,0,.89746.42969,1.47217,1.19434,1.47217C72.65773,13.86621,73.11671,13.2959,73.11671,12.394Z" style="fill: #fff"/> | ||||
|           <path d="M79.21241,13.48584a1.828,1.828,0,0,1-1.95117,1.30273,2.04531,2.04531,0,0,1-2.08008-2.32422,2.07685,2.07685,0,0,1,2.07617-2.35254c1.25293,0,2.00879.856,2.00879,2.27v.31006H76.08644v.0498a1.1902,1.1902,0,0,0,1.19922,1.29,1.07934,1.07934,0,0,0,1.07129-.5459Zm-3.126-1.45117h2.27441a1.08647,1.08647,0,0,0-1.1084-1.1665A1.15162,1.15162,0,0,0,76.08644,12.03467Z" style="fill: #fff"/> | ||||
|           <path d="M80.45948,10.19873H81.315v.69043h.06641a1.22092,1.22092,0,0,1,1.21582-.76514,1.86836,1.86836,0,0,1,.39648.03711v.877a2.43442,2.43442,0,0,0-.49609-.05371A1.05507,1.05507,0,0,0,81.34815,12.043v2.65869h-.88867Z" style="fill: #fff"/> | ||||
|           <path d="M86.19581,12.44824c0-1.42285.73145-2.32422,1.86914-2.32422a1.484,1.484,0,0,1,1.38086.79h.06641V8.44092h.88867v6.26074h-.85156v-.71143H89.479a1.56284,1.56284,0,0,1-1.41406.78564C86.91944,14.77588,86.19581,13.87451,86.19581,12.44824Zm.918,0c0,.95508.4502,1.52979,1.20313,1.52979.749,0,1.21191-.583,1.21191-1.52588,0-.93848-.46777-1.52979-1.21191-1.52979C87.56886,10.92236,87.11378,11.501,87.11378,12.44824Z" style="fill: #fff"/> | ||||
|           <path d="M91.60206,13.42822c0-.81055.60352-1.27783,1.6748-1.34424l1.21973-.07031V11.625c0-.47559-.31445-.74414-.92187-.74414-.49609,0-.83984.18213-.93848.50049h-.86035c.09082-.77344.81836-1.26953,1.83984-1.26953,1.12891,0,1.76563.562,1.76563,1.51318v3.07666h-.85547v-.63281h-.07031a1.515,1.515,0,0,1-1.35254.707A1.36026,1.36026,0,0,1,91.60206,13.42822Zm2.89453-.38477V12.667L93.397,12.7373c-.62012.0415-.90137.25244-.90137.64941,0,.40527.35156.64111.835.64111A1.0615,1.0615,0,0,0,94.49659,13.04346Z" style="fill: #fff"/> | ||||
|           <path d="M96.773,10.19873h.85547v.71533h.06641a1.348,1.348,0,0,1,1.34375-.80225,1.46456,1.46456,0,0,1,1.55859,1.6748v2.915h-.88867V12.00977c0-.72363-.31445-1.0835-.97168-1.0835a1.03294,1.03294,0,0,0-1.0752,1.14111v2.63428H96.773Z" style="fill: #fff"/> | ||||
|           <path d="M103.61769,10.11182c1.0127,0,1.6748.47119,1.76172,1.26514h-.85254c-.082-.33057-.40527-.5415-.90918-.5415-.49609,0-.873.23535-.873.58691,0,.269.22754.43848.71582.55029l.748.17334c.85645.19873,1.25781.56689,1.25781,1.22852,0,.84766-.79,1.41406-1.86523,1.41406-1.07129,0-1.76953-.48389-1.84863-1.28174h.88965a.91365.91365,0,0,0,.97949.562c.55371,0,.94727-.248.94727-.60791,0-.26855-.21094-.44238-.66211-.5498l-.78516-.18213c-.85645-.20264-1.25293-.58691-1.25293-1.25684C101.86866,10.67383,102.60011,10.11182,103.61769,10.11182Z" style="fill: #fff"/> | ||||
|         </g> | ||||
|       </g> | ||||
|     </g> | ||||
|     <g> | ||||
|       <path d="M35.19825,18.06689h1.85938V30.48535H35.19825Z" style="fill: #fff"/> | ||||
|       <path d="M39.29786,22.61084l1.01563-4.54395h1.80664l-1.23047,4.54395Z" style="fill: #fff"/> | ||||
|       <path d="M49.14649,27.12891H44.4131l-1.13672,3.35645H41.27149l4.4834-12.41846h2.083l4.4834,12.41846H50.28224Zm-4.24316-1.54883h3.752l-1.84961-5.44775h-.05176Z" style="fill: #fff"/> | ||||
|       <path d="M62.00294,25.959c0,2.81348-1.50586,4.62109-3.77832,4.62109a3.0693,3.0693,0,0,1-2.84863-1.584h-.043v4.48438h-1.8584V21.43115h1.79883V22.937h.03418a3.21162,3.21162,0,0,1,2.88281-1.60059C60.48829,21.33643,62.00294,23.15283,62.00294,25.959Zm-1.91016,0c0-1.8335-.94727-3.03857-2.39258-3.03857-1.41992,0-2.375,1.23047-2.375,3.03857,0,1.82422.95508,3.0459,2.375,3.0459C59.14552,29.00488,60.09278,27.80859,60.09278,25.959Z" style="fill: #fff"/> | ||||
|       <path d="M71.9673,25.959c0,2.81348-1.50586,4.62109-3.77832,4.62109a3.0693,3.0693,0,0,1-2.84863-1.584h-.043v4.48438H63.43946V21.43115H65.2378V22.937H65.272a3.21162,3.21162,0,0,1,2.88281-1.60059C70.45265,21.33643,71.9673,23.15283,71.9673,25.959Zm-1.91016,0c0-1.8335-.94727-3.03857-2.39258-3.03857-1.41992,0-2.375,1.23047-2.375,3.03857,0,1.82422.95508,3.0459,2.375,3.0459C69.10987,29.00488,70.05714,27.80859,70.05714,25.959Z" style="fill: #fff"/> | ||||
|       <path d="M78.55323,27.02539c.1377,1.23145,1.334,2.04,2.96875,2.04,1.56641,0,2.69336-.80859,2.69336-1.91895,0-.96387-.67969-1.541-2.28906-1.93652l-1.60937-.38818C78.03663,24.271,76.978,23.20459,76.978,21.47412c0-2.14258,1.86719-3.61426,4.51855-3.61426,2.624,0,4.42285,1.47168,4.4834,3.61426H84.104c-.1123-1.23926-1.13672-1.9873-2.63379-1.9873s-2.52148.75684-2.52148,1.8584c0,.87793.6543,1.39453,2.25488,1.79l1.36816.33594c2.54785.60254,3.60645,1.62646,3.60645,3.44287,0,2.32324-1.85059,3.77832-4.79395,3.77832-2.75391,0-4.61328-1.4209-4.7334-3.667Z" style="fill: #fff"/> | ||||
|       <path d="M90.19,19.28857v2.14258h1.72168v1.47168H90.19v4.9917c0,.77539.34473,1.13672,1.10156,1.13672a5.80752,5.80752,0,0,0,.61133-.043v1.46289a5.10351,5.10351,0,0,1-1.03223.08594c-1.833,0-2.54785-.68848-2.54785-2.44434V22.90283H87.00636V21.43115h1.31641V19.28857Z" style="fill: #fff"/> | ||||
|       <path d="M92.90773,25.959c0-2.84912,1.67773-4.63916,4.29395-4.63916,2.625,0,4.29492,1.79,4.29492,4.63916,0,2.85645-1.66113,4.63867-4.29492,4.63867C94.56886,30.59766,92.90773,28.81543,92.90773,25.959Zm6.69531,0c0-1.95459-.89551-3.10791-2.40137-3.10791s-2.40039,1.16211-2.40039,3.10791c0,1.96191.89453,3.10645,2.40039,3.10645S99.603,27.9209,99.603,25.959Z" style="fill: #fff"/> | ||||
|       <path d="M103.02882,21.43115h1.77246v1.541h.043a2.1594,2.1594,0,0,1,2.17773-1.63574,2.86616,2.86616,0,0,1,.63672.06934V23.144a2.59794,2.59794,0,0,0-.835-.1123,1.8728,1.8728,0,0,0-1.93652,2.0835v5.37012h-1.8584Z" style="fill: #fff"/> | ||||
|       <path d="M116.22608,27.82617c-.25,1.64355-1.85059,2.77148-3.89844,2.77148-2.63379,0-4.26855-1.76465-4.26855-4.5957,0-2.84033,1.64355-4.68213,4.19043-4.68213,2.50488,0,4.08008,1.7207,4.08008,4.46631v.63672h-6.39453v.1123a2.358,2.358,0,0,0,2.43555,2.56445,2.04834,2.04834,0,0,0,2.09082-1.27344ZM109.94386,25.124h4.52637a2.17744,2.17744,0,0,0-2.2207-2.29834A2.29214,2.29214,0,0,0,109.94386,25.124Z" style="fill: #fff"/> | ||||
|     </g> | ||||
|   </g> | ||||
| </svg> | ||||
| After Width: | Height: | Size: 12 KiB | 
							
								
								
									
										49
									
								
								note_kfet/static/img/playstore_badge_fr.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								note_kfet/static/img/playstore_badge_fr.svg
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,49 @@ | ||||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <svg id="artwork" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 135 40"> | ||||
|   <!-- Generator: Adobe Illustrator 29.5.1, SVG Export Plug-In . SVG Version: 2.1.0 Build 141)  --> | ||||
|   <defs> | ||||
|     <style> | ||||
|       .st0 { | ||||
|         fill: #4285f4; | ||||
|       } | ||||
|  | ||||
|       .st1 { | ||||
|         fill: #a6a6a6; | ||||
|       } | ||||
|  | ||||
|       .st2 { | ||||
|         fill: #34a853; | ||||
|       } | ||||
|  | ||||
|       .st3 { | ||||
|         fill: #fbbc04; | ||||
|       } | ||||
|  | ||||
|       .st4, .st5 { | ||||
|         fill: #fff; | ||||
|       } | ||||
|  | ||||
|       .st6 { | ||||
|         fill: #ea4335; | ||||
|       } | ||||
|  | ||||
|       .st5 { | ||||
|         font-family: GoogleSans-Medium, 'Google Sans'; | ||||
|         font-size: 8.7px; | ||||
|         font-weight: 500; | ||||
|       } | ||||
|     </style> | ||||
|   </defs> | ||||
|   <g> | ||||
|     <rect width="135" height="40" rx="5" ry="5"/> | ||||
|     <path class="st1" d="M130,.8c2.316,0,4.2,1.884,4.2,4.2v30c0,2.316-1.884,4.2-4.2,4.2H5c-2.316,0-4.2-1.884-4.2-4.2V5C.8,2.684,2.684.8,5,.8h125M130,0H5C2.25,0,0,2.25,0,5v30c0,2.75,2.25,5,5,5h125c2.75,0,5-2.25,5-5V5C135,2.25,132.75,0,130,0h0Z"/> | ||||
|     <path class="st4" d="M68.136,21.752c-2.352,0-4.269,1.788-4.269,4.253,0,2.449,1.917,4.253,4.269,4.253s4.269-1.804,4.269-4.253c0-2.465-1.917-4.253-4.269-4.253ZM68.136,28.583c-1.289,0-2.4-1.063-2.4-2.578,0-1.531,1.112-2.578,2.4-2.578s2.4,1.047,2.4,2.578c0,1.514-1.112,2.578-2.4,2.578ZM58.822,21.752c-2.352,0-4.269,1.788-4.269,4.253,0,2.449,1.917,4.253,4.269,4.253s4.269-1.804,4.269-4.253c0-2.465-1.917-4.253-4.269-4.253ZM58.822,28.583c-1.289,0-2.4-1.063-2.4-2.578,0-1.531,1.112-2.578,2.4-2.578s2.4,1.047,2.4,2.578c0,1.514-1.112,2.578-2.4,2.578ZM47.744,23.057v1.804h4.318c-.129,1.015-.467,1.756-.983,2.272-.628.628-1.611,1.321-3.335,1.321-2.658,0-4.736-2.143-4.736-4.801s2.078-4.801,4.736-4.801c1.434,0,2.481.564,3.254,1.289l1.273-1.273c-1.079-1.031-2.513-1.82-4.527-1.82-3.641,0-6.702,2.964-6.702,6.605s3.061,6.605,6.702,6.605c1.965,0,3.448-.644,4.608-1.853,1.192-1.192,1.563-2.868,1.563-4.221,0-.419-.032-.805-.097-1.128h-6.074ZM93.052,24.458c-.354-.95-1.434-2.707-3.641-2.707-2.191,0-4.011,1.724-4.011,4.253,0,2.384,1.804,4.253,4.221,4.253,1.949,0,3.077-1.192,3.544-1.885l-1.45-.967c-.483.709-1.144,1.176-2.094,1.176s-1.627-.435-2.062-1.289l5.687-2.352-.193-.483ZM87.252,25.876c-.048-1.643,1.273-2.481,2.223-2.481.741,0,1.369.37,1.579.902l-3.802,1.579ZM82.628,30h1.869v-12.502h-1.869v12.502ZM79.567,22.702h-.064c-.419-.499-1.224-.951-2.239-.951-2.127,0-4.076,1.869-4.076,4.269,0,2.384,1.949,4.237,4.076,4.237,1.015,0,1.82-.451,2.239-.967h.064v.612c0,1.627-.87,2.497-2.272,2.497-1.144,0-1.853-.822-2.143-1.514l-1.627.677c.467,1.128,1.708,2.513,3.77,2.513,2.191,0,4.044-1.289,4.044-4.43v-7.636h-1.772v.693ZM77.425,28.583c-1.289,0-2.368-1.079-2.368-2.562,0-1.498,1.079-2.594,2.368-2.594,1.273,0,2.272,1.096,2.272,2.594,0,1.482-.999,2.562-2.272,2.562ZM101.806,17.499h-4.471v12.501h1.866v-4.736h2.605c2.068,0,4.101-1.497,4.101-3.883s-2.033-3.882-4.101-3.882ZM101.854,23.524h-2.654v-4.285h2.654c1.395,0,2.187,1.155,2.187,2.143,0,.969-.792,2.143-2.187,2.143ZM113.386,21.729c-1.351,0-2.75.595-3.329,1.914l1.657.692c.354-.692,1.013-.917,1.705-.917.965,0,1.946.579,1.962,1.608v.129c-.338-.193-1.061-.483-1.946-.483-1.785,0-3.603.981-3.603,2.815,0,1.673,1.463,2.75,3.104,2.75,1.254,0,1.946-.563,2.38-1.222h.064v.965h1.801v-4.793c0-2.22-1.657-3.458-3.796-3.458ZM113.16,28.58c-.611,0-1.464-.305-1.464-1.061,0-.965,1.061-1.335,1.978-1.335.82,0,1.206.177,1.705.418-.145,1.158-1.142,1.978-2.219,1.978ZM123.743,22.002l-2.139,5.42h-.064l-2.219-5.42h-2.01l3.329,7.575-1.898,4.214h1.946l5.131-11.789h-2.075ZM106.936,30h1.866v-12.501h-1.866v12.501Z"/> | ||||
|     <g> | ||||
|       <path class="st6" d="M20.717,19.424l-10.647,11.3s.001.005.002.007c.327,1.227,1.447,2.13,2.777,2.13.531,0,1.031-.144,1.459-.396l.034-.02,11.984-6.915-5.609-6.106Z"/> | ||||
|       <path class="st3" d="M31.488,17.5l-.01-.007-5.174-3-5.829,5.187,5.849,5.848,5.146-2.969c.902-.487,1.515-1.438,1.515-2.535,0-1.09-.604-2.036-1.498-2.525Z"/> | ||||
|       <path class="st0" d="M10.07,9.277c-.064.236-.098.484-.098.74v19.968c0,.256.033.504.098.739l11.013-11.011-11.013-10.436Z"/> | ||||
|       <path class="st2" d="M20.796,20.001l5.51-5.509-11.97-6.94c-.435-.261-.943-.411-1.486-.411-1.33,0-2.452.905-2.779,2.134,0,0,0,.002,0,.003l10.726,10.724Z"/> | ||||
|     </g> | ||||
|   </g> | ||||
|   <text class="st5" transform="translate(41.08 13.134)"><tspan x="0" y="0">DISPONIBLE SUR</tspan></text> | ||||
| </svg> | ||||
| After Width: | Height: | Size: 4.2 KiB | 
| @@ -39,6 +39,17 @@ SPDX-License-Identifier: GPL-2.0-or-later | ||||
|             <a href="{% url 'password_reset' %}" | ||||
|                 class="badge badge-light">{% trans 'Forgotten your password or username?' %}</a> | ||||
|         </form> | ||||
|  | ||||
|         <div class="text-center mt-4"> | ||||
|             <a href="https://apps.apple.com/fr/app/la-note-kfet/id6754661723" class="d-inline-block mx-1" aria-label="{% trans 'Download on the AppStore' %}" style="cursor: pointer;"> | ||||
|                 <img src="/static/img/appstore_badge_fr.svg" | ||||
|                      alt="{% trans 'Download on the AppStore' %}" style="height: 50px;"> | ||||
|             </a> | ||||
|             <a href="https://play.google.com/store/apps/details?id=org.crans.bde.note&hl=fr" class="d-inline-block mx-1" aria-label="{% trans 'Get it on Google Play' %}" style="cursor: pointer;"> | ||||
|                 <img src="/static/img/playstore_badge_fr.svg" | ||||
|                      alt="{% trans 'Get it on Google Play' %}" style="height: 50px;"> | ||||
|             </a> | ||||
|         </div> | ||||
|     </div> | ||||
| </div> | ||||
| {% endblock %} | ||||
| @@ -12,7 +12,7 @@ django-filter~=25.1 | ||||
| django-mailer~=2.3.2 | ||||
| django-oauth-toolkit~=3.0.1 | ||||
| django-phonenumber-field~=8.1.0 | ||||
| django-polymorphic~=3.1.0 | ||||
| django-polymorphic~=4.1.0 | ||||
| djangorestframework~=3.16.0 | ||||
| django-rest-polymorphic~=0.1.10 | ||||
| django-tables2~=2.7.5 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user