mirror of
				https://gitlab.crans.org/bde/nk20
				synced 2025-10-26 05:23:18 +01:00 
			
		
		
		
	Compare commits
	
		
			23 Commits
		
	
	
		
			c4404ef995
			...
			darbonne
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 5a8acbde00 | ||
|  | f60dc8cfa0 | ||
|  | 067dd6f9d1 | ||
|  | 7b1e32e514 | ||
|  | 3d34270959 | ||
|  | 3bb99671ec | ||
|  | 0d69383dfd | ||
|  | 7b9ff119e8 | ||
|  | 108a56745c | ||
|  | 9643d7652b | ||
|  | fadb289ed7 | ||
|  | 905fc6e7cc | ||
|  | cdd81c1444 | ||
|  | 4afafceba1 | ||
|  | 3065eacc96 | ||
|  | 71ef3aedd8 | ||
|  | 0cf11c6348 | ||
|  | 70abd0f490 | ||
|  | 4445dd4a96 | ||
|  | 03932672f3 | ||
|  | dc6a40de02 | ||
|  | d58a299a8b | ||
|  | ad0a219ed3 | 
							
								
								
									
										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. | ||||||
|   | |||||||
| @@ -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,7 +7,7 @@ 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, CreateView | 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 | ||||||
| @@ -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,11 @@ 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).filter( |             end_of_life='', is_ready=True).exclude( | ||||||
|                 Q(polymorphic_ctype__model='basicfood', |                 Q(polymorphic_ctype__model='basicfood', | ||||||
|                   basicfood__date_type='DLC', |                   basicfood__date_type='DLC', | ||||||
|                   expiry_date__lte=timezone.now(),) |                   expiry_date__lte=timezone.now(),) | ||||||
| @@ -106,7 +108,7 @@ class FoodListView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin, Li | |||||||
|         return context |         return context | ||||||
|  |  | ||||||
|  |  | ||||||
| class QRCodeCreateView(ProtectQuerysetMixin, CreateView): | class QRCodeCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView): | ||||||
|     """ |     """ | ||||||
|     A view to add qrcode |     A view to add qrcode | ||||||
|     """ |     """ | ||||||
| @@ -166,7 +168,8 @@ class BasicFoodCreateView(ProtectQuerysetMixin, ProtectedCreateView): | |||||||
|     template_name = "food/food_update.html" |     template_name = "food/food_update.html" | ||||||
|  |  | ||||||
|     def get_sample_object(self): |     def get_sample_object(self): | ||||||
|         return BasicFood( |         # We choose a club which may work or BDE else | ||||||
|  |         food = BasicFood( | ||||||
|             name="", |             name="", | ||||||
|             owner_id=1, |             owner_id=1, | ||||||
|             expiry_date=timezone.now(), |             expiry_date=timezone.now(), | ||||||
| @@ -175,6 +178,14 @@ class BasicFoodCreateView(ProtectQuerysetMixin, ProtectedCreateView): | |||||||
|             date_type='DLC', |             date_type='DLC', | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|  |         for membership in self.request.user.memberships.all(): | ||||||
|  |             club_id = membership.club.id | ||||||
|  |             food.owner_id = club_id | ||||||
|  |             if PermissionBackend.check_perm(self.request, "food.add_basicfood", food): | ||||||
|  |                 return food | ||||||
|  |  | ||||||
|  |         return food | ||||||
|  |  | ||||||
|     @transaction.atomic |     @transaction.atomic | ||||||
|     def form_valid(self, form): |     def form_valid(self, form): | ||||||
|         if QRCode.objects.filter(qr_code_number=self.kwargs['slug']).count() > 0: |         if QRCode.objects.filter(qr_code_number=self.kwargs['slug']).count() > 0: | ||||||
| @@ -225,13 +236,22 @@ class TransformedFoodCreateView(ProtectQuerysetMixin, ProtectedCreateView): | |||||||
|     template_name = "food/food_update.html" |     template_name = "food/food_update.html" | ||||||
|  |  | ||||||
|     def get_sample_object(self): |     def get_sample_object(self): | ||||||
|         return TransformedFood( |         # We choose a club which may work or BDE else | ||||||
|  |         food = TransformedFood( | ||||||
|             name="", |             name="", | ||||||
|             owner_id=1, |             owner_id=1, | ||||||
|             expiry_date=timezone.now(), |             expiry_date=timezone.now(), | ||||||
|             is_ready=True, |             is_ready=True, | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|  |         for membership in self.request.user.memberships.all(): | ||||||
|  |             club_id = membership.club.id | ||||||
|  |             food.owner_id = club_id | ||||||
|  |             if PermissionBackend.check_perm(self.request, "food.add_transformedfood", food): | ||||||
|  |                 return food | ||||||
|  |  | ||||||
|  |         return food | ||||||
|  |  | ||||||
|     @transaction.atomic |     @transaction.atomic | ||||||
|     def form_valid(self, form): |     def form_valid(self, form): | ||||||
|         form.instance.expiry_date = timezone.now() + timedelta(days=3) |         form.instance.expiry_date = timezone.now() + timedelta(days=3) | ||||||
| @@ -243,7 +263,80 @@ 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 = 100 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class ManageIngredientsView(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() | ||||||
|  |         # We recalculate new expiry date and allergens | ||||||
|  |         self.object.expiry_date = self.object.creation_date + self.object.shelf_life | ||||||
|  |         self.object.allergens.clear() | ||||||
|  |  | ||||||
|  |         for ingredient in self.object.ingredients.iterator(): | ||||||
|  |             if not (ingredient.polymorphic_ctype.model == 'basicfood' and ingredient.date_type == 'DDM'): | ||||||
|  |                 self.object.expiry_date = min(self.object.expiry_date, ingredient.expiry_date) | ||||||
|  |             self.object.allergens.set(self.object.allergens.union(ingredient.allergens.all())) | ||||||
|  |  | ||||||
|  |         self.object.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 | ||||||
|     """ |     """ | ||||||
| @@ -366,6 +459,8 @@ class FoodDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): | |||||||
|         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) | ||||||
| @@ -404,6 +499,7 @@ 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): | ||||||
|   | |||||||
							
								
								
									
										46
									
								
								apps/member/migrations/0014_create_bda.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								apps/member/migrations/0014_create_bda.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,46 @@ | |||||||
|  | from django.db import migrations | ||||||
|  |  | ||||||
|  | def create_bda(apps, schema_editor): | ||||||
|  |     """ | ||||||
|  |     The club BDA is now pre-injected. | ||||||
|  |     """ | ||||||
|  |     Club = apps.get_model("member", "club") | ||||||
|  |     NoteClub = apps.get_model("note", "noteclub") | ||||||
|  |     Alias = apps.get_model("note", "alias") | ||||||
|  |     ContentType = apps.get_model('contenttypes', 'ContentType') | ||||||
|  |     polymorphic_ctype_id = ContentType.objects.get_for_model(NoteClub).id | ||||||
|  |      | ||||||
|  |     Club.objects.get_or_create( | ||||||
|  |         id=10, | ||||||
|  |         name="BDA", | ||||||
|  |         email="bda.ensparissaclay@gmail.com", | ||||||
|  |         require_memberships=True, | ||||||
|  |         membership_fee_paid=750, | ||||||
|  |         membership_fee_unpaid=750, | ||||||
|  |         membership_duration=396, | ||||||
|  |         membership_start="2024-08-01", | ||||||
|  |         membership_end="2025-09-30", | ||||||
|  |     ) | ||||||
|  |     NoteClub.objects.get_or_create( | ||||||
|  |         id=1937, | ||||||
|  |         club_id=10, | ||||||
|  |         polymorphic_ctype_id=polymorphic_ctype_id, | ||||||
|  |     ) | ||||||
|  |     Alias.objects.get_or_create( | ||||||
|  |         id=1937, | ||||||
|  |         note_id=1937, | ||||||
|  |         name="BDA", | ||||||
|  |         normalized_name="bda", | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Migration(migrations.Migration): | ||||||
|  |  | ||||||
|  |     dependencies = [ | ||||||
|  |         ('member', '0013_auto_20240801_1436'), | ||||||
|  |     ] | ||||||
|  |      | ||||||
|  |     operations = [ | ||||||
|  |         migrations.RunPython(create_bda), | ||||||
|  |     ] | ||||||
|  |  | ||||||
| @@ -4152,8 +4152,8 @@ | |||||||
|             "name": "Pr\u00e9sident\u22c5e de club", |             "name": "Pr\u00e9sident\u22c5e de club", | ||||||
|             "permissions": [ |             "permissions": [ | ||||||
|                 62, |                 62, | ||||||
|                 142, |                 135, | ||||||
|                 135 |                 142 | ||||||
|             ] |             ] | ||||||
|         } |         } | ||||||
|     }, |     }, | ||||||
| @@ -4562,6 +4562,133 @@ | |||||||
|             ] |             ] | ||||||
|         } |         } | ||||||
|     },   |     },   | ||||||
|  |     { | ||||||
|  |         "model": "permission.role", | ||||||
|  |         "pk": 23, | ||||||
|  |             "fields": { | ||||||
|  |             "for_club": 2, | ||||||
|  |             "name": "Darbonne", | ||||||
|  |             "permissions": [ | ||||||
|  |                 30, | ||||||
|  |                 31, | ||||||
|  |                 32 | ||||||
|  |             ] | ||||||
|  |         } | ||||||
|  |     },  | ||||||
|  |     { | ||||||
|  |         "model": "permission.role", | ||||||
|  |         "pk": 24, | ||||||
|  |             "fields": { | ||||||
|  |             "for_club": null, | ||||||
|  |             "name": "Staffeur⋅euse (S&L,Respo Tech,...)", | ||||||
|  |             "permissions": [] | ||||||
|  |         } | ||||||
|  |     },  | ||||||
|  |     { | ||||||
|  |         "model": "permission.role", | ||||||
|  |         "pk": 25, | ||||||
|  |             "fields": { | ||||||
|  |             "for_club": null, | ||||||
|  |             "name": "Référent⋅e Bus", | ||||||
|  |             "permissions": [ | ||||||
|  |                 22, | ||||||
|  |                 84, | ||||||
|  |                 115, | ||||||
|  |                 117, | ||||||
|  |                 118, | ||||||
|  |                 119, | ||||||
|  |                 120, | ||||||
|  |                 121, | ||||||
|  |                 122 | ||||||
|  |             ] | ||||||
|  |         } | ||||||
|  |     },  | ||||||
|  |     { | ||||||
|  |         "model": "permission.role", | ||||||
|  |         "pk": 28, | ||||||
|  |             "fields": { | ||||||
|  |             "for_club": 10, | ||||||
|  |             "name": "Trésorièr⸱e BDA", | ||||||
|  |             "permissions": [ | ||||||
|  |                 55, | ||||||
|  |                 56, | ||||||
|  |                 57, | ||||||
|  |                 58, | ||||||
|  |                 135, | ||||||
|  |                 143, | ||||||
|  |                 176, | ||||||
|  |                 177, | ||||||
|  |                 178, | ||||||
|  |                 243, | ||||||
|  |                 260, | ||||||
|  |                 261, | ||||||
|  |                 262, | ||||||
|  |                 263, | ||||||
|  |                 264, | ||||||
|  |                 265, | ||||||
|  |                 266, | ||||||
|  |                 267, | ||||||
|  |                 268, | ||||||
|  |                 269 | ||||||
|  |             ] | ||||||
|  |         } | ||||||
|  |     },  | ||||||
|  |     { | ||||||
|  |         "model": "permission.role", | ||||||
|  |         "pk": 30, | ||||||
|  |             "fields": { | ||||||
|  |             "for_club": 10, | ||||||
|  |             "name": "Respo sorties", | ||||||
|  |             "permissions": [ | ||||||
|  |                 49,  | ||||||
|  |                 62,  | ||||||
|  |                 141,  | ||||||
|  |                 241,  | ||||||
|  |                 242,  | ||||||
|  |                 243 | ||||||
|  |             ] | ||||||
|  |         } | ||||||
|  |     },  | ||||||
|  |     { | ||||||
|  |         "model": "permission.role", | ||||||
|  |         "pk": 31, | ||||||
|  |             "fields": { | ||||||
|  |             "for_club": 1, | ||||||
|  |             "name": "Respo comm", | ||||||
|  |             "permissions": [ | ||||||
|  |                 135, | ||||||
|  |                 244 | ||||||
|  |             ] | ||||||
|  |         } | ||||||
|  |     },  | ||||||
|  |     { | ||||||
|  |         "model": "permission.role", | ||||||
|  |         "pk": 32, | ||||||
|  |             "fields": { | ||||||
|  |             "for_club": 10, | ||||||
|  |             "name": "Respo comm Art", | ||||||
|  |             "permissions": [ | ||||||
|  |                 135, | ||||||
|  |                 245 | ||||||
|  |             ] | ||||||
|  |         } | ||||||
|  |     },  | ||||||
|  |     { | ||||||
|  |         "model": "permission.role", | ||||||
|  |         "pk": 33, | ||||||
|  |             "fields": { | ||||||
|  |             "for_club": 10, | ||||||
|  |             "name": "Respo Jam", | ||||||
|  |             "permissions": [ | ||||||
|  |                 247,  | ||||||
|  |                 250,  | ||||||
|  |                 251,  | ||||||
|  |                 252,  | ||||||
|  |                 253,  | ||||||
|  |                 254 | ||||||
|  |             ] | ||||||
|  |         } | ||||||
|  |     },  | ||||||
|     { |     { | ||||||
|         "model": "wei.weirole", |         "model": "wei.weirole", | ||||||
|         "pk": 12, |         "pk": 12, | ||||||
| @@ -4596,5 +4723,15 @@ | |||||||
|         "model": "wei.weirole", |         "model": "wei.weirole", | ||||||
|         "pk": 18, |         "pk": 18, | ||||||
|         "fields": {} |         "fields": {} | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |         "model": "wei.weirole", | ||||||
|  |         "pk": 24, | ||||||
|  |         "fields": {} | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |         "model": "wei.weirole", | ||||||
|  |         "pk": 25, | ||||||
|  |         "fields": {} | ||||||
|     } |     } | ||||||
| ] | ] | ||||||
|   | |||||||
| @@ -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 | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -7,7 +7,7 @@ msgid "" | |||||||
| msgstr "" | msgstr "" | ||||||
| "Project-Id-Version: \n" | "Project-Id-Version: \n" | ||||||
| "Report-Msgid-Bugs-To: \n" | "Report-Msgid-Bugs-To: \n" | ||||||
| "POT-Creation-Date: 2025-04-24 18:22+0200\n" | "POT-Creation-Date: 2025-04-30 11:44+0200\n" | ||||||
| "PO-Revision-Date: 2022-04-11 22:05+0200\n" | "PO-Revision-Date: 2022-04-11 22:05+0200\n" | ||||||
| "Last-Translator: bleizi <bleizi@crans.org>\n" | "Last-Translator: bleizi <bleizi@crans.org>\n" | ||||||
| "Language-Team: French <http://translate.ynerant.fr/projects/nk20/nk20/fr/>\n" | "Language-Team: French <http://translate.ynerant.fr/projects/nk20/nk20/fr/>\n" | ||||||
| @@ -383,7 +383,9 @@ msgstr "Entrée effectuée !" | |||||||
|  |  | ||||||
| #: apps/activity/templates/activity/activity_form.html:16 | #: apps/activity/templates/activity/activity_form.html:16 | ||||||
| #: apps/food/templates/food/food_update.html:17 | #: apps/food/templates/food/food_update.html:17 | ||||||
|  | #: apps/food/templates/food/manage_ingredients.html:48 | ||||||
| #: apps/food/templates/food/qrcode.html:18 | #: apps/food/templates/food/qrcode.html:18 | ||||||
|  | #: apps/food/templates/food/transformedfood_update.html:45 | ||||||
| #: apps/member/templates/member/add_members.html:46 | #: apps/member/templates/member/add_members.html:46 | ||||||
| #: apps/member/templates/member/club_form.html:16 | #: apps/member/templates/member/club_form.html:16 | ||||||
| #: apps/note/templates/note/transactiontemplate_form.html:18 | #: apps/note/templates/note/transactiontemplate_form.html:18 | ||||||
| @@ -495,26 +497,40 @@ msgstr "API" | |||||||
| msgid "food" | msgid "food" | ||||||
| msgstr "bouffe" | msgstr "bouffe" | ||||||
|  |  | ||||||
| #: apps/food/forms.py:50 | #: apps/food/forms.py:49 | ||||||
| msgid "Pasta METRO 5kg" | msgid "Pasta METRO 5kg" | ||||||
| msgstr "Pâtes METRO 5kg" | msgstr "Pâtes METRO 5kg" | ||||||
|  |  | ||||||
| #: apps/food/forms.py:54 apps/food/forms.py:82 | #: apps/food/forms.py:53 apps/food/forms.py:81 | ||||||
| msgid "Specific order given to GCKs" | msgid "Specific order given to GCKs" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: apps/food/forms.py:78 | #: apps/food/forms.py:77 | ||||||
| msgid "Lasagna" | msgid "Lasagna" | ||||||
| msgstr "Lasagnes" | msgstr "Lasagnes" | ||||||
|  |  | ||||||
| #: apps/food/forms.py:117 | #: apps/food/forms.py:116 | ||||||
| msgid "Shelf life (in hours)" | msgid "Shelf life (in hours)" | ||||||
| msgstr "Durée de vie (en heure)" | msgstr "Durée de vie (en heure)" | ||||||
|  |  | ||||||
| #: apps/food/forms.py:139 | #: apps/food/forms.py:138 apps/food/forms.py:162 | ||||||
|  | #: apps/food/templates/food/transformedfood_update.html:25 | ||||||
| msgid "Fully used" | msgid "Fully used" | ||||||
| msgstr "Entièrement utilisé" | msgstr "Entièrement utilisé" | ||||||
|  |  | ||||||
|  | #: apps/food/forms.py:171 apps/food/templates/food/qrcode.html:29 | ||||||
|  | #: apps/food/templates/food/transformedfood_update.html:23 | ||||||
|  | #: apps/note/templates/note/transaction_form.html:132 | ||||||
|  | #: apps/treasury/models.py:61 | ||||||
|  | msgid "Name" | ||||||
|  | msgstr "Nom" | ||||||
|  |  | ||||||
|  | #: apps/food/forms.py:181 | ||||||
|  | #, fuzzy | ||||||
|  | #| msgid "QR-code number" | ||||||
|  | msgid "QR code number" | ||||||
|  | msgstr "numéro de QR-code" | ||||||
|  |  | ||||||
| #: apps/food/models.py:23 | #: apps/food/models.py:23 | ||||||
| msgid "Allergen" | msgid "Allergen" | ||||||
| msgstr "Allergène" | msgstr "Allergène" | ||||||
| @@ -547,7 +563,7 @@ msgstr "est prêt" | |||||||
| msgid "order" | msgid "order" | ||||||
| msgstr "consigne" | msgstr "consigne" | ||||||
|  |  | ||||||
| #: apps/food/models.py:107 apps/food/views.py:32 | #: apps/food/models.py:107 apps/food/views.py:34 | ||||||
| #: note_kfet/templates/base.html:72 | #: note_kfet/templates/base.html:72 | ||||||
| msgid "Food" | msgid "Food" | ||||||
| msgstr "Bouffe" | msgstr "Bouffe" | ||||||
| @@ -605,6 +621,7 @@ msgid "QR-codes" | |||||||
| msgstr "QR-codes" | msgstr "QR-codes" | ||||||
|  |  | ||||||
| #: apps/food/models.py:286 | #: apps/food/models.py:286 | ||||||
|  | #: apps/food/templates/food/transformedfood_update.html:24 | ||||||
| msgid "QR-code number" | msgid "QR-code number" | ||||||
| msgstr "numéro de QR-code" | msgstr "numéro de QR-code" | ||||||
|  |  | ||||||
| @@ -624,7 +641,11 @@ msgstr "Modifier" | |||||||
| msgid "Add to a meal" | msgid "Add to a meal" | ||||||
| msgstr "Ajouter à un plat" | msgstr "Ajouter à un plat" | ||||||
|  |  | ||||||
| #: apps/food/templates/food/food_detail.html:44 | #: apps/food/templates/food/food_detail.html:45 | ||||||
|  | msgid "Manage ingredients" | ||||||
|  | msgstr "Gérer les ingrédients" | ||||||
|  |  | ||||||
|  | #: apps/food/templates/food/food_detail.html:49 | ||||||
| msgid "Return to the food list" | msgid "Return to the food list" | ||||||
| msgstr "Retour à la liste de nourriture" | msgstr "Retour à la liste de nourriture" | ||||||
|  |  | ||||||
| @@ -660,6 +681,16 @@ msgstr "Bouffe du club" | |||||||
| msgid "Yours club has not food yet." | msgid "Yours club has not food yet." | ||||||
| msgstr "Ton club n'a pas de bouffe pour l'instant" | msgstr "Ton club n'a pas de bouffe pour l'instant" | ||||||
|  |  | ||||||
|  | #: apps/food/templates/food/manage_ingredients.html:45 | ||||||
|  | #: apps/food/templates/food/transformedfood_update.html:42 | ||||||
|  | msgid "Add ingredient" | ||||||
|  | msgstr "Ajouter un ingrédient" | ||||||
|  |  | ||||||
|  | #: apps/food/templates/food/manage_ingredients.html:46 | ||||||
|  | #: apps/food/templates/food/transformedfood_update.html:43 | ||||||
|  | msgid "Remove ingredient" | ||||||
|  | msgstr "Enlever un ingrédient" | ||||||
|  |  | ||||||
| #: apps/food/templates/food/qrcode.html:22 | #: apps/food/templates/food/qrcode.html:22 | ||||||
| msgid "Copy constructor" | msgid "Copy constructor" | ||||||
| msgstr "Constructeur de copie" | msgstr "Constructeur de copie" | ||||||
| @@ -668,12 +699,6 @@ msgstr "Constructeur de copie" | |||||||
| msgid "New food" | msgid "New food" | ||||||
| msgstr "Nouvel aliment" | msgstr "Nouvel aliment" | ||||||
|  |  | ||||||
| #: apps/food/templates/food/qrcode.html:29 |  | ||||||
| #: apps/note/templates/note/transaction_form.html:132 |  | ||||||
| #: apps/treasury/models.py:61 |  | ||||||
| msgid "Name" |  | ||||||
| msgstr "Nom" |  | ||||||
|  |  | ||||||
| #: apps/food/templates/food/qrcode.html:32 | #: apps/food/templates/food/qrcode.html:32 | ||||||
| msgid "Owner" | msgid "Owner" | ||||||
| msgstr "Propriétaire" | msgstr "Propriétaire" | ||||||
| @@ -726,40 +751,49 @@ msgstr "semaines" | |||||||
| msgid "and" | msgid "and" | ||||||
| msgstr "et" | msgstr "et" | ||||||
|  |  | ||||||
| #: apps/food/views.py:116 | #: apps/food/views.py:118 | ||||||
| msgid "Add a new QRCode" | msgid "Add a new QRCode" | ||||||
| msgstr "Ajouter un nouveau QR-code" | msgstr "Ajouter un nouveau QR-code" | ||||||
|  |  | ||||||
| #: apps/food/views.py:165 | #: apps/food/views.py:167 | ||||||
| msgid "Add an aliment" | msgid "Add an aliment" | ||||||
| msgstr "Ajouter un nouvel aliment" | msgstr "Ajouter un nouvel aliment" | ||||||
|  |  | ||||||
| #: apps/food/views.py:224 | #: apps/food/views.py:226 | ||||||
| msgid "Add a meal" | msgid "Add a meal" | ||||||
| msgstr "Ajouter un plat" | msgstr "Ajouter un plat" | ||||||
|  |  | ||||||
| #: apps/food/views.py:251 | #: apps/food/views.py:262 | ||||||
|  | msgid "Manage ingredients of:" | ||||||
|  | msgstr "Gestion des ingrédienrs de :" | ||||||
|  |  | ||||||
|  | #: apps/food/views.py:276 apps/food/views.py:284 | ||||||
|  | #, python-brace-format | ||||||
|  | msgid "Fully used in {meal}" | ||||||
|  | msgstr "Aliment entièrement utilisé dans : {meal}" | ||||||
|  |  | ||||||
|  | #: apps/food/views.py:323 | ||||||
| msgid "Add the ingredient:" | msgid "Add the ingredient:" | ||||||
| msgstr "Ajouter l'ingrédient" | msgstr "Ajouter l'ingrédient" | ||||||
|  |  | ||||||
| #: apps/food/views.py:275 | #: apps/food/views.py:349 | ||||||
| #, python-brace-format | #, python-brace-format | ||||||
| msgid "Food fully used in : {meal.name}" | msgid "Food fully used in : {meal.name}" | ||||||
| msgstr "Aliment entièrement utilisé dans : {meal.name}" | msgstr "Aliment entièrement utilisé dans : {meal.name}" | ||||||
|  |  | ||||||
| #: apps/food/views.py:294 | #: apps/food/views.py:368 | ||||||
| msgid "Update an aliment" | msgid "Update an aliment" | ||||||
| msgstr "Modifier un aliment" | msgstr "Modifier un aliment" | ||||||
|  |  | ||||||
| #: apps/food/views.py:342 | #: apps/food/views.py:416 | ||||||
| msgid "Details of:" | msgid "Details of:" | ||||||
| msgstr "Détails de :" | msgstr "Détails de :" | ||||||
|  |  | ||||||
| #: apps/food/views.py:352 apps/treasury/tables.py:149 | #: apps/food/views.py:426 apps/treasury/tables.py:149 | ||||||
| msgid "Yes" | msgid "Yes" | ||||||
| msgstr "Oui" | msgstr "Oui" | ||||||
|  |  | ||||||
| #: apps/food/views.py:354 apps/member/models.py:99 apps/treasury/tables.py:149 | #: apps/food/views.py:428 apps/member/models.py:99 apps/treasury/tables.py:149 | ||||||
| msgid "No" | msgid "No" | ||||||
| msgstr "Non" | msgstr "Non" | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user