mirror of
				https://gitlab.crans.org/bde/nk20
				synced 2025-10-30 23:39:54 +01:00 
			
		
		
		
	Compare commits
	
		
			17 Commits
		
	
	
		
			6d6583bfe6
			...
			delete_act
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | fadb289ed7 | ||
|  | 905fc6e7cc | ||
|  | cdd81c1444 | ||
|  | 4afafceba1 | ||
|  | 3065eacc96 | ||
|  | 71ef3aedd8 | ||
|  | 0cf11c6348 | ||
|  | 70abd0f490 | ||
|  | 4445dd4a96 | ||
|  | 03932672f3 | ||
|  | dc6a40de02 | ||
|  | d58a299a8b | ||
|  | ad0a219ed3 | ||
|  | c4404ef995 | ||
|  | b4f3a158a6 | ||
|  | f0e9a7d3dc | ||
|  | a2b42c5329 | 
							
								
								
									
										19
									
								
								apps/activity/migrations/0007_alter_guest_activity.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								apps/activity/migrations/0007_alter_guest_activity.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | |||||||
|  | # Generated by Django 4.2.20 on 2025-05-08 19:07 | ||||||
|  |  | ||||||
|  | from django.db import migrations, models | ||||||
|  | import django.db.models.deletion | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Migration(migrations.Migration): | ||||||
|  |  | ||||||
|  |     dependencies = [ | ||||||
|  |         ('activity', '0006_guest_school'), | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |     operations = [ | ||||||
|  |         migrations.AlterField( | ||||||
|  |             model_name='guest', | ||||||
|  |             name='activity', | ||||||
|  |             field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='activity.activity'), | ||||||
|  |         ), | ||||||
|  |     ] | ||||||
| @@ -234,7 +234,7 @@ class Guest(models.Model): | |||||||
|     """ |     """ | ||||||
|     activity = models.ForeignKey( |     activity = models.ForeignKey( | ||||||
|         Activity, |         Activity, | ||||||
|         on_delete=models.PROTECT, |         on_delete=models.CASCADE, | ||||||
|         related_name='+', |         related_name='+', | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -95,5 +95,23 @@ SPDX-License-Identifier: GPL-3.0-or-later | |||||||
|             errMsg(xhr.responseJSON); |             errMsg(xhr.responseJSON); | ||||||
|         }); |         }); | ||||||
|     }); |     }); | ||||||
|  |     $("#delete_activity").click(function () { | ||||||
|  |         if (!confirm("{% trans 'Are you sure you want to delete this activity?' %}")) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         $.ajax({ | ||||||
|  |             url: "/api/activity/activity/{{ activity.pk }}/", | ||||||
|  |             type: "DELETE", | ||||||
|  |             headers: { | ||||||
|  |                 "X-CSRFTOKEN": CSRF_TOKEN | ||||||
|  |             } | ||||||
|  |         }).done(function () { | ||||||
|  |             addMsg("{% trans 'Activity deleted' %}", "success"); | ||||||
|  |             window.location.href = "/activity/";  // Redirige vers la liste des activités | ||||||
|  |         }).fail(function (xhr) { | ||||||
|  |             errMsg(xhr.responseJSON); | ||||||
|  |         }); | ||||||
|  |     }); | ||||||
| </script> | </script> | ||||||
| {% endblock %} | {% endblock %} | ||||||
|   | |||||||
| @@ -70,7 +70,10 @@ SPDX-License-Identifier: GPL-3.0-or-later | |||||||
|             {% if ".change_"|has_perm:activity %} |             {% if ".change_"|has_perm:activity %} | ||||||
|                 <a class="btn btn-primary btn-sm my-1" href="{% url 'activity:activity_update' pk=activity.pk %}" data-turbolinks="false"> {% trans "edit"|capfirst %}</a> |                 <a class="btn btn-primary btn-sm my-1" href="{% url 'activity:activity_update' pk=activity.pk %}" data-turbolinks="false"> {% trans "edit"|capfirst %}</a> | ||||||
|             {% endif %} |             {% endif %} | ||||||
|             {% if activity.activity_type.can_invite and not activity_started %} |             {% if not activity.valid and ".delete_"|has_perm:activity %} | ||||||
|  |                 <a class="btn btn-danger btn-sm my-1" id="delete_activity"> {% trans "delete"|capfirst %} </a> | ||||||
|  |             {% endif %} | ||||||
|  |             {% if activity.activity_type.can_invite and not activity_started and activity.valid %} | ||||||
|                 <a class="btn btn-primary btn-sm my-1" href="{% url 'activity:activity_invite' pk=activity.pk %}" data-turbolinks="false"> {% trans "Invite" %}</a> |                 <a class="btn btn-primary btn-sm my-1" href="{% url 'activity:activity_invite' pk=activity.pk %}" data-turbolinks="false"> {% trans "Invite" %}</a> | ||||||
|             {% endif %} |             {% endif %} | ||||||
|         {% endif %} |         {% endif %} | ||||||
|   | |||||||
| @@ -15,4 +15,5 @@ urlpatterns = [ | |||||||
|     path('<int:pk>/update/', views.ActivityUpdateView.as_view(), name='activity_update'), |     path('<int:pk>/update/', views.ActivityUpdateView.as_view(), name='activity_update'), | ||||||
|     path('new/', views.ActivityCreateView.as_view(), name='activity_create'), |     path('new/', views.ActivityCreateView.as_view(), name='activity_create'), | ||||||
|     path('calendar.ics', views.CalendarView.as_view(), name='calendar_ics'), |     path('calendar.ics', views.CalendarView.as_view(), name='calendar_ics'), | ||||||
|  |     path('<int:pk>/delete', views.ActivityDeleteView.as_view(), name='delete_activity'), | ||||||
| ] | ] | ||||||
|   | |||||||
| @@ -9,7 +9,7 @@ from django.contrib.contenttypes.models import ContentType | |||||||
| from django.core.exceptions import PermissionDenied | from django.core.exceptions import PermissionDenied | ||||||
| from django.db import transaction | from django.db import transaction | ||||||
| from django.db.models import F, Q | from django.db.models import F, Q | ||||||
| from django.http import HttpResponse | from django.http import HttpResponse, JsonResponse | ||||||
| from django.urls import reverse_lazy | from django.urls import reverse_lazy | ||||||
| from django.utils import timezone | from django.utils import timezone | ||||||
| from django.utils.decorators import method_decorator | from django.utils.decorators import method_decorator | ||||||
| @@ -153,6 +153,34 @@ class ActivityUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): | |||||||
|         return reverse_lazy('activity:activity_detail', kwargs={"pk": self.kwargs["pk"]}) |         return reverse_lazy('activity:activity_detail', kwargs={"pk": self.kwargs["pk"]}) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class ActivityDeleteView(View): | ||||||
|  |     """ | ||||||
|  |     Deletes an Activity | ||||||
|  |     """ | ||||||
|  |     def delete(self, request, pk): | ||||||
|  |         try: | ||||||
|  |             activity = Activity.objects.get(pk=pk) | ||||||
|  |             activity.delete() | ||||||
|  |             return JsonResponse({"message": "Activity deleted"}) | ||||||
|  |         except Activity.DoesNotExist: | ||||||
|  |             return JsonResponse({"error": "Activity not found"}, status=404) | ||||||
|  |  | ||||||
|  |     def dispatch(self, *args, **kwargs): | ||||||
|  |         """ | ||||||
|  |         Don't display the delete button if the user has no right to delete. | ||||||
|  |         """ | ||||||
|  |         if not self.request.user.is_authenticated: | ||||||
|  |             return self.handle_no_permission() | ||||||
|  |  | ||||||
|  |         activity = Activity.objects.get(pk=self.kwargs["pk"]) | ||||||
|  |         if not PermissionBackend.check_perm(self.request, "activity.delete_activity", activity): | ||||||
|  |             raise PermissionDenied(_("You are not allowed to delete this activity.")) | ||||||
|  |  | ||||||
|  |         if activity.valid: | ||||||
|  |             raise PermissionDenied(_("This activity is valid.")) | ||||||
|  |         return super().dispatch(*args, **kwargs) | ||||||
|  |  | ||||||
|  |  | ||||||
| class ActivityInviteView(ProtectQuerysetMixin, ProtectedCreateView): | class ActivityInviteView(ProtectQuerysetMixin, ProtectedCreateView): | ||||||
|     """ |     """ | ||||||
|     Invite a Guest, The rules to invites someone are defined in `forms:activity.GuestForm` |     Invite a Guest, The rules to invites someone are defined in `forms:activity.GuestForm` | ||||||
|   | |||||||
| @@ -3,7 +3,7 @@ | |||||||
|  |  | ||||||
| from rest_framework import serializers | from rest_framework import serializers | ||||||
|  |  | ||||||
| from ..models import Allergen, BasicFood, TransformedFood, QRCode | from ..models import Allergen, Food, BasicFood, TransformedFood, QRCode | ||||||
|  |  | ||||||
|  |  | ||||||
| class AllergenSerializer(serializers.ModelSerializer): | class AllergenSerializer(serializers.ModelSerializer): | ||||||
| @@ -16,6 +16,16 @@ class AllergenSerializer(serializers.ModelSerializer): | |||||||
|         fields = '__all__' |         fields = '__all__' | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class FoodSerializer(serializers.ModelSerializer): | ||||||
|  |     """ | ||||||
|  |     REST API Serializer for Food. | ||||||
|  |     The djangorestframework plugin will analyse the model `Food` and parse all fields in the API. | ||||||
|  |     """ | ||||||
|  |     class Meta: | ||||||
|  |         model = Food | ||||||
|  |         fields = '__all__' | ||||||
|  |  | ||||||
|  |  | ||||||
| class BasicFoodSerializer(serializers.ModelSerializer): | class BasicFoodSerializer(serializers.ModelSerializer): | ||||||
|     """ |     """ | ||||||
|     REST API Serializer for BasicFood. |     REST API Serializer for BasicFood. | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| # Copyright (C) 2018-2025 by BDE ENS Paris-Saclay | # Copyright (C) 2018-2025 by BDE ENS Paris-Saclay | ||||||
| # SPDX-License-Identifier: GPL-3.0-or-later | # SPDX-License-Identifier: GPL-3.0-or-later | ||||||
|  |  | ||||||
| from .views import AllergenViewSet, BasicFoodViewSet, TransformedFoodViewSet, QRCodeViewSet | from .views import AllergenViewSet, FoodViewSet, BasicFoodViewSet, TransformedFoodViewSet, QRCodeViewSet | ||||||
|  |  | ||||||
|  |  | ||||||
| def register_food_urls(router, path): | def register_food_urls(router, path): | ||||||
| @@ -9,6 +9,7 @@ def register_food_urls(router, path): | |||||||
|     Configure router for Food REST API. |     Configure router for Food REST API. | ||||||
|     """ |     """ | ||||||
|     router.register(path + '/allergen', AllergenViewSet) |     router.register(path + '/allergen', AllergenViewSet) | ||||||
|  |     router.register(path + '/food', FoodViewSet) | ||||||
|     router.register(path + '/basicfood', BasicFoodViewSet) |     router.register(path + '/basicfood', BasicFoodViewSet) | ||||||
|     router.register(path + '/transformedfood', TransformedFoodViewSet) |     router.register(path + '/transformedfood', TransformedFoodViewSet) | ||||||
|     router.register(path + '/qrcode', QRCodeViewSet) |     router.register(path + '/qrcode', QRCodeViewSet) | ||||||
|   | |||||||
| @@ -5,8 +5,8 @@ from api.viewsets import ReadProtectedModelViewSet | |||||||
| from django_filters.rest_framework import DjangoFilterBackend | from django_filters.rest_framework import DjangoFilterBackend | ||||||
| from rest_framework.filters import SearchFilter | from rest_framework.filters import SearchFilter | ||||||
|  |  | ||||||
| from .serializers import AllergenSerializer, BasicFoodSerializer, TransformedFoodSerializer, QRCodeSerializer | from .serializers import AllergenSerializer, FoodSerializer, BasicFoodSerializer, TransformedFoodSerializer, QRCodeSerializer | ||||||
| from ..models import Allergen, BasicFood, TransformedFood, QRCode | from ..models import Allergen, Food, BasicFood, TransformedFood, QRCode | ||||||
|  |  | ||||||
|  |  | ||||||
| class AllergenViewSet(ReadProtectedModelViewSet): | class AllergenViewSet(ReadProtectedModelViewSet): | ||||||
| @@ -22,6 +22,19 @@ class AllergenViewSet(ReadProtectedModelViewSet): | |||||||
|     search_fields = ['$name', ] |     search_fields = ['$name', ] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class FoodViewSet(ReadProtectedModelViewSet): | ||||||
|  |     """ | ||||||
|  |     REST API View set. | ||||||
|  |     The djangorestframework plugin will get all `Food` objects, serialize it to JSON with the given serializer, | ||||||
|  |     then render it on /api/food/food/ | ||||||
|  |     """ | ||||||
|  |     queryset = Food.objects.order_by('id') | ||||||
|  |     serializer_class = FoodSerializer | ||||||
|  |     filter_backends = [DjangoFilterBackend, SearchFilter] | ||||||
|  |     filterset_fields = ['name', ] | ||||||
|  |     search_fields = ['$name', ] | ||||||
|  |  | ||||||
|  |  | ||||||
| class BasicFoodViewSet(ReadProtectedModelViewSet): | class BasicFoodViewSet(ReadProtectedModelViewSet): | ||||||
|     """ |     """ | ||||||
|     REST API View set. |     REST API View set. | ||||||
|   | |||||||
							
								
								
									
										100
									
								
								apps/food/fixtures/initial.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										100
									
								
								apps/food/fixtures/initial.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,100 @@ | |||||||
|  | [ | ||||||
|  |     { | ||||||
|  | 	"model": "food.allergen", | ||||||
|  | 	"pk": 1, | ||||||
|  | 	"fields": { | ||||||
|  | 	    "name": "Lait" | ||||||
|  | 	} | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  | 	"model": "food.allergen", | ||||||
|  | 	"pk": 2, | ||||||
|  | 	"fields": { | ||||||
|  | 	    "name": "Oeufs" | ||||||
|  | 	} | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  | 	"model": "food.allergen", | ||||||
|  | 	"pk": 3, | ||||||
|  | 	"fields": { | ||||||
|  | 	    "name": "Gluten" | ||||||
|  | 	} | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  | 	"model": "food.allergen", | ||||||
|  | 	"pk": 4, | ||||||
|  | 	"fields": { | ||||||
|  | 	    "name": "Fruits à coques" | ||||||
|  | 	} | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  | 	"model": "food.allergen", | ||||||
|  | 	"pk": 5, | ||||||
|  | 	"fields": { | ||||||
|  | 	    "name": "Arachides" | ||||||
|  | 	} | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  | 	"model": "food.allergen", | ||||||
|  | 	"pk": 6, | ||||||
|  | 	"fields": { | ||||||
|  | 	    "name": "Sésame" | ||||||
|  | 	} | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  | 	"model": "food.allergen", | ||||||
|  | 	"pk": 7, | ||||||
|  | 	"fields": { | ||||||
|  | 	    "name": "Soja" | ||||||
|  | 	} | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  | 	"model": "food.allergen", | ||||||
|  | 	"pk": 8, | ||||||
|  | 	"fields": { | ||||||
|  | 	    "name": "Céleri" | ||||||
|  | 	} | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  | 	"model": "food.allergen", | ||||||
|  | 	"pk": 9, | ||||||
|  | 	"fields": { | ||||||
|  | 	    "name": "Lupin" | ||||||
|  | 	} | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  | 	"model": "food.allergen", | ||||||
|  | 	"pk": 10, | ||||||
|  | 	"fields": { | ||||||
|  | 	    "name": "Moutarde" | ||||||
|  | 	} | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  | 	"model": "food.allergen", | ||||||
|  | 	"pk": 11, | ||||||
|  | 	"fields": { | ||||||
|  | 	    "name": "Sulfites" | ||||||
|  | 	} | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  | 	"model": "food.allergen", | ||||||
|  | 	"pk": 12, | ||||||
|  | 	"fields": { | ||||||
|  | 	    "name": "Crustacés" | ||||||
|  | 	} | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  | 	"model": "food.allergen", | ||||||
|  | 	"pk": 13, | ||||||
|  | 	"fields": { | ||||||
|  | 	    "name": "Mollusques" | ||||||
|  | 	} | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  | 	"model": "food.allergen", | ||||||
|  | 	"pk": 14, | ||||||
|  | 	"fields": { | ||||||
|  | 	    "name": "Poissons" | ||||||
|  | 	} | ||||||
|  |     } | ||||||
|  | ] | ||||||
| @@ -12,7 +12,7 @@ from note_kfet.inputs import Autocomplete | |||||||
| from note_kfet.middlewares import get_current_request | from note_kfet.middlewares import get_current_request | ||||||
| from permission.backends import PermissionBackend | from permission.backends import PermissionBackend | ||||||
|  |  | ||||||
| from .models import BasicFood, TransformedFood, QRCode | from .models import Food, BasicFood, TransformedFood, QRCode | ||||||
|  |  | ||||||
|  |  | ||||||
| class QRCodeForms(forms.ModelForm): | class QRCodeForms(forms.ModelForm): | ||||||
| @@ -22,7 +22,6 @@ class QRCodeForms(forms.ModelForm): | |||||||
|     def __init__(self, *args, **kwargs): |     def __init__(self, *args, **kwargs): | ||||||
|         super().__init__(*args, **kwargs) |         super().__init__(*args, **kwargs) | ||||||
|         self.fields['food_container'].queryset = self.fields['food_container'].queryset.filter( |         self.fields['food_container'].queryset = self.fields['food_container'].queryset.filter( | ||||||
|             is_ready=False, |  | ||||||
|             end_of_life__isnull=True, |             end_of_life__isnull=True, | ||||||
|             polymorphic_ctype__model='transformedfood', |             polymorphic_ctype__model='transformedfood', | ||||||
|         ).filter(PermissionBackend.filter_queryset( |         ).filter(PermissionBackend.filter_queryset( | ||||||
| @@ -151,3 +150,38 @@ class AddIngredientForms(forms.ModelForm): | |||||||
|     class Meta: |     class Meta: | ||||||
|         model = TransformedFood |         model = TransformedFood | ||||||
|         fields = ('ingredients',) |         fields = ('ingredients',) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class ManageIngredientsForm(forms.Form): | ||||||
|  |     """ | ||||||
|  |     Form to manage ingredient | ||||||
|  |     """ | ||||||
|  |     fully_used = forms.BooleanField() | ||||||
|  |     fully_used.initial = True | ||||||
|  |     fully_used.required = True | ||||||
|  |     fully_used.label = _('Fully used') | ||||||
|  |  | ||||||
|  |     name = forms.CharField() | ||||||
|  |     name.widget = Autocomplete( | ||||||
|  |         model=Food, | ||||||
|  |         resetable=True, | ||||||
|  |         attrs={"api_url": "/api/food/food", | ||||||
|  |                "class": "autocomplete"}, | ||||||
|  |     ) | ||||||
|  |     name.label = _('Name') | ||||||
|  |  | ||||||
|  |     qrcode = forms.IntegerField() | ||||||
|  |     qrcode.widget = Autocomplete( | ||||||
|  |         model=QRCode, | ||||||
|  |         resetable=True, | ||||||
|  |         attrs={"api_url": "/api/food/qrcode/", | ||||||
|  |                "name_field": "qr_code_number", | ||||||
|  |                "class": "autocomplete"}, | ||||||
|  |     ) | ||||||
|  |     qrcode.label = _('QR code number') | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ManageIngredientsFormSet = forms.formset_factory( | ||||||
|  |     ManageIngredientsForm, | ||||||
|  |     extra=1, | ||||||
|  | ) | ||||||
|   | |||||||
| @@ -39,6 +39,11 @@ SPDX-License-Identifier: GPL-3.0-or-later | |||||||
| 	<a class="btn btn-sm btn-primary" href="{% url "food:add_ingredient" pk=food.pk %}"> | 	<a class="btn btn-sm btn-primary" href="{% url "food:add_ingredient" pk=food.pk %}"> | ||||||
| 	  {% trans "Add to a meal" %} | 	  {% trans "Add to a meal" %} | ||||||
| 	</a> | 	</a> | ||||||
|  |       {% endif %} | ||||||
|  |       {% if manage_ingredients %} | ||||||
|  |         <a class="btn btn-sm btn-secondary" href="{% url "food:manage_ingredients" pk=food.pk %}"> | ||||||
|  | 	  {% trans "Manage ingredients" %} | ||||||
|  | 	</a> | ||||||
|       {% endif %} |       {% endif %} | ||||||
| 	<a class="btn btn-sm btn-primary" href="{% url "food:food_list" %}"> | 	<a class="btn btn-sm btn-primary" href="{% url "food:food_list" %}"> | ||||||
| 	  {% trans "Return to the food list" %} | 	  {% trans "Return to the food list" %} | ||||||
|   | |||||||
							
								
								
									
										116
									
								
								apps/food/templates/food/manage_ingredients.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										116
									
								
								apps/food/templates/food/manage_ingredients.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,116 @@ | |||||||
|  | {% 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"></div> | ||||||
|  |   <form method="post" action=""> | ||||||
|  |     {% csrf_token %} | ||||||
|  |     <table class="table table-condensed table-striped"> | ||||||
|  |       {# Fill initial data #} | ||||||
|  |       {% for display, form in formset %} | ||||||
|  |       {% if forloop.first %} | ||||||
|  |       <thead> | ||||||
|  | 	<tr> | ||||||
|  | 	  <th>{{ form.name.label }}</th> | ||||||
|  | 	  <th>{{ form.qrcode.label }}</th> | ||||||
|  | 	  <th>{{ form.fully_used.label }}</th> | ||||||
|  | 	</tr> | ||||||
|  |       </thead> | ||||||
|  |       <tbody id="form_body"> | ||||||
|  | 	{% endif %} | ||||||
|  | 	{% if display %} | ||||||
|  | 	<tr class="row-formset ingredients"> | ||||||
|  | 	{% else %} | ||||||
|  | 	<tr class="row-formset ingredients" style="display: none"> | ||||||
|  | 	{% endif %} | ||||||
|  | 	  <td>{{ form.name }}</td> | ||||||
|  | 	  <td>{{ form.qrcode }}</td> | ||||||
|  | 	  <td>{{ form.fully_used }}</td> | ||||||
|  | 	</tr> | ||||||
|  |       {% endfor %} | ||||||
|  |       </tbody> | ||||||
|  |     </table> | ||||||
|  |  | ||||||
|  |     {# 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">{% trans "Submit"%}</button> | ||||||
|  |     </div> | ||||||
|  |   </form> | ||||||
|  | </div> | ||||||
|  |  | ||||||
|  | {% endblock %} | ||||||
|  | {% block extrajavascript %}  | ||||||
|  | <script> | ||||||
|  | /* script that handles add and remove lines */ | ||||||
|  |  | ||||||
|  | const foods = {{ ingredients | safe }}; | ||||||
|  |  | ||||||
|  | function set_ingredient_id () { | ||||||
|  | 	let ingredients = document.getElementsByClassName('ingredients'); | ||||||
|  | 	for (var i = 0; i < ingredients.length; i++) { | ||||||
|  | 		ingredients[i].id = 'ingredients-' + parseInt(i); | ||||||
|  | 	}; | ||||||
|  | } | ||||||
|  | set_ingredient_id(); | ||||||
|  |  | ||||||
|  | function prepopulate () { | ||||||
|  | 	for (var i = 0; i < {{ ingredients_count }}; i++) { | ||||||
|  | 		let prefix = 'id_form-' + parseInt(i) + '-'; | ||||||
|  | 		document.getElementById(prefix + 'name_pk').value = parseInt(foods[i]['food_pk']); | ||||||
|  | 		document.getElementById(prefix + 'name').value = foods[i]['food_name']; | ||||||
|  | 		document.getElementById(prefix + 'qrcode_pk').value = parseInt(foods[i]['qr_pk']); | ||||||
|  | 		if (foods[i]['qr_number'] === '') { | ||||||
|  | 			document.getElementById(prefix + 'qrcode').value = ''; | ||||||
|  | 		} | ||||||
|  | 		else { | ||||||
|  | 		document.getElementById(prefix + 'qrcode').value = parseInt(foods[i]['qr_number']); | ||||||
|  | 		}; | ||||||
|  | 		document.getElementById(prefix + 'fully_used').checked = Boolean(foods[i]['fully_used']); | ||||||
|  | 	}; | ||||||
|  | } | ||||||
|  | prepopulate(); | ||||||
|  |  | ||||||
|  | function delete_form_data (form_id) { | ||||||
|  | 	let prefix = "id_form-" + parseInt(form_id) + "-"; | ||||||
|  | 	document.getElementById(prefix + "name_pk").value = ""; | ||||||
|  | 	document.getElementById(prefix + "name").value = ""; | ||||||
|  | 	document.getElementById(prefix + "qrcode_pk").value = ""; | ||||||
|  | 	document.getElementById(prefix + "qrcode").value = ""; | ||||||
|  | 	document.getElementById(prefix + "fully_used").checked = true; | ||||||
|  | } | ||||||
|  | var form_count = {{ ingredients_count }} + 1; | ||||||
|  |  | ||||||
|  | $('#add_more').click(function () { | ||||||
|  | 	let ingredient_form = document.getElementById('ingredients-' + parseInt(form_count)); | ||||||
|  | 	if (ingredient_form === null) { | ||||||
|  | 		addMsg(gettext("You can't add more ingredient"), "danger",  5000); | ||||||
|  | 		return;}; | ||||||
|  | 	ingredient_form.style = "display: true"; | ||||||
|  | 	form_count += 1; | ||||||
|  | }); | ||||||
|  |    | ||||||
|  | $('#remove_one').click(function () { | ||||||
|  | 	let ingredient_form = document.getElementById('ingredients-' + parseInt(form_count - 1)); | ||||||
|  | 	if (ingredient_form === null) { | ||||||
|  | 		return;}; | ||||||
|  | 	ingredient_form.style = "display: none"; | ||||||
|  | 	delete_form_data(form_count - 1); | ||||||
|  | 	form_count -= 1; | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | addMsg(gettext("Add ingredient with their name or their qrcode, if two different priority is given to qrcode"), "warning");  | ||||||
|  |  | ||||||
|  | </script> | ||||||
|  | {% endblock %} | ||||||
							
								
								
									
										87
									
								
								apps/food/templates/food/transformedfood_update.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								apps/food/templates/food/transformedfood_update.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,87 @@ | |||||||
|  | {% extends "base.html" %} | ||||||
|  | {% comment %} | ||||||
|  | Copyright (C) by BDE ENS Paris-Saclay | ||||||
|  | SPDX-License-Identifier: GPL-3.0-or-later | ||||||
|  | {% endcomment %} | ||||||
|  | {% load i18n crispy_forms_tags %} | ||||||
|  |  | ||||||
|  | {% block content %} | ||||||
|  | <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 }} | ||||||
|  |       <table class="table table-condensed table-striped"> | ||||||
|  |         {# Fill initial data #} | ||||||
|  |         {% for ingredient_form in formset %} | ||||||
|  |         {% if forloop.first %} | ||||||
|  |         <thead> | ||||||
|  |           <tr> | ||||||
|  | 	    <th>{% trans "Name" %}</th> | ||||||
|  | 	    <th>{% trans "QR-code number" %}</th> | ||||||
|  | 	    <th>{% trans "Fully used" %}<th> | ||||||
|  |           </tr> | ||||||
|  |         </thead> | ||||||
|  |         <tbody id="form_body"> | ||||||
|  |         {% endif %} | ||||||
|  |         <tr class="row-formset"> | ||||||
|  | 		{{ ingredient_form | crispy }} | ||||||
|  |           <td>{{ ingredient_form.name }}</td> | ||||||
|  | 	  <td>{{ ingredient_form.qrcode }}</td> | ||||||
|  | 	  <td>{{ ingredient_form.fully_used }}</td> | ||||||
|  |         </tr> | ||||||
|  |         {% endfor %} | ||||||
|  |         </tbody> | ||||||
|  |       </table> | ||||||
|  |       {# Display buttons to add and remove products #} | ||||||
|  |       <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 type="submit" class="btn btn-block btn-primary">{% trans "Submit" %}</button> | ||||||
|  |       </div> | ||||||
|  |     </form> | ||||||
|  |   </div> | ||||||
|  | </div> | ||||||
|  |  | ||||||
|  | {# Hidden div that store an empty product 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> | ||||||
|  | 		<td>{{ formset.empty_form.qrcode }}</td> | ||||||
|  | 		<td>{{ formset.empty_form.fully_used }}</td> | ||||||
|  |             </tr> | ||||||
|  |         </tbody> | ||||||
|  |     </table> | ||||||
|  | </div> | ||||||
|  | {% endblock %} | ||||||
|  | {% block extrajavascript %} | ||||||
|  | <script> | ||||||
|  |     /* script that handles add and remove lines */ | ||||||
|  |     IDS = {}; | ||||||
|  |  | ||||||
|  |     $("#id_form-TOTAL_FORMS").val($(".row-formset").length - 1); | ||||||
|  |  | ||||||
|  |     $('#add_more').click(function () { | ||||||
|  |         let form_idx = $('#id_form-TOTAL_FORMS').val(); | ||||||
|  |         $('#form_body').append($('#for_real').html().replace(/__prefix__/g, form_idx)); | ||||||
|  |         $('#id_form-TOTAL_FORMS').val(parseInt(form_idx) + 1); | ||||||
|  |         $('#id_form-' + parseInt(form_idx) + '-id').val(IDS[parseInt(form_idx)]); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     $('#remove_one').click(function () { | ||||||
|  |         let form_idx = $('#id_form-TOTAL_FORMS').val(); | ||||||
|  |         if (form_idx > 0) { | ||||||
|  |             IDS[parseInt(form_idx) - 1] = $('#id_form-' + (parseInt(form_idx) - 1) + '-id').val(); | ||||||
|  |             $('#form_body tr:last-child').remove(); | ||||||
|  |             $('#id_form-TOTAL_FORMS').val(parseInt(form_idx) - 1); | ||||||
|  |         } | ||||||
|  |     }); | ||||||
|  | </script> | ||||||
|  | {% endblock %} | ||||||
| @@ -13,6 +13,7 @@ urlpatterns = [ | |||||||
|     path('<int:slug>/add/basic', views.BasicFoodCreateView.as_view(), name='basicfood_create'), |     path('<int:slug>/add/basic', views.BasicFoodCreateView.as_view(), name='basicfood_create'), | ||||||
|     path('add/transformed', views.TransformedFoodCreateView.as_view(), name='transformedfood_create'), |     path('add/transformed', views.TransformedFoodCreateView.as_view(), name='transformedfood_create'), | ||||||
|     path('update/<int:pk>', views.FoodUpdateView.as_view(), name='food_update'), |     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/<int:pk>', views.FoodDetailView.as_view(), name='food_view'), | ||||||
|     path('detail/basic/<int:pk>', views.BasicFoodDetailView.as_view(), name='basicfood_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('detail/transformed/<int:pk>', views.TransformedFoodDetailView.as_view(), name='transformedfood_view'), | ||||||
|   | |||||||
| @@ -7,8 +7,8 @@ from api.viewsets import is_regex | |||||||
| from django_tables2.views import MultiTableMixin | from django_tables2.views import MultiTableMixin | ||||||
| from django.db import transaction | from django.db import transaction | ||||||
| from django.db.models import Q | from django.db.models import Q | ||||||
| from django.http import HttpResponseRedirect | from django.http import HttpResponseRedirect, Http404 | ||||||
| from django.views.generic import DetailView, UpdateView | from django.views.generic import DetailView, UpdateView, CreateView | ||||||
| from django.views.generic.list import ListView | from django.views.generic.list import ListView | ||||||
| from django.urls import reverse_lazy | from django.urls import reverse_lazy | ||||||
| from django.utils import timezone | from django.utils import timezone | ||||||
| @@ -18,7 +18,9 @@ from permission.backends import PermissionBackend | |||||||
| from permission.views import ProtectQuerysetMixin, ProtectedCreateView, LoginRequiredMixin | from permission.views import ProtectQuerysetMixin, ProtectedCreateView, LoginRequiredMixin | ||||||
|  |  | ||||||
| from .models import Food, BasicFood, TransformedFood, QRCode | from .models import Food, BasicFood, TransformedFood, QRCode | ||||||
| from .forms import AddIngredientForms, BasicFoodForms, TransformedFoodForms, BasicFoodUpdateForms, TransformedFoodUpdateForms, QRCodeForms | from .forms import QRCodeForms, BasicFoodForms, TransformedFoodForms, \ | ||||||
|  |     ManageIngredientsForm, ManageIngredientsFormSet, AddIngredientForms, \ | ||||||
|  |     BasicFoodUpdateForms, TransformedFoodUpdateForms | ||||||
| from .tables import FoodTable | from .tables import FoodTable | ||||||
| from .utils import pretty_duration | from .utils import pretty_duration | ||||||
|  |  | ||||||
| @@ -69,11 +71,17 @@ class FoodListView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin, Li | |||||||
|         open_table = self.get_queryset().order_by('expiry_date').filter( |         open_table = self.get_queryset().order_by('expiry_date').filter( | ||||||
|             Q(polymorphic_ctype__model='transformedfood') |             Q(polymorphic_ctype__model='transformedfood') | ||||||
|             | Q(polymorphic_ctype__model='basicfood', basicfood__date_type='DLC')).filter( |             | Q(polymorphic_ctype__model='basicfood', basicfood__date_type='DLC')).filter( | ||||||
|                 expiry_date__lt=timezone.now()).filter( |                 expiry_date__lt=timezone.now(), end_of_life='').filter( | ||||||
|                     PermissionBackend.filter_queryset(self.request, Food, 'view')) |                     PermissionBackend.filter_queryset(self.request, Food, 'view')) | ||||||
|         # table served |         # table served | ||||||
|         served_table = self.get_queryset().order_by('-pk').filter( |         served_table = self.get_queryset().order_by('-pk').filter( | ||||||
|             end_of_life='', is_ready=True) |             end_of_life='', is_ready=True).exclude( | ||||||
|  |                 Q(polymorphic_ctype__model='basicfood', | ||||||
|  |                   basicfood__date_type='DLC', | ||||||
|  |                   expiry_date__lte=timezone.now(),) | ||||||
|  |                 | Q(polymorphic_ctype__model='transformedfood', | ||||||
|  |                     expiry_date__lte=timezone.now(), | ||||||
|  |                     )) | ||||||
|         # tables club |         # tables club | ||||||
|         bureau_role_pk = 4 |         bureau_role_pk = 4 | ||||||
|         clubs = Club.objects.filter(membership__in=Membership.objects.filter( |         clubs = Club.objects.filter(membership__in=Membership.objects.filter( | ||||||
| @@ -100,7 +108,7 @@ class FoodListView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin, Li | |||||||
|         return context |         return context | ||||||
|  |  | ||||||
|  |  | ||||||
| class QRCodeCreateView(ProtectQuerysetMixin, ProtectedCreateView): | class QRCodeCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView): | ||||||
|     """ |     """ | ||||||
|     A view to add qrcode |     A view to add qrcode | ||||||
|     """ |     """ | ||||||
| @@ -237,7 +245,72 @@ class TransformedFoodCreateView(ProtectQuerysetMixin, ProtectedCreateView): | |||||||
|         return reverse_lazy('food:transformedfood_view', kwargs={"pk": self.object.pk}) |         return reverse_lazy('food:transformedfood_view', kwargs={"pk": self.object.pk}) | ||||||
|  |  | ||||||
|  |  | ||||||
| class AddIngredientView(ProtectQuerysetMixin, UpdateView): | MAX_FORMS = 10 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class ManageIngredientsView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): | ||||||
|  |     """ | ||||||
|  |     A view to manage ingredient for a transformed food | ||||||
|  |     """ | ||||||
|  |     model = TransformedFood | ||||||
|  |     fields = ['ingredients'] | ||||||
|  |     extra_context = {"title": _("Manage ingredients of:")} | ||||||
|  |     template_name = 'food/manage_ingredients.html' | ||||||
|  |  | ||||||
|  |     @transaction.atomic | ||||||
|  |     def form_valid(self, form): | ||||||
|  |         old_ingredients = list(self.object.ingredients.all()).copy() | ||||||
|  |         old_allergens = list(self.object.allergens.all()).copy() | ||||||
|  |         self.object.ingredients.clear() | ||||||
|  |         for i in range(self.object.ingredients.all().count() + 1 + MAX_FORMS): | ||||||
|  |             prefix = 'form-' + str(i) + '-' | ||||||
|  |             if form.data[prefix + 'qrcode'] not in ['0', '']: | ||||||
|  |                 ingredient = QRCode.objects.get(pk=form.data[prefix + 'qrcode']).food_container | ||||||
|  |                 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() | ||||||
|  |  | ||||||
|  |         self.object.save(old_ingredients=old_ingredients, old_allergens=old_allergens) | ||||||
|  |         return HttpResponseRedirect(self.get_success_url()) | ||||||
|  |  | ||||||
|  |     def get_context_data(self, *args, **kwargs): | ||||||
|  |         context = super().get_context_data(*args, **kwargs) | ||||||
|  |         context['title'] += ' ' + self.object.name | ||||||
|  |         formset = ManageIngredientsFormSet() | ||||||
|  |         ingredients = self.object.ingredients.all() | ||||||
|  |         formset.extra += ingredients.count() + MAX_FORMS | ||||||
|  |         context['form'] = ManageIngredientsForm() | ||||||
|  |         context['ingredients_count'] = ingredients.count() | ||||||
|  |         display = [True] * (1 + ingredients.count()) + [False] * (formset.extra - ingredients.count() - 1) | ||||||
|  |         context['formset'] = zip(display, formset) | ||||||
|  |         context['ingredients'] = [] | ||||||
|  |         for ingredient in ingredients: | ||||||
|  |             qr = QRCode.objects.filter(food_container=ingredient) | ||||||
|  |  | ||||||
|  |             context['ingredients'].append({ | ||||||
|  |                 'food_pk': ingredient.pk, | ||||||
|  |                 'food_name': ingredient.name, | ||||||
|  |                 'qr_pk': '' if qr.count() == 0 else qr[0].pk, | ||||||
|  |                 '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): | ||||||
|  |         return reverse_lazy('food:transformedfood_view', kwargs={"pk": self.object.pk}) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class AddIngredientView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): | ||||||
|     """ |     """ | ||||||
|     A view to add ingredient to a meal |     A view to add ingredient to a meal | ||||||
|     """ |     """ | ||||||
| @@ -254,6 +327,8 @@ class AddIngredientView(ProtectQuerysetMixin, UpdateView): | |||||||
|     @transaction.atomic |     @transaction.atomic | ||||||
|     def form_valid(self, form): |     def form_valid(self, form): | ||||||
|         meals = TransformedFood.objects.filter(pk__in=form.data.getlist('ingredients')).all() |         meals = TransformedFood.objects.filter(pk__in=form.data.getlist('ingredients')).all() | ||||||
|  |         if not meals: | ||||||
|  |             return HttpResponseRedirect(reverse_lazy('food:food_view', kwargs={"pk": self.object.pk})) | ||||||
|         for meal in meals: |         for meal in meals: | ||||||
|             old_ingredients = list(meal.ingredients.all()).copy() |             old_ingredients = list(meal.ingredients.all()).copy() | ||||||
|             old_allergens = list(meal.allergens.all()).copy() |             old_allergens = list(meal.allergens.all()).copy() | ||||||
| @@ -354,10 +429,12 @@ class FoodDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): | |||||||
|             value) for field, value in fields.items()] |             value) for field, value in fields.items()] | ||||||
|         context["meals"] = self.object.transformed_ingredient_inv.all() |         context["meals"] = self.object.transformed_ingredient_inv.all() | ||||||
|         context["update"] = PermissionBackend.check_perm(self.request, "food.change_food") |         context["update"] = PermissionBackend.check_perm(self.request, "food.change_food") | ||||||
|         context["add_ingredient"] = self.object.end_of_life = '' and PermissionBackend.check_perm(self.request, "food.change_transformedfood") |         context["add_ingredient"] = (self.object.end_of_life == '' and PermissionBackend.check_perm(self.request, "food.change_transformedfood")) | ||||||
|         return context |         return context | ||||||
|  |  | ||||||
|     def get(self, *args, **kwargs): |     def get(self, *args, **kwargs): | ||||||
|  |         if Food.objects.filter(pk=kwargs['pk']).count() != 1: | ||||||
|  |             return Http404 | ||||||
|         model = Food.objects.get(pk=kwargs['pk']).polymorphic_ctype.model |         model = Food.objects.get(pk=kwargs['pk']).polymorphic_ctype.model | ||||||
|         if 'stop_redirect' in kwargs and kwargs['stop_redirect']: |         if 'stop_redirect' in kwargs and kwargs['stop_redirect']: | ||||||
|             return super().get(*args, **kwargs) |             return super().get(*args, **kwargs) | ||||||
| @@ -379,6 +456,7 @@ class BasicFoodDetailView(FoodDetailView): | |||||||
|         return context |         return context | ||||||
|  |  | ||||||
|     def get(self, *args, **kwargs): |     def get(self, *args, **kwargs): | ||||||
|  |         if Food.objects.filter(pk=kwargs['pk']).count() == 1: | ||||||
|             kwargs['stop_redirect'] = (Food.objects.get(pk=kwargs['pk']).polymorphic_ctype.model == 'basicfood') |             kwargs['stop_redirect'] = (Food.objects.get(pk=kwargs['pk']).polymorphic_ctype.model == 'basicfood') | ||||||
|         return super().get(*args, **kwargs) |         return super().get(*args, **kwargs) | ||||||
|  |  | ||||||
| @@ -395,8 +473,10 @@ class TransformedFoodDetailView(FoodDetailView): | |||||||
|             pretty_duration(self.object.shelf_life) |             pretty_duration(self.object.shelf_life) | ||||||
|         )) |         )) | ||||||
|         context["foods"] = self.object.ingredients.all() |         context["foods"] = self.object.ingredients.all() | ||||||
|  |         context["manage_ingredients"] = True | ||||||
|         return context |         return context | ||||||
|  |  | ||||||
|     def get(self, *args, **kwargs): |     def get(self, *args, **kwargs): | ||||||
|  |         if Food.objects.filter(pk=kwargs['pk']).count() == 1: | ||||||
|             kwargs['stop_redirect'] = (Food.objects.get(pk=kwargs['pk']).polymorphic_ctype.model == 'transformedfood') |             kwargs['stop_redirect'] = (Food.objects.get(pk=kwargs['pk']).polymorphic_ctype.model == 'transformedfood') | ||||||
|         return super().get(*args, **kwargs) |         return super().get(*args, **kwargs) | ||||||
|   | |||||||
| @@ -3312,14 +3312,13 @@ | |||||||
| 	"fields": { | 	"fields": { | ||||||
| 	    "model": [ | 	    "model": [ | ||||||
| 		"food", | 		"food", | ||||||
|                 "transformedfood" | 		"qrcode" | ||||||
| 	    ], | 	    ], | ||||||
| 	    "query": "{}", | 	    "query": "{}", | ||||||
| 	    "type": "view", | 	    "type": "view", | ||||||
|             "mask": 3, | 	    "mask": 2, | ||||||
|             "field": "", |  | ||||||
| 	    "permanent": false, | 	    "permanent": false, | ||||||
|             "description": "Voir tout les plats" | 	    "description": "Voir n'importe quel QR-code" | ||||||
| 	} | 	} | ||||||
|     }, |     }, | ||||||
|     { |     { | ||||||
| @@ -3328,14 +3327,13 @@ | |||||||
| 	"fields": { | 	"fields": { | ||||||
| 	    "model": [ | 	    "model": [ | ||||||
| 		"food", | 		"food", | ||||||
|                 "transformedfood" | 		"allergen" | ||||||
| 	    ], | 	    ], | ||||||
|             "query": "{\"owner\": [\"club\"]}", | 	    "query": "{}", | ||||||
| 	    "type": "view", | 	    "type": "view", | ||||||
|             "mask": 3, | 	    "mask": 1, | ||||||
|             "field": "", |  | ||||||
| 	    "permanent": false, | 	    "permanent": false, | ||||||
|             "description": "Voir tout les plats de son club" | 	    "description": "Voir n'importe quel allergène" | ||||||
| 	} | 	} | ||||||
|     }, |     }, | ||||||
|     { |     { | ||||||
| @@ -3344,14 +3342,13 @@ | |||||||
| 	"fields": { | 	"fields": { | ||||||
| 	    "model": [ | 	    "model": [ | ||||||
| 		"food", | 		"food", | ||||||
|                 "transformedfood" | 		"food" | ||||||
| 	    ], | 	    ], | ||||||
|             "query": "{\"is_ready\": true}", | 	    "query": "{}", | ||||||
| 	    "type": "view", | 	    "type": "view", | ||||||
|             "mask": 1, | 	    "mask": 2, | ||||||
|             "field": "", |  | ||||||
| 	    "permanent": false, | 	    "permanent": false, | ||||||
|             "description": "Voir les plats préparés actifs servis" | 	    "description": "Voir n'importe quelle bouffe" | ||||||
| 	} | 	} | ||||||
|     }, |     }, | ||||||
|     { |     { | ||||||
| @@ -3364,10 +3361,9 @@ | |||||||
| 	    ], | 	    ], | ||||||
| 	    "query": "{}", | 	    "query": "{}", | ||||||
| 	    "type": "add", | 	    "type": "add", | ||||||
|             "mask": 3, | 	    "mask": 2, | ||||||
|             "field": "", |  | ||||||
| 	    "permanent": false, | 	    "permanent": false, | ||||||
|             "description": "Initialiser un QR code de traçabilité" | 	    "description": "Ajouter n'importe quel QR-code" | ||||||
| 	} | 	} | ||||||
|     }, |     }, | ||||||
|     { |     { | ||||||
| @@ -3376,14 +3372,13 @@ | |||||||
| 	"fields": { | 	"fields": { | ||||||
| 	    "model": [ | 	    "model": [ | ||||||
| 		"food", | 		"food", | ||||||
|                 "basicfood" | 		"food" | ||||||
| 	    ], | 	    ], | ||||||
|             "query": "{\"owner\": [\"club\"]}", | 	    "query": "{}", | ||||||
| 	    "type": "add", | 	    "type": "add", | ||||||
|             "mask": 3, | 	    "mask": 2, | ||||||
|             "field": "", |  | ||||||
| 	    "permanent": false, | 	    "permanent": false, | ||||||
|             "description": "Créer un nouvel ingrédient pour son club" | 	    "description": "Ajouter n'importe quelle bouffe" | ||||||
| 	} | 	} | ||||||
|     }, |     }, | ||||||
|     { |     { | ||||||
| @@ -3392,14 +3387,13 @@ | |||||||
| 	"fields": { | 	"fields": { | ||||||
| 	    "model": [ | 	    "model": [ | ||||||
| 		"food", | 		"food", | ||||||
|                 "basicfood" | 		"food" | ||||||
| 	    ], | 	    ], | ||||||
| 	    "query": "{}", | 	    "query": "{}", | ||||||
|             "type": "add", | 	    "type": "change", | ||||||
|             "mask": 3, | 	    "mask": 2, | ||||||
|             "field": "", |  | ||||||
| 	    "permanent": false, | 	    "permanent": false, | ||||||
|             "description": "Créer un nouvel ingrédient" | 	    "description": "Modifier n'importe quelle bouffe" | ||||||
| 	} | 	} | ||||||
|     }, |     }, | ||||||
|     { |     { | ||||||
| @@ -3408,14 +3402,13 @@ | |||||||
| 	"fields": { | 	"fields": { | ||||||
| 	    "model": [ | 	    "model": [ | ||||||
| 		"food", | 		"food", | ||||||
|                 "basicfood" | 		"qrcode" | ||||||
| 	    ], | 	    ], | ||||||
|             "query": "{}", | 	    "query": "{\"food_container__owner\": [\"club\"]}", | ||||||
| 	    "type": "view", | 	    "type": "view", | ||||||
|             "mask": 3, | 	    "mask": 2, | ||||||
|             "field": "", |  | ||||||
| 	    "permanent": false, | 	    "permanent": false, | ||||||
|             "description": "Voir toute la bouffe" | 	    "description": "Voir un QR-code lié à son club" | ||||||
|     	} |     	} | ||||||
|     }, |     }, | ||||||
|     { |     { | ||||||
| @@ -3424,14 +3417,13 @@ | |||||||
| 	"fields": { | 	"fields": { | ||||||
| 	    "model": [ | 	    "model": [ | ||||||
| 		"food", | 		"food", | ||||||
|                 "basicfood" | 		"food" | ||||||
| 	    ], | 	    ], | ||||||
|             "query": "{\"is_ready\": true}", | 	    "query": "{\"owner\": [\"club\"]}", | ||||||
| 	    "type": "view", | 	    "type": "view", | ||||||
|             "mask": 3, | 	    "mask": 2, | ||||||
|             "field": "", |  | ||||||
| 	    "permanent": false, | 	    "permanent": false, | ||||||
|             "description": "Voir toute la bouffe active" | 	    "description": "Voir la bouffe de son club" | ||||||
| 	} | 	} | ||||||
|     }, |     }, | ||||||
|     { |     { | ||||||
| @@ -3440,14 +3432,13 @@ | |||||||
| 	"fields": { | 	"fields": { | ||||||
| 	    "model": [ | 	    "model": [ | ||||||
| 		"food", | 		"food", | ||||||
|                 "basicfood" | 		"qrcode" | ||||||
| 	    ], | 	    ], | ||||||
|             "query": "{\"is_ready\": true, \"owner\": [\"club\"]}", | 	    "query": "{\"food_container__owner\": [\"club\"]}", | ||||||
|             "type": "view", | 	    "type": "add", | ||||||
|             "mask": 3, | 	    "mask": 2, | ||||||
|             "field": "", |  | ||||||
| 	    "permanent": false, | 	    "permanent": false, | ||||||
|             "description": "Voir la bouffe active de son club" | 	    "description": "Ajouter un QR-code pour son club" | ||||||
| 	} | 	} | ||||||
|     }, |     }, | ||||||
|     { |     { | ||||||
| @@ -3456,14 +3447,13 @@ | |||||||
| 	"fields": { | 	"fields": { | ||||||
| 	    "model": [ | 	    "model": [ | ||||||
| 		"food", | 		"food", | ||||||
|                 "basicfood" | 		"food" | ||||||
| 	    ], | 	    ], | ||||||
|             "query": "{}", | 	    "query": "{\"owner\": [\"club\"]}", | ||||||
|             "type": "change", | 	    "type": "add", | ||||||
|             "mask": 3, | 	    "mask": 2, | ||||||
|             "field": "", |  | ||||||
| 	    "permanent": false, | 	    "permanent": false, | ||||||
|             "description": "Modifier de la bouffe" | 	    "description": "Ajouter de la bouffe appartenant à son club" | ||||||
| 	} | 	} | ||||||
|     }, |     }, | ||||||
|     { |     { | ||||||
| @@ -3472,270 +3462,28 @@ | |||||||
| 	"fields": { | 	"fields": { | ||||||
| 	    "model": [ | 	    "model": [ | ||||||
| 		"food", | 		"food", | ||||||
|                 "basicfood" | 		"food" | ||||||
| 	    ], | 	    ], | ||||||
|             "query": "{\"is_ready\": true}", | 	    "query": "{\"owner\": [\"club\"]}", | ||||||
| 	    "type": "change", | 	    "type": "change", | ||||||
|             "mask": 3, | 	    "mask": 2, | ||||||
|             "field": "allergens", |  | ||||||
| 	    "permanent": false, | 	    "permanent": false, | ||||||
|             "description": "Modifier les allergènes de la bouffe existante" | 	    "description": "Modifier la bouffe appartenant à son club" | ||||||
| 	} | 	} | ||||||
|     }, |     }, | ||||||
|     { |     { | ||||||
| 	"model": "permission.permission", | 	"model": "permission.permission", | ||||||
| 	"pk": 222, | 	"pk": 222, | ||||||
|         "fields": { |  | ||||||
|             "model": [ |  | ||||||
|                 "food", |  | ||||||
|                 "basicfood" |  | ||||||
|             ], |  | ||||||
|             "query": "{\"is_ready\": true, \"owner\": [\"club\"]}", |  | ||||||
|             "type": "change", |  | ||||||
|             "mask": 3, |  | ||||||
|             "field": "allergens", |  | ||||||
|             "permanent": false, |  | ||||||
|             "description": "Modifier les allergènes de la bouffe appartenant à son club" |  | ||||||
|         } |  | ||||||
|     }, |  | ||||||
|     { |  | ||||||
|         "model": "permission.permission", |  | ||||||
|         "pk": 223, |  | ||||||
|         "fields": { |  | ||||||
|             "model": [ |  | ||||||
|                 "food", |  | ||||||
|                 "transformedfood" |  | ||||||
|             ], |  | ||||||
|             "query": "{}", |  | ||||||
|             "type": "add", |  | ||||||
|             "mask": 3, |  | ||||||
|             "field": "", |  | ||||||
|             "permanent": false, |  | ||||||
|             "description": "Créer un plat" |  | ||||||
|         } |  | ||||||
|     }, |  | ||||||
|     { |  | ||||||
|         "model": "permission.permission", |  | ||||||
|         "pk": 224, |  | ||||||
|         "fields": { |  | ||||||
|             "model": [ |  | ||||||
|                 "food", |  | ||||||
|                 "transformedfood" |  | ||||||
|             ], |  | ||||||
|             "query": "{\"owner\": [\"club\"]}", |  | ||||||
|             "type": "add", |  | ||||||
|             "mask": 3, |  | ||||||
|             "field": "", |  | ||||||
|             "permanent": false, |  | ||||||
|             "description": "Créer un plat pour son club" |  | ||||||
|         } |  | ||||||
|     }, |  | ||||||
|     { |  | ||||||
|         "model": "permission.permission", |  | ||||||
|         "pk": 225, |  | ||||||
|         "fields": { |  | ||||||
|             "model": [ |  | ||||||
|                 "food", |  | ||||||
|                 "transformedfood" |  | ||||||
|             ], |  | ||||||
|             "query": "{}", |  | ||||||
|             "type": "change", |  | ||||||
|             "mask": 3, |  | ||||||
|             "field": "", |  | ||||||
|             "permanent": false, |  | ||||||
|             "description": "Modifier tout les plats" |  | ||||||
|         } |  | ||||||
|     }, |  | ||||||
|     { |  | ||||||
|         "model": "permission.permission", |  | ||||||
|         "pk": 226, |  | ||||||
|         "fields": { |  | ||||||
|             "model": [ |  | ||||||
|                 "food", |  | ||||||
|                 "transformedfood" |  | ||||||
|             ], |  | ||||||
|             "query": "{\"is_ready\": true}", |  | ||||||
|             "type": "change", |  | ||||||
|             "mask": 3, |  | ||||||
|             "field": "end_of_life", |  | ||||||
|             "permanent": false, |  | ||||||
|             "description": "Indiquer si un plat a été mangé" |  | ||||||
|         } |  | ||||||
|     }, |  | ||||||
|     { |  | ||||||
|         "model": "permission.permission", |  | ||||||
|         "pk": 227, |  | ||||||
|         "fields": { |  | ||||||
|             "model": [ |  | ||||||
|                 "food", |  | ||||||
|                 "transformedfood" |  | ||||||
|             ], |  | ||||||
|             "query": "{\"is_ready\": true, \"owner\": [\"club\"]}", |  | ||||||
|             "type": "change", |  | ||||||
|             "mask": 3, |  | ||||||
|             "field": "is_ready", |  | ||||||
|             "permanent": false, |  | ||||||
|             "description": "Indiquer si un plat de son club est prêt" |  | ||||||
|         } |  | ||||||
|     }, |  | ||||||
|     { |  | ||||||
|         "model": "permission.permission", |  | ||||||
|         "pk": 228, |  | ||||||
|         "fields": { |  | ||||||
|             "model": [ |  | ||||||
|                 "food", |  | ||||||
|                 "transformedfood" |  | ||||||
|             ], |  | ||||||
|             "query": "{\"is_ready\": true}", |  | ||||||
|             "type": "change", |  | ||||||
|             "mask": 3, |  | ||||||
|             "field": "is_ready", |  | ||||||
|             "permanent": false, |  | ||||||
|             "description": "Archiver un plat" |  | ||||||
|         } |  | ||||||
|     }, |  | ||||||
|     { |  | ||||||
|         "model": "permission.permission", |  | ||||||
|         "pk": 229, |  | ||||||
|         "fields": { |  | ||||||
|             "model": [ |  | ||||||
|                 "food", |  | ||||||
|                 "basicfood" |  | ||||||
|             ], |  | ||||||
|             "query": "{\"is_ready\": true}", |  | ||||||
|             "type": "change", |  | ||||||
|             "mask": 3, |  | ||||||
|             "field": "is_ready", |  | ||||||
|             "permanent": false, |  | ||||||
|             "description": "Archiver de la bouffe" |  | ||||||
|         } |  | ||||||
|     }, |  | ||||||
|     { |  | ||||||
|         "model": "permission.permission", |  | ||||||
|         "pk": 231, |  | ||||||
|         "fields": { |  | ||||||
|             "model": [ |  | ||||||
|                 "food", |  | ||||||
|                 "qrcode" |  | ||||||
|             ], |  | ||||||
|             "query": "{}", |  | ||||||
|             "type": "view", |  | ||||||
|             "mask": 3, |  | ||||||
|             "field": "", |  | ||||||
|             "permanent": false, |  | ||||||
|             "description": "Voir tous les QR codes" |  | ||||||
|         } |  | ||||||
|     }, |  | ||||||
|     { |  | ||||||
|         "model": "permission.permission", |  | ||||||
|         "pk": 232, |  | ||||||
|         "fields": { |  | ||||||
|             "model": [ |  | ||||||
|                 "food", |  | ||||||
|                 "qrcode" |  | ||||||
|             ], |  | ||||||
|             "query": "{\"food_container__is_ready\": true}", |  | ||||||
|             "type": "view", |  | ||||||
|             "mask": 3, |  | ||||||
|             "field": "", |  | ||||||
|             "permanent": false, |  | ||||||
|             "description": "Voir tous les QR codes actifs" |  | ||||||
|         } |  | ||||||
|     }, |  | ||||||
|     { |  | ||||||
|         "model": "permission.permission", |  | ||||||
|         "pk": 233, |  | ||||||
|         "fields": { |  | ||||||
|             "model": [ |  | ||||||
|                 "food", |  | ||||||
|                 "qrcode" |  | ||||||
|             ], |  | ||||||
|             "query": "{\"food_container__owner\": [\"club\"], \"food_container__is_ready\": true}", |  | ||||||
|             "type": "view", |  | ||||||
|             "mask": 3, |  | ||||||
|             "field": "", |  | ||||||
|             "permanent": false, |  | ||||||
|             "description": "Voir tous les QR codes actifs de son club" |  | ||||||
|         } |  | ||||||
|     }, |  | ||||||
|     { |  | ||||||
|         "model": "permission.permission", |  | ||||||
|         "pk" : 234, |  | ||||||
|         "fields": { |  | ||||||
|             "model": [ |  | ||||||
|                 "food", |  | ||||||
|                 "transformedfood" |  | ||||||
|             ], |  | ||||||
|             "query": "{\"owner\": [\"club\"]}", |  | ||||||
|             "type": "change", |  | ||||||
|             "mask": 3, |  | ||||||
|             "field": "ingredients", |  | ||||||
|             "permanent": false, |  | ||||||
|             "description": "Changer les ingrédients d'un plat actif de son club" |  | ||||||
|         } |  | ||||||
|     }, |  | ||||||
|     { |  | ||||||
|         "model": "permission.permission", |  | ||||||
|         "pk": 235, |  | ||||||
| 	"fields": { | 	"fields": { | ||||||
| 	    "model": [ | 	    "model": [ | ||||||
| 	        "food", | 	        "food", | ||||||
|     		"food" |     		"food" | ||||||
| 	    ], | 	    ], | ||||||
|             "query": "{}", | 	    "query": "{\"end_of_life\": \"\"}", | ||||||
| 	    "type": "view", | 	    "type": "view", | ||||||
|             "mask": 3, | 	    "mask": 1, | ||||||
|             "field": "", |  | ||||||
| 	    "permanent": false, | 	    "permanent": false, | ||||||
|             "description": "Voir bouffe" | 	    "description": "Voir la bouffe servie" | ||||||
|         } |  | ||||||
|     }, |  | ||||||
|     { |  | ||||||
|         "model": "permission.permission", |  | ||||||
|         "pk": 236, |  | ||||||
|         "fields": { |  | ||||||
|             "model": [ |  | ||||||
|                 "food", |  | ||||||
|                 "food" |  | ||||||
|             ], |  | ||||||
|             "query": "[]", |  | ||||||
|             "type": "view", |  | ||||||
|             "mask": 3, |  | ||||||
|             "field": "", |  | ||||||
|             "permanent": false, |  | ||||||
|             "description": "Voir bouffe active" |  | ||||||
|         } |  | ||||||
|     }, |  | ||||||
|     { |  | ||||||
|         "model": "permission.permission", |  | ||||||
|         "pk": 237, |  | ||||||
|         "fields": { |  | ||||||
|             "model": [ |  | ||||||
|                 "food", |  | ||||||
|                 "food" |  | ||||||
|             ], |  | ||||||
|             "query": "{\"owner\": [\"club\"]}", |  | ||||||
|             "type": "view", |  | ||||||
|             "mask": 3, |  | ||||||
|             "field": "", |  | ||||||
|             "permanent": false, |  | ||||||
|             "description": "Voir bouffe active de son club" |  | ||||||
|         } |  | ||||||
|     }, |  | ||||||
|     { |  | ||||||
|         "model": "permission.permission", |  | ||||||
|         "pk": 238, |  | ||||||
|         "fields": { |  | ||||||
|             "model": [ |  | ||||||
|                 "food", |  | ||||||
|                 "food" |  | ||||||
|             ], |  | ||||||
|             "query": "{}", |  | ||||||
|             "type": "change", |  | ||||||
|             "mask": 3, |  | ||||||
|             "field": "", |  | ||||||
|             "permanent": false, |  | ||||||
|             "description": "Modifier bouffe" |  | ||||||
| 	} | 	} | ||||||
|     }, |     }, | ||||||
|     { |     { | ||||||
| @@ -4343,7 +4091,8 @@ | |||||||
|                 158, |                 158, | ||||||
|                 159, |                 159, | ||||||
|                 160, |                 160, | ||||||
|                 213 | 		212, | ||||||
|  | 		222 | ||||||
|             ] |             ] | ||||||
|         } |         } | ||||||
|     }, |     }, | ||||||
| @@ -4384,16 +4133,11 @@ | |||||||
|                 50, |                 50, | ||||||
|                 141, |                 141, | ||||||
|                 169, |                 169, | ||||||
|                 212, | 		217, | ||||||
|                 214, | 		218, | ||||||
|                 215, |  | ||||||
| 		219, | 		219, | ||||||
|                 222, | 		220, | ||||||
|                 224, | 		221, | ||||||
|                 227, |  | ||||||
|                 233, |  | ||||||
|                 234, |  | ||||||
|                 237, |  | ||||||
| 		247, | 		247, | ||||||
| 		258, | 		258, | ||||||
| 		259 | 		259 | ||||||
| @@ -4574,20 +4318,7 @@ | |||||||
|                 166, |                 166, | ||||||
|                 167, |                 167, | ||||||
|                 168, |                 168, | ||||||
|                 182, |                 182 | ||||||
|                 212, |  | ||||||
|                 214, |  | ||||||
|                 215, |  | ||||||
|                 218, |  | ||||||
|                 221, |  | ||||||
|                 224, |  | ||||||
|                 226, |  | ||||||
|                 227, |  | ||||||
|                 228, |  | ||||||
|                 229, |  | ||||||
|                 232, |  | ||||||
|                 234, |  | ||||||
|                 236 |  | ||||||
|             ] |             ] | ||||||
|         } |         } | ||||||
|     }, |     }, | ||||||
| @@ -4795,8 +4526,7 @@ | |||||||
|                 168, |                 168, | ||||||
|                 176, |                 176, | ||||||
|                 177, |                 177, | ||||||
|                 197, |                 197 | ||||||
|                 211 |  | ||||||
|             ] |             ] | ||||||
|         } |         } | ||||||
|     }, |     }, | ||||||
| @@ -4824,15 +4554,11 @@ | |||||||
|             "permissions": [ |             "permissions": [ | ||||||
|                 137, |                 137, | ||||||
|                 211, |                 211, | ||||||
|  |                 212, | ||||||
|  | 		213, | ||||||
| 		214, | 		214, | ||||||
|                 216, | 		215, | ||||||
|                 217, | 		216 | ||||||
|                 220, |  | ||||||
|                 223, |  | ||||||
|                 225, |  | ||||||
|                 231, |  | ||||||
|                 235, |  | ||||||
|                 238 |  | ||||||
|             ] |             ] | ||||||
|         } |         } | ||||||
|     },   |     },   | ||||||
|   | |||||||
| @@ -19,8 +19,9 @@ Le modèle regroupe : | |||||||
| * Propriétaire (doit-être un Club) | * Propriétaire (doit-être un Club) | ||||||
| * Allergènes (ManyToManyField) | * Allergènes (ManyToManyField) | ||||||
| * date d'expiration | * date d'expiration | ||||||
| * a été mangé (booléen) | * fin de vie | ||||||
| * est prêt (booléen) | * est prêt (booléen) | ||||||
|  | * consigne (pour les GCKs) | ||||||
|  |  | ||||||
| BasicFood | BasicFood | ||||||
| ~~~~~~~~~ | ~~~~~~~~~ | ||||||
| @@ -40,7 +41,7 @@ Les TransformedFood correspondent aux produits préparés à la Kfet. Ils peuven | |||||||
|  |  | ||||||
| Le modèle regroupe : | Le modèle regroupe : | ||||||
|  |  | ||||||
| * Durée de consommation (par défaut 3 jours) | * Durée de conservation (par défaut 3 jours) | ||||||
| * Ingrédients (ManyToManyField vers Food) | * Ingrédients (ManyToManyField vers Food) | ||||||
| * Date de création | * Date de création | ||||||
| * Champs de Food | * Champs de Food | ||||||
|   | |||||||
| @@ -12,6 +12,7 @@ Applications de la Note Kfet 2020 | |||||||
|    ../api/index |    ../api/index | ||||||
|    registration |    registration | ||||||
|    logs |    logs | ||||||
|  |    food | ||||||
|    treasury |    treasury | ||||||
|    wei |    wei | ||||||
|    wrapped |    wrapped | ||||||
| @@ -66,6 +67,8 @@ Applications facultatives | |||||||
|     Serveur central d'authentification, permet d'utiliser son compte de la NoteKfet2020 pour se connecter à d'autre application ayant intégrer un client. |     Serveur central d'authentification, permet d'utiliser son compte de la NoteKfet2020 pour se connecter à d'autre application ayant intégrer un client. | ||||||
| * `Scripts <https://gitlab.crans.org/bde/nk20-scripts>`_ | * `Scripts <https://gitlab.crans.org/bde/nk20-scripts>`_ | ||||||
|      Ensemble de commande `./manage.py` pour la gestion de la note: import de données, verification d'intégrité, etc... |      Ensemble de commande `./manage.py` pour la gestion de la note: import de données, verification d'intégrité, etc... | ||||||
|  | * `Food <food>`_ : | ||||||
|  |     Gestion de la nourriture dans Kfet pour les clubs. | ||||||
| * `Treasury <treasury>`_ : | * `Treasury <treasury>`_ : | ||||||
|     Interface de gestion pour les trésorièr⋅es, émission de factures, remises de chèque, statistiques... |     Interface de gestion pour les trésorièr⋅es, émission de factures, remises de chèque, statistiques... | ||||||
| * `WEI <wei>`_ : | * `WEI <wei>`_ : | ||||||
|   | |||||||
| @@ -183,6 +183,7 @@ Contributeur⋅rices | |||||||
|    * korenst1 |    * korenst1 | ||||||
|    * nicomarg |    * nicomarg | ||||||
|    * PAC |    * PAC | ||||||
|  |    * Quark | ||||||
|    * ÿnérant |    * ÿnérant | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
		Reference in New Issue
	
	Block a user